123 lines
4.3 KiB
Python
123 lines
4.3 KiB
Python
"""What a folder needs to be before Paperclip can give a task its own worktree.
|
|
|
|
A worktree is a git feature: Paperclip makes a directory and a branch per task
|
|
from the project's repository. A folder that is not a repository cannot have
|
|
one, and a repository with no commit has no branch to cut from. Both cases are
|
|
checked here, and neither is repaired without being asked.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
|
|
|
|
def git(folder, *args, timeout=60):
|
|
return subprocess.run(["git", "-C", folder] + list(args),
|
|
capture_output=True, text=True, timeout=timeout)
|
|
|
|
|
|
def is_repo(folder):
|
|
if not os.path.isdir(folder):
|
|
return False
|
|
return git(folder, "rev-parse", "--is-inside-work-tree").stdout.strip() == "true"
|
|
|
|
|
|
def has_commit(folder):
|
|
return git(folder, "rev-parse", "--verify", "HEAD").returncode == 0
|
|
|
|
|
|
def branch(folder):
|
|
"""The branch to cut task branches from: what is checked out, else main, else master."""
|
|
out = git(folder, "symbolic-ref", "--short", "HEAD").stdout.strip()
|
|
if out:
|
|
return out
|
|
for name in ("main", "master"):
|
|
if git(folder, "rev-parse", "--verify", name).returncode == 0:
|
|
return name
|
|
return "main"
|
|
|
|
|
|
def untracked_and_modified(folder):
|
|
out = git(folder, "status", "--porcelain").stdout.splitlines()
|
|
return len([ln for ln in out if ln.strip()])
|
|
|
|
|
|
def has_ignored(folder):
|
|
return os.path.isfile(os.path.join(folder, ".gitignore"))
|
|
|
|
|
|
def make_repo(folder, quiet=False):
|
|
"""git init, add everything, and one commit. Returns (ok, message)."""
|
|
say = (lambda *_: None) if quiet else print
|
|
steps = [
|
|
("init", ["init", "-b", "main"]),
|
|
]
|
|
for label, args in steps:
|
|
r = git(folder, *args)
|
|
if r.returncode != 0:
|
|
return False, "git %s failed: %s" % (label, (r.stderr or r.stdout).strip()[:200])
|
|
if git(folder, "add", "-A").returncode != 0:
|
|
return False, "git add failed"
|
|
r = git(folder, "-c", "user.name=Paperclip Harness",
|
|
"-c", "user.email=harness@localhost",
|
|
"commit", "-q", "-m", "Initial commit, before any Paperclip task")
|
|
if r.returncode != 0:
|
|
detail = (r.stderr or r.stdout).strip()
|
|
if "nothing to commit" in detail:
|
|
return False, "the folder has no files to commit"
|
|
return False, "git commit failed: %s" % detail[:200]
|
|
say(" made a repository on branch %s, one commit" % branch(folder))
|
|
return True, ""
|
|
|
|
|
|
STARTER = """# Paperclip puts each task's worktree inside this folder, at .paperclip/.
|
|
# It is a checkout of this repository, so it must never be committed.
|
|
.paperclip/
|
|
|
|
# Add your own patterns below, one per line. For example:
|
|
# evidence/
|
|
# .env
|
|
# *secret*
|
|
"""
|
|
|
|
|
|
def ensure_ignore(folder):
|
|
"""Make sure .gitignore exists and that it excludes .paperclip/.
|
|
|
|
Returns (changed, note). The .paperclip entry is not a preference: without
|
|
it the first commit takes the worktrees with it.
|
|
"""
|
|
path = os.path.join(folder, ".gitignore")
|
|
if not os.path.exists(path):
|
|
with open(path, "w") as fh:
|
|
fh.write(STARTER)
|
|
return True, "wrote .gitignore, ignoring .paperclip/"
|
|
with open(path) as fh:
|
|
text = fh.read()
|
|
for line in text.splitlines():
|
|
line = line.strip().strip("/")
|
|
if line == ".paperclip" or line.startswith(".paperclip/"):
|
|
return False, ".gitignore already ignores .paperclip/"
|
|
add = "" if text.endswith("\n") or not text else "\n"
|
|
add += "\n# Paperclip's task worktrees; a commit must never take them.\n.paperclip/\n"
|
|
with open(path, "a") as fh:
|
|
fh.write(add)
|
|
return True, "added .paperclip/ to the existing .gitignore"
|
|
|
|
|
|
def stage_all(folder):
|
|
r = git(folder, "add", "-A")
|
|
return r.returncode == 0
|
|
|
|
|
|
def commit_all(folder, message="Initial commit, before any Paperclip task"):
|
|
"""Commit what is staged. Returns (ok, message)."""
|
|
r = git(folder, "-c", "user.name=Paperclip Harness",
|
|
"-c", "user.email=harness@localhost",
|
|
"commit", "-q", "-m", message)
|
|
if r.returncode != 0:
|
|
detail = (r.stderr or r.stdout).strip()
|
|
if "nothing to commit" in detail:
|
|
return False, "nothing to commit: the folder has no files git will take"
|
|
return False, "git commit failed: %s" % detail[:200]
|
|
return True, ""
|