gitwt

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package gitwt is the typed git/worktree seam for the dot-agents managed worktree platform. It exposes worktree lifecycle (create/list/remove/prune), branch operations, base-ref record/read, and per-worktree index/status/commit as typed Go operations — never as stringly-typed shell calls.

The concrete implementation (NewManager) wraps go-git v6 (github.com/go-git/go-git/v6/x/plumbing/worktree), verified in the wt0 spike as a pure-Go, zero-shell-git mechanism (Decision A). Callers and skills bind to the Manager / Worktree interfaces only, so a later mechanism swap — or the hybrid shell-git fallback the spike proved unnecessary — stays invisible to them.

Load-bearing constraints inherited from the go-git v6 x/plumbing/worktree package (see wt0 spike findings in the design doc):

  • A worktree name must match ^[a-zA-Z0-9-]+$ (no slashes, dots, or underscores). Callers with arbitrary branch names must encode them into this charset; SafeName provides the canonical hash-based encoding.
  • Branch-mode Add always creates a NEW branch named identically to the worktree and errors if that branch already exists.
  • The object store is shared across the main repo and all linked worktrees; isolation is at the index / HEAD / working-tree level, which is exactly what concurrent committers require.

mergeback.go implements the first-class sub-branch create / merge-back workflow on top of wt1's Manager (branch + recorded base ref) and wt2's Registry (semantic metadata).

Load-bearing property: merge-back resolves the integration base EXCLUSIVELY from the recorded base ref — it NEVER calls git merge-base to re-derive a fork point. Re-derivation is the stale-base trap this workflow exists to kill: after a parent is advanced or force-pushed, merge-base returns a fork point that silently rebases the sub-branch onto the wrong commit. Here the recorded base is authoritative, any drift between it and the parent's current tip is a loud failure (ErrStaleBase), and a sub-branch HEAD that moved out from under the integration is caught (ErrBranchDrift) rather than assumed intact.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrWorktreeNotFound is returned when an operation targets a worktree
	// (by name) that has no admin metadata under .git/worktrees/<name>.
	ErrWorktreeNotFound = errors.New("gitwt: worktree not found")

	// ErrWorktreeExists is returned by AddBranch/AddDetached when a worktree
	// with the given name already exists.
	ErrWorktreeExists = errors.New("gitwt: worktree already exists")

	// ErrBranchExists is returned by AddBranch when the branch it would create
	// (named identically to the worktree) already exists in the shared object
	// store. Branch-mode Add cannot reuse an existing branch.
	ErrBranchExists = errors.New("gitwt: branch already exists")

	// ErrInvalidName is returned when a worktree name does not match the
	// charset go-git enforces (^[a-zA-Z0-9-]+$). Use SafeName to encode.
	ErrInvalidName = errors.New("gitwt: invalid worktree name")

	// ErrBaseRefNotRecorded is returned by Worktree.BaseRef when no base ref
	// has been recorded for the worktree.
	ErrBaseRefNotRecorded = errors.New("gitwt: base ref not recorded")
)

Typed errors. Callers should match these with errors.Is rather than string comparison. They wrap (or stand in for) the go-git package errors so the underlying mechanism stays hidden.

View Source
var (
	// ErrStaleBase is returned by MergeBack when the recorded base ref no longer
	// equals the parent branch's current tip: the parent advanced or was
	// force-pushed after the sub-branch was created, so the recorded fork point
	// no longer describes the real relationship. MergeBack refuses to re-derive
	// a base with git merge-base and fails here instead of silently rebasing
	// onto the wrong commit.
	ErrStaleBase = errors.New("gitwt: recorded base is stale (parent moved since sub-branch creation)")

	// ErrBranchDrift is returned by MergeBack when, after integration, the
	// sub-branch worktree's HEAD no longer matches the commit that was
	// integrated. A concurrent pre-commit-hook stash/restore can silently land a
	// commit on a sibling worktree's branch (the parallel-worker-branch-drift
	// lesson); rather than assume the integrated tip was current, MergeBack
	// re-reads the worktree HEAD and fails loud on a mismatch.
	ErrBranchDrift = errors.New("gitwt: sub-branch HEAD drifted during merge-back")

	// ErrBaseNotAncestor is returned by MergeBack when the recorded base is not
	// an ancestor of the sub-branch tip, so advancing the parent to the sub tip
	// would not be a fast-forward and would orphan the parent's history.
	ErrBaseNotAncestor = errors.New("gitwt: recorded base is not an ancestor of the sub-branch tip")
)

Merge-back errors. Callers match with errors.Is.

View Source
var (
	// ErrMetadataExists is returned by Registry.Create when a metadata record
	// already exists for the worktree. Use Update to modify an existing record.
	ErrMetadataExists = errors.New("gitwt: worktree metadata already exists")

	// ErrMetadataNotRecorded is returned by Registry.Get/Update when the
	// worktree exists but carries no metadata record.
	ErrMetadataNotRecorded = errors.New("gitwt: worktree metadata not recorded")
)

Registry errors. Callers should match with errors.Is. ErrWorktreeNotFound (declared in gitwt.go) is reused for operations targeting a worktree whose admin metadata is absent.

Functions

func SafeName

func SafeName(input string) string

SafeName encodes an arbitrary caller string (e.g. a branch or task name) into a worktree name that satisfies go-git's ^[a-zA-Z0-9-]+$ constraint. It is deterministic: the same input always yields the same name, so callers can re-derive the worktree name without storing it. The human-readable branch/path mapping belongs in the wt2 registry.

Types

type CommitOptions

type CommitOptions struct {
	// AuthorName and AuthorEmail set the commit author. When empty, go-git
	// falls back to repository/user config.
	AuthorName  string
	AuthorEmail string

	// All stages modified and deleted tracked files before committing (it does
	// not stage new untracked files), mirroring `git commit -a`.
	All bool

	// AllowEmpty permits a commit with no tree changes.
	AllowEmpty bool
}

CommitOptions is the typed, mechanism-agnostic subset of commit configuration the seam exposes. A nil *CommitOptions is valid and means "use defaults".

type Coordinator added in v0.5.0

type Coordinator struct {
	// contains filtered or unexported fields
}

Coordinator wires wt1's Manager and wt2's Registry into the sub-branch create / merge-back workflow. It binds to the concrete go-git Manager so it can resolve branch refs and walk ancestry without widening the public Manager interface with those seams.

func NewCoordinator added in v0.5.0

func NewCoordinator(mgr Manager, reg *Registry) (*Coordinator, error)

NewCoordinator binds a Coordinator to a Manager and Registry. The Manager must be the go-git implementation returned by NewManager (the same constraint NewRegistry enforces) so base resolution and ancestry checks can read the underlying repository.

func (*Coordinator) CreateSubBranch added in v0.5.0

func (c *Coordinator) CreateSubBranch(opts CreateOptions) (CreateResult, error)

CreateSubBranch creates a linked worktree on a new branch rooted at the resolved base, records that base as the worktree's base ref (via wt1's Manager), and registers semantic metadata (via wt2's Registry). The recorded base is exactly what MergeBack reads later — it is never re-derived.

The worktree+branch is created before the metadata is registered; a metadata failure therefore leaves an unregistered worktree recoverable via Registry.Reconcile rather than a partially-created branch.

func (*Coordinator) MergeBack added in v0.5.0

func (c *Coordinator) MergeBack(opts MergeBackOptions) (MergeBackResult, error)

MergeBack integrates the sub-branch into its parent by fast-forwarding the parent branch to the sub-branch tip. It is the anti-stale-base path:

  • The base is read EXCLUSIVELY from the recorded base ref (recordedBase). No git merge-base call re-derives a fork point anywhere in this function.
  • The recorded base must still equal the parent's current tip; otherwise the parent moved (advanced or force-pushed) and MergeBack fails with ErrStaleBase rather than silently rebasing onto a re-derived base.
  • After integration it re-reads the sub-branch worktree HEAD and confirms it still matches the integrated commit, failing with ErrBranchDrift if a concurrent hook moved the branch (the parallel-worker-branch-drift lesson).

type CreateOptions added in v0.5.0

type CreateOptions struct {
	// Name is the worktree name; the created branch is named identically. It
	// must satisfy the ^[a-zA-Z0-9-]+$ charset (use SafeName to encode).
	Name string
	// Path is the working-tree directory for the new linked worktree.
	Path string
	// BaseBranch is the parent branch whose current tip becomes the recorded
	// base (fork point). One of BaseBranch / Base must be set; Base wins when
	// both are provided.
	BaseBranch string
	// Base is an explicit fork-point commit. Takes precedence over BaseBranch
	// when non-zero.
	Base plumbing.Hash
	// Purpose is the free-form registry note (the task/slice the worktree is
	// for).
	Purpose string
	// ParentPR is the pull-request number the work feeds into (0 when none).
	ParentPR int
	// AppType is the app_type the delegated task runs under; it is recorded on
	// the worktree metadata and is the routing key the resolved execution shape
	// below was loaded from. Empty when the caller supplies no app_type.
	AppType string
	// Profile is the free-form execution profile (e.g. "loop-worker") recorded
	// verbatim on the worktree metadata.
	Profile string
	// VerifierSequence / LensSet / LensConcurrency / GraphBackend are the
	// app_type-routed execution shape the caller already resolved from the
	// project's execution_profile.by_app_type[AppType]. They are recorded as-is
	// so the worktree self-describes its execution shape; all are empty when no
	// profile entry resolved.
	VerifierSequence []string
	LensSet          []string
	LensConcurrency  string
	GraphBackend     string
}

CreateOptions parameterizes CreateSubBranch.

type CreateResult added in v0.5.0

type CreateResult struct {
	// Metadata is the registry record stamped for the new worktree.
	Metadata Metadata
	// Base is the fork-point commit recorded as the worktree's base ref.
	Base plumbing.Hash
}

CreateResult reports the outcome of a successful CreateSubBranch.

type Manager

type Manager interface {
	// AddBranch creates a linked worktree at path checked out onto a NEW branch
	// named identically to name, rooted at base. The base hash is recorded so
	// later rebase/merge-back never re-derives it. Returns ErrWorktreeExists if
	// the worktree name is taken, ErrBranchExists if the branch already exists,
	// or ErrInvalidName if name is malformed.
	AddBranch(name, path string, base plumbing.Hash) error

	// AddDetached creates a linked worktree at path with a detached HEAD at
	// commit (swarm-cd style) — for read-only or ephemeral checkouts that need
	// no branch. The commit is also recorded as the base ref.
	AddDetached(name, path string, commit plumbing.Hash) error

	// Remove deletes the worktree's admin metadata (.git/worktrees/<name>) and
	// its working-tree directory at path. A zero-value path removes only the
	// metadata. Returns ErrWorktreeNotFound if the worktree is unknown.
	Remove(name, path string) error

	// List returns the names of all linked worktrees known to the repository
	// (enumerated natively from .git/worktrees, not from any external
	// registry).
	List() ([]string, error)

	// Prune removes admin metadata for linked worktrees whose working-tree
	// directory no longer exists on disk (the deterministic prune-if-gone
	// contract). It returns the names that were pruned.
	Prune() ([]string, error)

	// Open returns a Worktree handle for operating the linked worktree rooted
	// at path. The handle has its own index / HEAD / working tree, independent
	// of the main repository and of every other linked worktree.
	Open(path string) (Worktree, error)

	// RecordBaseRef stores base as the recorded base ref for the named
	// worktree, overwriting any prior value. Returns ErrWorktreeNotFound if the
	// worktree is unknown.
	RecordBaseRef(name string, base plumbing.Hash) error

	// BaseRef reads the recorded base ref for the named worktree. Returns
	// ErrBaseRefNotRecorded if none was recorded, or ErrWorktreeNotFound if the
	// worktree is unknown.
	BaseRef(name string) (plumbing.Hash, error)
}

Manager is the lifecycle seam for linked worktrees attached to one repository.

Implementations are tied to a single repository (the "main" worktree's repository). All names passed here are worktree names subject to the ^[a-zA-Z0-9-]+$ constraint; in branch mode the created branch is named identically to the worktree.

func NewManager

func NewManager(repoPath string) (Manager, error)

NewManager opens the repository at repoPath and returns a go-git-backed Manager for its linked worktrees. repoPath is the path to the main worktree (the dir containing .git), not a bare repo's git dir.

type MergeBackOptions added in v0.5.0

type MergeBackOptions struct {
	// Name is the sub-branch / worktree to merge back.
	Name string
	// ParentBranch is the branch the sub-branch is integrated into. Its current
	// tip MUST still equal the recorded base, or MergeBack fails with
	// ErrStaleBase — the parent is not allowed to have moved since creation.
	ParentBranch string
}

MergeBackOptions parameterizes MergeBack.

type MergeBackResult added in v0.5.0

type MergeBackResult struct {
	// Base is the recorded fork point read back from the base ref (NOT derived).
	Base plumbing.Hash
	// SubHead is the sub-branch tip that was integrated.
	SubHead plumbing.Hash
	// ParentBranch is the branch that was fast-forwarded.
	ParentBranch string
	// ParentTip is the parent branch tip after integration (equals SubHead).
	ParentTip plumbing.Hash
}

MergeBackResult reports the outcome of a successful merge-back.

type Metadata added in v0.5.0

type Metadata struct {
	// Name is the worktree name; it is the registry key and is always set to
	// the lookup name on write, so a mutator can never repoint it.
	Name string `yaml:"name"`
	// Purpose is a free-form human note (e.g. the task or delegation slice the
	// worktree was created for).
	Purpose string `yaml:"purpose"`
	// ParentPR is the pull-request number the worktree's work feeds into, or 0
	// when none is associated yet.
	ParentPR int `yaml:"parent_pr"`
	// CreatedAt is stamped once at Create and never rewritten afterwards.
	CreatedAt time.Time `yaml:"created_at"`
	// LastUsed drives the auto-prune-if-unchanged idle check; Touch re-stamps
	// it whenever the worktree is worked in.
	LastUsed time.Time `yaml:"last_used"`
	// AppType is the app_type this worktree's delegated task runs under, as
	// requested at create time. It is the routing key the resolved execution
	// shape below was loaded from (empty when the worktree carries no app_type).
	AppType string `yaml:"app_type,omitempty"`
	// Profile is the free-form execution profile the task runs under (e.g.
	// "loop-worker"), recorded verbatim from create.
	Profile string `yaml:"profile,omitempty"`
	// VerifierSequence is the app_type-routed ordered verifier-profile ids
	// resolved from the project's execution_profile.by_app_type[AppType]
	// topology. Empty when no profile entry resolved.
	VerifierSequence []string `yaml:"verifier_sequence,omitempty"`
	// LensSet is the resolved review-lens set for the app_type (from the
	// profile's lenses facet). Empty when unresolved.
	LensSet []string `yaml:"lens_set,omitempty"`
	// LensConcurrency is how the lens set runs ("parallel"/"gated"/"tiered"),
	// resolved from the profile's lenses facet. Empty when unresolved.
	LensConcurrency string `yaml:"lens_concurrency,omitempty"`
	// GraphBackend is the graph-backend adapter-ref the app_type profile
	// selects. Empty when unresolved.
	GraphBackend string `yaml:"graph_backend,omitempty"`
}

Metadata is the semantic, per-worktree record the registry persists next to wt1's base-ref file. It is keyed by worktree name so it reconciles 1:1 with Manager.List(). Base-ref storage is NOT duplicated here — that stays wt1's RecordBaseRef/BaseRef; this carries only what go-git does not track.

type PruneScan added in v0.5.0

type PruneScan struct {
	// Eligible worktrees have no commits past their recorded base AND are idle
	// past the TTL — safe to auto-prune.
	Eligible []string
	// Abandoned worktrees have commits past their recorded base and are idle:
	// that is unmerged work, never auto-pruned — surfaced for a human to judge.
	Abandoned []string
	// Kept worktrees are held for any other reason: recently used (fresh), or
	// with an indeterminate base/tip or missing metadata.
	Kept []string
}

PruneScan is the auto-prune-if-unchanged classification of every recorded worktree that Manager.List() still returns.

type ReconcileResult added in v0.5.0

type ReconcileResult struct {
	// Tracked names are recorded AND still returned by List().
	Tracked []string
	// Orphaned names are recorded but no longer returned by List() (their admin
	// dir was removed out-of-band). Reconcile drops them from the roster so
	// they are not silently kept.
	Orphaned []string
	// Untracked names are returned by List() but have no metadata record.
	Untracked []string
}

ReconcileResult reports how the registry's recorded names line up with the worktrees Manager.List() currently returns.

type Registry added in v0.5.0

type Registry struct {
	// contains filtered or unexported fields
}

Registry layers rich per-worktree metadata and an auto-prune-if-unchanged staleness policy on top of wt1's Manager. It persists the metadata under the same admin dir as the recorded base ref and keeps a separate name roster so reconciliation against Manager.List() can surface orphans.

func NewRegistry added in v0.5.0

func NewRegistry(m Manager, ttl time.Duration) (*Registry, error)

NewRegistry binds a Registry to a Manager. ttl is the last-used idle window PruneScan enforces (an unchanged, idle-past-ttl worktree is prune-eligible). The Manager must be the go-git implementation returned by NewManager.

func (*Registry) Create added in v0.5.0

func (r *Registry) Create(name string, meta Metadata) (Metadata, error)

Create records new metadata for the named worktree. It returns the stored record with CreatedAt/LastUsed stamped (from the registry clock when the caller leaves them zero). It returns ErrMetadataExists if a record already exists, or ErrWorktreeNotFound if the worktree has no admin metadata.

func (*Registry) Deregister added in v0.5.0

func (r *Registry) Deregister(name string) error

Deregister removes the metadata sidecar (if present) and drops the name from the roster. It is idempotent and tolerates an already-removed worktree admin dir, so it doubles as the cleanup for a name Reconcile flagged as orphaned.

func (*Registry) Get added in v0.5.0

func (r *Registry) Get(name string) (Metadata, error)

Get reads the metadata record for the named worktree. It returns ErrMetadataNotRecorded if the worktree exists but has no record, or ErrWorktreeNotFound if the worktree itself is unknown.

func (*Registry) PruneScan added in v0.5.0

func (r *Registry) PruneScan() (PruneScan, error)

PruneScan classifies each recorded, still-listed worktree by the auto-prune-if-unchanged policy: (no commits past base) AND (idle past TTL) => Eligible; (commits past base) AND idle => Abandoned; everything else => Kept. Unlike Manager.Prune (which only reclaims worktrees whose dir is gone), this compares the current tip against wt1's recorded base ref.

func (*Registry) Reconcile added in v0.5.0

func (r *Registry) Reconcile() (ReconcileResult, error)

Reconcile diffs the recorded roster against Manager.List(). Orphaned entries (recorded but no longer listed) are detected and pruned from the roster; untracked worktrees (listed but unrecorded) are surfaced but left alone, since the registry has no metadata to attach to them.

func (*Registry) Touch added in v0.5.0

func (r *Registry) Touch(name string) (Metadata, error)

Touch re-stamps LastUsed to the current time — the last-used marker the idle check keys off. It is the common Update the orchestrator makes each time a worktree is worked in.

func (*Registry) Update added in v0.5.0

func (r *Registry) Update(name string, mutate func(*Metadata)) (Metadata, error)

Update reads the record, applies mutate, and persists the result. The name key is always restored after mutate so it cannot be repointed. It returns ErrMetadataNotRecorded/ErrWorktreeNotFound like Get.

type Worktree

type Worktree interface {
	// Stage adds the working-tree path (file or directory, relative to the
	// worktree root) to this worktree's index. Staging here can never touch
	// another worktree's index.
	Stage(path string) error

	// Status returns the porcelain status of this worktree.
	Status() (git.Status, error)

	// Commit records the current index of this worktree as a new commit on its
	// current branch and returns the new commit hash. opts may be nil.
	Commit(message string, opts *CommitOptions) (plumbing.Hash, error)

	// Head returns the worktree's current HEAD reference.
	Head() (*plumbing.Reference, error)

	// Branch returns the short name of the branch HEAD points to, or "" if HEAD
	// is detached.
	Branch() (string, error)
}

Worktree is the per-worktree operations seam: index/status/commit plus branch inspection, all scoped to a single linked worktree's isolated index and HEAD.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL