Documentation
¶
Overview ¶
Package worktree manages git worktrees for isolating parallel task execution.
When cloop runs multiple tasks in parallel under git mode, each task gets its own git worktree in .cloop/worktrees/task-<id>/ on a dedicated branch (cloop/task-<id>-<slug>). Tasks can modify files independently without stepping on each other; their changes are later merged back to the base branch sequentially via pkg/mergequeue.
Index ¶
- Constants
- func BranchName(task *pm.Task) string
- func IsGitRepo(dir string) bool
- func ListBranches(repoDir string) ([]string, error)
- func Path(repoDir string, task *pm.Task) string
- func WorktreesDir(repoDir string) string
- type Entry
- type KeepCode
- type KeptEntry
- type PruneError
- type PruneOptions
- type PruneResult
- type PrunedEntry
- type Worktree
Constants ¶
const DefaultMinAge = 2 * time.Hour
DefaultMinAge is the age below which Prune refuses to touch a worktree when the caller did not choose one.
It is a compromise between two failure modes with very different costs. Too low and a sweep can delete the tree of a task that is merely thinking — an agent waiting on a slow model call writes nothing for minutes at a time, and the work it loses is unrecoverable. Too high and leaked directories sit around longer, which costs disk. Two hours is long enough to cover any plausible quiet period inside a running task and short enough that a daily sweep still collects yesterday's crash.
Variables ¶
This section is empty.
Functions ¶
func BranchName ¶
BranchName returns the branch name used for a task worktree.
func IsGitRepo ¶
IsGitRepo reports whether dir is inside a git repository (working tree or linked worktree). Useful as a guard before invoking worktree machinery.
func ListBranches ¶
ListBranches returns the cloop/task-<id>-* branches of repoDir, ordered by task ID.
for-each-ref rather than `git branch --list`: the latter decorates the current branch with an asterisk and indents the rest, which is a display format that has changed before and would silently start yielding branch names with a leading "* " if it changed again.
func Path ¶
Path returns the absolute path to the worktree directory for a task. The directory lives under <repoDir>/.cloop/worktrees/task-<id>.
func WorktreesDir ¶
WorktreesDir is the directory this package owns: the only place Prune will remove anything from. Exported so a caller wiring a startup sweep can log it (and so a test can assert that Prune's blast radius is what it claims).
Types ¶
type Entry ¶
type Entry struct {
// Path is the absolute path of the worktree directory, whether or not it
// still exists.
Path string
// Branch is the branch checked out there, without the refs/heads/ prefix.
// Empty when git no longer registers the worktree and no single
// cloop/task-<id>-* branch could be matched to it — see List for why an
// ambiguous match is deliberately left empty rather than guessed.
Branch string
// TaskID is the cloop task this worktree was created for, parsed from the
// directory name or, failing that, from the branch. Zero means the entry
// could not be attributed to a task, which makes it invisible to
// PruneOptions.Active and therefore protected only by MinAge.
TaskID int
// DirExists reports whether the directory is on disk.
DirExists bool
// Registered reports whether `git worktree list` still knows about it.
Registered bool
// Locked reports that the worktree carries git's own lock marker
// (`git worktree lock`), which is an operator saying "do not collect this".
// Prune honours it unconditionally.
Locked bool
// LockReason is the message passed to `git worktree lock`, if any.
LockReason string
// ModTime is the most recent modification observed for this worktree. It is
// a heuristic, not a liveness signal: see entryModTime.
ModTime time.Time
}
Entry is one cloop-managed worktree, as reconciled from git's registration and from the directory on disk. Either source alone can be missing; see the file header for why both cases matter.
func List ¶
List enumerates the cloop-managed worktrees of repoDir, reconciling git's registrations with the contents of .cloop/worktrees.
Only entries under <repoDir>/.cloop/worktrees are returned. That containment rule is what keeps the main worktree — and any worktree the operator created by hand elsewhere — out of a sweep's reach: `git worktree list` reports the main working tree first and reports every linked worktree regardless of where it lives, so filtering by branch name alone would happily list (and Prune would then happily delete) the checkout the operator is sitting in, if they happened to have a cloop/task-N branch checked out there.
A failure of `git worktree list` is fatal rather than degraded. The alternative — falling back to the directory scan — would report every registered worktree as unregistered, which is not a partial answer but a wrong one, and Prune acts on it.
type KeepCode ¶
type KeepCode string
KeepCode is the machine-readable reason a worktree was spared. Callers should switch on this rather than parse KeptEntry.Reason, which is prose for humans.
const ( // KeepActive: the caller told us this task is running. KeepActive KeepCode = "active" // KeepTooYoung: the worktree is younger than PruneOptions.MinAge, or its // age could not be determined at all. KeepTooYoung KeepCode = "too-young" // KeepLocked: `git worktree lock` was used on it. KeepLocked KeepCode = "locked" )
type PruneError ¶
PruneError is one entry's failure. The sweep records it and moves on.
func (*PruneError) Error ¶
func (e *PruneError) Error() string
func (*PruneError) Unwrap ¶
func (e *PruneError) Unwrap() error
type PruneOptions ¶
type PruneOptions struct {
// DryRun computes and reports the whole plan without changing anything.
// The merged-branch check still runs, because it is read-only, so a dry run
// reports the branch deletions a real run would attempt. It cannot report
// the outcome of git's own second gate — `git branch -d` applies a stricter
// merged test of its own, against HEAD rather than against BaseBranch — so
// a real run can keep a branch a dry run listed for deletion. It can never
// go the other way and delete one a dry run did not list.
DryRun bool
// MinAge is the single most important safety property in this package.
//
// A live parallel run's worktrees are in this directory *right now*, and
// pruning one destroys in-flight, uncommitted work that no git operation
// can bring back. Nothing younger than MinAge is touched under any
// circumstance.
//
// Zero means DefaultMinAge — the zero value of this struct must be safe,
// because it is what a caller writes when they have not thought about it.
// A *negative* value disables the guard entirely, which is deliberately
// something a caller has to type on purpose; it is meant for tests and for
// an operator who has already established that nothing is running.
MinAge time.Duration
// Active names the task IDs that are executing right now. This is the
// authoritative liveness signal and MinAge is only the backstop for tasks
// the caller has lost track of: a caller that has a task table should pass
// it, because mtime cannot distinguish "finished an hour ago" from "still
// running and quiet for an hour".
Active map[int]bool
// KeepTaskIDs is the same instruction in slice form, for callers that have
// a list rather than a set. The two are unioned; neither overrides the
// other.
KeepTaskIDs []int
// DeleteBranches also removes each collected worktree's cloop/task-N-*
// branch — but only when that branch's commits are already contained in
// BaseBranch. An unmerged branch is the only copy of its task's work, so it
// is kept and the reason recorded. Off by default.
DeleteBranches bool
// BaseBranch is the branch a task branch must be merged into before
// DeleteBranches will remove it. Empty means the current branch of repoDir,
// which is the branch a task worktree was created from and merged back to.
BaseBranch string
}
PruneOptions configures a sweep. The zero value is safe: it prunes nothing younger than DefaultMinAge and deletes no branches at all.
type PruneResult ¶
type PruneResult struct {
// DryRun echoes the option, so a caller holding only the result can tell
// whether Removed describes the past or a plan.
DryRun bool
// Removed lists the worktrees collected, or that would be.
Removed []PrunedEntry
// Kept lists the worktrees spared, each with the reason.
Kept []KeptEntry
// Errors lists per-entry failures. A non-empty Errors does not mean the
// sweep failed; it means these entries were skipped and the rest were done.
Errors []*PruneError
}
PruneResult reports what a sweep did, what it declined to do, and why.
It reports kept entries as prominently as removed ones on purpose: the question an operator actually has after a sweep is "why is this directory still full", and a result that only listed successes could not answer it.
func Prune ¶
func Prune(repoDir string, opts PruneOptions) (*PruneResult, error)
Prune removes leaked cloop worktrees from repoDir and, when asked, their merged task branches.
The only fatal errors are the ones that make the whole sweep meaningless: a repoDir that is not a git repository, or a `git worktree list` that fails. A failure on one entry is recorded in PruneResult.Errors and the sweep continues, because a single worktree wedged by an open file handle or a permission error must not stop the other twenty from being collected.
func (*PruneResult) Summary ¶
func (r *PruneResult) Summary() string
Summary renders a one-line report for a startup log.
type PrunedEntry ¶
type PrunedEntry struct {
Entry
// BranchDeleted reports whether the task branch was deleted too.
BranchDeleted bool
// BranchKept explains why it was not, and is empty when it was deleted or
// when DeleteBranches was off. The common value is "not merged", which is
// the API refusing to destroy the only copy of a task's work.
BranchKept string
}
PrunedEntry is a worktree that was removed, or that a dry run would remove.
type Worktree ¶
type Worktree struct {
// Path is the absolute path to the worktree directory.
Path string
// Branch is the branch checked out in the worktree.
Branch string
// BaseBranch is the branch the worktree was created from (the merge target).
BaseBranch string
// TaskID is the cloop task ID this worktree was created for.
TaskID int
}
Worktree represents a single live git worktree for a task.
func Create ¶
Create provisions a fresh worktree for the given task at .cloop/worktrees/task-<id> off the current HEAD of repoDir. If a stale worktree exists at that path it is removed first (git worktree remove --force). The created branch is cloop/task-<id>-<slug>; if a branch by that name already exists it is reset to the current HEAD so the worktree always starts from a clean baseline.
func (*Worktree) Commit ¶
Commit stages all changes in the worktree and commits them with a task-scoped message. Returns the new commit SHA. If the worktree is clean, returns "" and a nil error (no-op).
func (*Worktree) HasChanges ¶
HasChanges reports whether the worktree contains any uncommitted or untracked changes. Used by callers to decide whether a merge is even meaningful.