Documentation
¶
Overview ¶
Package gitops is a thin wrapper around the git CLI for the operations `aiwf` needs: rename a tracked file, stage paths, and create a commit carrying structured trailers.
We shell out to git rather than embedding go-git for two reasons: the host's git config (signing keys, hook installation, identity) is what users expect to apply, and our needs are small enough that a subprocess is the boring choice.
Index ¶
- Constants
- Variables
- func Add(ctx context.Context, workdir string, paths ...string) error
- func AddCommitSHA(ctx context.Context, workdir, relPath string) (string, error)
- func BlobAllZero(id string) bool
- func BranchExists(ctx context.Context, workdir, branch string) (bool, error)
- func BulkRevwalk(ctx context.Context, root string, fn func(CommitRecord) error) error
- func CanonicalTrailerKeys() map[string]bool
- func Commit(ctx context.Context, workdir, subject, body string, trailers []Trailer) error
- func CommitAllowEmpty(ctx context.Context, workdir, subject, body string, trailers []Trailer) error
- func CommitExists(ctx context.Context, workdir, ref string) (bool, error)
- func CommitMessage(subject, body string, trailers []Trailer) string
- func CommitTree(ctx context.Context, workdir string, removes []string, writes []PathWrite, ...) (string, error)
- func CommitVerbChange(ctx context.Context, workdir string, removes []string, writes []PathWrite, ...) (sha string, err error)
- func DeleteBranch(ctx context.Context, workdir, branch string) error
- func FetchAll(ctx context.Context, workdir string) error
- func FormatTestMetrics(m TestMetrics) string
- func GitDir(ctx context.Context, workdir string) (string, error)
- func HasAnyRemoteTrackingRefs(ctx context.Context, workdir string) (bool, error)
- func HasRef(ctx context.Context, workdir, ref string) (bool, error)
- func HasRemotes(ctx context.Context, workdir string) (bool, error)
- func HeadBody(ctx context.Context, workdir string) (string, error)
- func HeadSubject(ctx context.Context, workdir string) (string, error)
- func HooksDir(ctx context.Context, workdir string) (string, error)
- func InWorktree(ctx context.Context, workdir string) (bool, error)
- func Init(ctx context.Context, workdir string) error
- func IsAncestor(ctx context.Context, workdir, commit, ref string) (bool, error)
- func IsRenameVerb(value string) bool
- func IsRepo(ctx context.Context, workdir string) bool
- func LocalBranchRefs(ctx context.Context, workdir string) ([]string, error)
- func LsTreePaths(ctx context.Context, workdir, ref string, prefixes ...string) ([]string, error)
- func MainCheckoutRoot(ctx context.Context, workdir string) (string, error)
- func Mv(ctx context.Context, workdir, from, to string) error
- func ReadFromHEAD(ctx context.Context, workdir, relPath string) ([]byte, error)
- func ReconcilePaths(ctx context.Context, workdir string, removes []string, writes []PathWrite) error
- func RemoteTrackingRefs(ctx context.Context, workdir string) ([]string, error)
- func RenamesFromRef(ctx context.Context, workdir, ref string) (map[string]string, error)
- func RenamesInCommit(ctx context.Context, workdir, sha string) (map[string]string, error)
- func RunPostCommitHook(ctx context.Context, workdir string) error
- func ShortSHA(ctx context.Context, workdir, ref string, n int) (string, error)
- func StagedPaths(ctx context.Context, workdir string) ([]string, error)
- func TrunkRenamesFromRef(ctx context.Context, workdir, ref string) (map[string]string, error)
- func ValidateTrailer(key, value string) error
- func WorktreeAdd(ctx context.Context, workdir, path, branch string) error
- func WorktreeAddNewBranch(ctx context.Context, workdir, path, branch, base string) error
- func WorktreeRemove(ctx context.Context, workdir, path string) error
- type BlobReader
- type CommitRecord
- type PathTouch
- type PathWrite
- type ReconcileError
- type TestMetrics
- type Trailer
- type Worktree
Constants ¶
const ( TrailerVerb = "aiwf-verb" TrailerEntity = "aiwf-entity" TrailerActor = "aiwf-actor" TrailerTo = "aiwf-to" TrailerForce = "aiwf-force" TrailerPriorEntity = "aiwf-prior-entity" TrailerPriorParent = "aiwf-prior-parent" TrailerTests = "aiwf-tests" // I2.5 provenance trailers. TrailerPrincipal = "aiwf-principal" TrailerOnBehalfOf = "aiwf-on-behalf-of" TrailerAuthorizedBy = "aiwf-authorized-by" TrailerScope = "aiwf-scope" // TrailerBranch (M-0102) records the ritual branch a scope is bound // to on an `aiwf authorize` commit (per ADR-0010). Emitted only when // the operator passes `--branch <name>`; absent flag = absent trailer // (backward compatible). M-0103's preflight enforces the requirement // for AI-actor scopes; this trailer is the metadata it leaves behind. TrailerBranch = "aiwf-branch" // TrailerBranchSHA (M-0161/AC-6, G-0206) records the bound // branch's tip SHA at scope-open time on `aiwf authorize` // commits. Composes with TrailerBranch (which records the // branch's short name): when a branch is renamed after scope // open, name-only resolution false-positives; SHA-based // resolution (via BranchOracle.BranchOfSHA) survives renames // transparently. The trailer is OPTIONAL — pre-M-0161/AC-6 // authorize commits and future-branch carve-outs (M-0103 / // M-0105 where --branch names a branch that doesn't yet // exist) omit it; the rule falls back to name-only // resolution in those cases. Value is the canonical // 40-char lowercase hex SHA-1 shape (enforced by // ValidateTrailer at write time). TrailerBranchSHA = "aiwf-branch-sha" TrailerScopeEnds = "aiwf-scope-ends" TrailerReason = "aiwf-reason" // I2.5 audit-only recovery (G24, plan step 5b). TrailerAuditOnly = "aiwf-audit-only" // M-0136 acknowledge-illegal — retroactive sovereign override for // historical FSM-illegal commits surfaced by // fsm-history-consistent's illegal-transition predicate. The value // is the target commit's SHA (7-40 hex); the acknowledgment // commit's other trailers (aiwf-verb: acknowledge-illegal, // aiwf-actor: human/..., aiwf-reason: ...) carry the audit trail. TrailerForceFor = "aiwf-force-for" )
Trailer key constants. Verbs and tests should reference these rather than literal strings so a future rename or audit lands in one place. Pre-I2.5 keys (Verb…Tests) preserve their existing semantics; I2.5 keys (Principal…Reason) are added by the provenance model — see docs/pocv3/design/provenance-model.md.
Variables ¶
var ErrBlobMissing = errors.New("gitops: blob missing at commit:path")
ErrBlobMissing signals the requested commit:path doesn't resolve to a blob — typically because the file didn't exist at that commit, or the rev / path string is malformed. Callers use `errors.Is(err, ErrBlobMissing)` to branch this case off from real protocol / subprocess errors.
The "missing" semantics match git cat-file --batch's `<input> missing\n` response shape: not a subprocess crash, just a "no such object" answer to a well-formed request.
var ErrRefNotFound = errors.New("ref not found")
ErrRefNotFound reports that the requested ref does not resolve in workdir's git repository. Wrapped by HasRef and LsTreePaths so callers can distinguish "ref absent" (potentially a sandbox repo) from "git failed for some other reason."
Functions ¶
func AddCommitSHA ¶
AddCommitSHA returns the SHA of the commit that introduced relPath into the repo. Returns ("", nil) when the file has no add commit visible from HEAD (newly staged but never committed). Wraps git failures.
`git log --diff-filter=A --pretty=%H -- <path>` is git's "when did this exact path first appear" query. We deliberately do NOT pass `--follow`: it traces *content* across renames as a heuristic, which produces wrong answers in the duplicate-id case the reallocate tiebreaker cares about — two entity files of the same kind have nearly-identical frontmatter/body shapes, and `--follow` will frequently mis-attribute one's add commit to the other's. The exact-path query is what we actually want: the commit that first put bytes at this exact path.
func BlobAllZero ¶ added in v0.21.0
BlobAllZero reports whether id is git's all-zero blob object id — the "no blob on this side" sentinel `git log --raw` emits for the pre-image of an add or the post-image of a delete. Consumers treat it the same as a missing blob (no content to read).
func BranchExists ¶ added in v0.26.0
BranchExists reports whether branch exists as a local branch in the repo containing workdir. `aiwf worktree add`'s caller uses this to pick between reusing an existing branch (WorktreeAdd) and creating a fresh one (WorktreeAddNewBranch) — plain `git worktree add <path> <branch>` fails on a nonexistent branch, and `-b` fails when the branch already exists, so the two forms are not interchangeable.
Mirrors StashTopRef's existence-probe shape: `--verify --quiet` exits 1 with empty output when the ref is absent, which maps to (false, nil); any other failure is wrapped and returned.
func BulkRevwalk ¶ added in v0.9.0
BulkRevwalk runs a single `git log --all --raw --no-abbrev -M --pretty=...` subprocess, reads its full output, then calls fn for each commit-diff CommitRecord in walk order. The single-subprocess shape replaces the per-entity `git log --follow` fan-out used by callers that walk every entity (fsm-history-consistent, status worktree views, show scope views) — collapsing ~3,000 fork/execs on the kernel tree into one long-lived process. `--raw --no-abbrev` carries each path's pre/post blob object ids (PathTouch.PreSHA / PostSHA) so status-reading consumers fetch content by object id rather than re-resolving `<commit>:<path>` per read (E-0053 / M-0216 AC-2).
The git output is buffered in full before the first callback — this is deliberately not a streaming reader, because every current caller consumes the whole walk (YAGNI). If fn returns a non-nil error, BulkRevwalk stops iterating the remaining records and returns that error verbatim (`errors.Is` works). That short-circuits the parse/callback loop only, not the git subprocess, which has already run to completion — so it saves callback work, not subprocess time.
Returns nil (no error, no callbacks) when root is empty, is not a git repo, or is a repo with no commits — the same "nothing to walk" semantic as internal/cli/history.readHistory uses.
The walk includes all reachable refs (--all) so feature-branch history is observed; -M enables rename detection (PathTouch.Status "R" with SrcPath set rather than separate D + A entries). `-m` (the per-parent diff fan-out for merge commits) is deliberately NOT requested (G-0372 Fix 1): every current consumer of BulkRevwalk's output discards merge-commit observations unconditionally (D-0010), so paying for the fan-out bought nothing — dropping it measurably cuts this walk's cost with no behavior change for any consumer. Without -m, a merge commit's record still carries Commit / Parents / Trailers, just an empty Paths (see CommitRecord).
func CanonicalTrailerKeys ¶ added in v0.12.0
CanonicalTrailerKeys returns the kernel's canonical trailer-key set as a membership map derived from trailerOrder. Callers — most notably TestTrailerShapePerMutatingVerb in internal/cli/integration — assert that every trailer key on a verb's commit is a member.
Why an accessor rather than exporting trailerOrder: the write-order slice is an implementation detail of SortedTrailers; the membership view is the public contract. G-0195 named the drift class — a hand-maintained mirror of trailerOrder in the test package — and this accessor eliminates the parallel source of truth. A new trailer added to trailerOrder is automatically considered canonical by every caller; the drift class becomes structurally impossible.
const-block ↔ trailerOrder drift is policed by internal/policies/PolicyTrailerOrderMatchesConstants — adding a Trailer* constant without appending to trailerOrder (or vice versa) fails CI with the offending identifier named.
The returned map is shared. Callers that need to mutate it (e.g., to scratch-augment with a synthetic key) must copy first.
func Commit ¶
Commit creates a commit with the given subject line, optional body, and trailers. The commit's index is whatever has been staged with Add prior to this call; this is intentionally low-level — verbs control staging. An empty body produces no body section.
func CommitAllowEmpty ¶
CommitAllowEmpty creates a commit even when the index has no staged changes. Used by verbs that record an event without touching files — `aiwf authorize` opens / pauses / resumes a scope by writing only trailers, and `aiwf <verb> --audit-only` (G24, plan step 5b) backfills an audit trail for state that was reached via a manual commit. Both are byte-identical to a normal commit except for the empty diff.
func CommitExists ¶ added in v0.11.0
CommitExists reports whether ref resolves to a commit object in workdir's repo. It is the commit-existence check behind the promote --by-commit write-time validation (G-0186): an operator can pass a full or abbreviated SHA, a branch name, a tag — anything git understands as a commit-ish — and this returns whether it actually resolves to a commit here and now.
Returns (false, nil) when ref does not resolve (the unresolvable-SHA case the validation rejects); (true, nil) when it does. Other git failures (broken workdir, git absent) propagate as wrapped errors so the caller can distinguish "doesn't resolve" from "couldn't check."
`git rev-parse --verify --quiet <ref>^{commit}` is the resolution path: the ^{commit} peel forces a commit object specifically (so an existing tree/blob SHA is not mistaken for a commit), --verify guarantees a single unambiguous result, and abbreviated SHAs are handled natively by git's object-name resolver (the legitimate G-0185 value f7fd1f99 is an 8-char prefix). This mirrors HasRef's mechanics; CommitExists exists as a distinct, intention-named verb so call sites read as the commit-resolvability check they are rather than reusing a helper documented for branch/tag refs.
func CommitMessage ¶
CommitMessage assembles a subject, optional body, and trailers into the conventional commit-message form: subject, blank line, body (when non-empty) blank line, trailers (one per line). Exposed so callers (and tests) can construct messages without invoking git.
The body is free-form prose. Whitespace is trimmed from both ends; an empty body produces no body section.
func CommitTree ¶ added in v0.26.0
func CommitTree(ctx context.Context, workdir string, removes []string, writes []PathWrite, subject, body string, trailers []Trailer) (string, error)
CommitTree builds a commit from HEAD's tree plus writes, entirely against a throwaway index — HEAD's tree, the temp index, and the object database are the only things read or written. The live index and worktree are never touched, which is what makes this primitive safe to call while the caller (or a concurrent process) has its own staged or unstaged changes pending: nothing here can desync them.
Steps: `git read-tree HEAD` into a temp index seeds it with the current tree; each write is hashed into a blob and added to that index via `update-index --add --cacheinfo`; `write-tree` produces the resulting tree; `commit-tree` builds the commit object against HEAD as its sole parent; `update-ref` moves HEAD (compare-and-swap against the parent SHA captured at the start, so a concurrent HEAD move is detected rather than silently overwritten). Returns the new commit's SHA.
Reconciling the written paths into the live index (so `git status` is clean for them) is a separate concern — see the post-commit reconciliation this primitive is paired with.
removes evicts paths from the tree seeded by read-tree — the mechanism a rename needs: read-tree carries the parent's tree forward unchanged, so a rename's old path stays present unless explicitly removed. A remove for a path absent from the parent tree is a no-op, not an error.
A repo with no commits yet (unborn HEAD) is not an error: CommitTree builds a root commit instead, the same as `git commit` does on a fresh repository. This matters for verb.Apply's very first commit against a brand-new consumer repo.
func CommitVerbChange ¶ added in v0.26.0
func CommitVerbChange(ctx context.Context, workdir string, removes []string, writes []PathWrite, subject, body string, trailers []Trailer) (sha string, err error)
CommitVerbChange runs the full verb-commit sequence in one call: CommitTree builds the commit against a throwaway index, the post-commit hook fires (best-effort, matching git's own tolerance for that hook), then ReconcilePaths syncs the written paths into the live index. This is the one exported entry point for commit-construction (M-0186/AC-5) — a future verb-commit consumer reuses this sequence rather than re-deriving the ordering of its three underlying steps.
A failure building the commit itself returns before anything lands (sha is empty, err is the plain CommitTree error). A failure during reconciliation returns as *ReconcileError: the commit already exists in git history, so the caller must not treat it as "nothing happened."
func DeleteBranch ¶ added in v0.26.0
DeleteBranch force-deletes a local branch regardless of merge status. Used by `aiwf worktree add`'s rollback path to undo a branch it just created when a later step in the same atomic operation fails.
func FetchAll ¶ added in v0.20.0
FetchAll runs `git fetch --all` in workdir, refreshing every remote-tracking ref (refs/remotes/*) across all configured remotes. Wraps git failures; the caller decides whether a failure is fatal or, as in the allocator's opt-in `--fetch` (M-0214), best-effort.
This is the broadened successor to M-0213's single-branch trunk fetch: the allocator's remote-side view now spans every remote-tracking ref (trunk.RemoteRefIDs), so `--fetch` refreshes all of them, not just the trunk branch. With no remotes configured it is a clean no-op (git exits 0) — nothing to fetch.
func FormatTestMetrics ¶
func FormatTestMetrics(m TestMetrics) string
FormatTestMetrics writes the canonical on-wire form of m. Order is pass / fail / skip / total; total is omitted when zero. Always emits `key=value` pairs separated by single spaces, matching the recipe in the I3 plan §4. Returns the empty string for a zero-value TestMetrics — callers should not write the trailer at all in that case.
func GitDir ¶
GitDir returns the absolute path to the git directory for workdir. Handles worktrees (where `.git` is a file, not a directory) and submodules transparently. Returns an error when workdir is not in a git repo.
func HasAnyRemoteTrackingRefs ¶
HasAnyRemoteTrackingRefs reports whether workdir has any refs/remotes/* ref recorded locally. Used by the trunk-awareness policy to distinguish "remote configured but never populated" (e.g., a clone of an empty bare repo, before the first push) from "remote configured and the trunk ref just doesn't match what's fetched" (a real misconfiguration).
Returns (false, nil) when no tracking refs exist; (true, nil) when at least one does. Other git failures propagate as wrapped errors.
func HasRef ¶
HasRef reports whether ref resolves to an object in workdir's repo. Returns (false, nil) when the ref is absent — distinguishing it from any other git failure, which propagates as a wrapped error.
func HasRemotes ¶
HasRemotes reports whether workdir has any configured git remote. A repo with no remotes has no possible cross-branch coordination surface, so the trunk-aware allocator skips its check there.
func HeadBody ¶
HeadBody returns the body of HEAD's commit (the part between the subject and any trailers). Used by tests to verify a `--reason` text landed in the commit; not used at runtime.
func HeadSubject ¶
HeadSubject returns the subject line of HEAD's commit. Used by tests to verify a commit landed; not used at runtime.
func HooksDir ¶
HooksDir returns the effective hooks directory for workdir. When `core.hooksPath` is set in the repo's git config, it is honored (relative paths resolve against workdir); otherwise the default `<commonGitDir>/hooks` is returned — the *shared* git dir, not the per-worktree one. From a linked worktree, git only fires hooks from the shared dir (per G-0136 / M-0133 / AC-2: writes to `.git/worktrees/<id>/hooks/` are inert).
`git config --get` exits 1 when the key is unset, which `output` surfaces as an error; we treat any error as "fall back to default."
Symlink resolution matters on macOS: `t.TempDir()` returns `/var/folders/...` but git resolves it to `/private/var/folders/...`. Without canonicalizing the relative-path branch, callers that compare the returned value against git-derived paths get long-up-and-back relative results that aren't useful for human-facing reports.
func InWorktree ¶ added in v0.8.1
InWorktree reports whether workdir is inside a linked git worktree (vs. the main checkout). True when the per-worktree git dir differs from the shared common dir. Useful for operator-facing messages that want to flag "this action affects all worktrees of the repo" when run from a linked worktree.
Returns false (no error) when workdir is the main checkout, and a non-nil error when git is not reachable from workdir.
func Init ¶
Init initializes a git repository at workdir with `main` as the default branch. Used by tests; not invoked by `aiwf` verbs at runtime. The explicit `-b main` is what makes the test set env-independent — without it, `git init` honours the runner's `init.defaultBranch` config and tests that later `git checkout main` fail on runners that default to `master` (or anything else).
func IsAncestor ¶
IsAncestor reports whether commit is an ancestor of ref (i.e. `git merge-base --is-ancestor <commit> <ref>` succeeds). Returns (false, nil) when commit is not an ancestor; (true, nil) when it is; an error only on real git failures (bad refs, missing repo).
The reallocate tiebreaker uses this to ask "which side already exists on trunk?" — the side that does keeps the id; the side that doesn't gets renumbered.
func IsRenameVerb ¶ added in v0.12.0
IsRenameVerb returns true when value is an aiwf-verb trailer value whose commit semantics include moving an entity file. Mirrors the closed set in renameVerbs above. The getter shape (rather than exporting the map) preserves the closed-set invariant — cross-package callers cannot mutate the membership.
M-0160/AC-4 adoption: internal/check/id_rename_untrailered.go's gather-side walker calls IsRenameVerb to decide whether a commit's rename is canonical-path (skip) or untrailered (emit a finding). Before this export, the consumer carried a duplicated map by value; the export eliminates the drift hazard reviewer N-2 flagged at M-0160/AC-4 GREEN.
func LocalBranchRefs ¶ added in v0.20.0
LocalBranchRefs returns the full refnames of every local branch (refs/heads/*) in workdir's repository, e.g. []string{"refs/heads/main", "refs/heads/epic/E-01-foo"}. Returns nil when the repo has no local branches (a freshly-init'd repo before its first commit, or an all-detached state). Wraps git failures.
Mirrors HasAnyRemoteTrackingRefs's for-each-ref shape, widened from a count to the full list. Used by the allocator's broadened cross-branch view (M-0212) via trunk.LocalRefIDs: a sibling git worktree's freshly-committed entity already lives in the shared local refs, so the allocator reads them to skip past it.
func LsTreePaths ¶
LsTreePaths returns the file paths under ref's tree, optionally filtered to those whose slash-normalized path begins with any of the supplied prefixes. Pass no prefixes to list every path. Paths are repo-relative and slash-separated; ordering is git's (sorted).
Returns ErrRefNotFound (wrapped) when ref does not resolve. Other git failures propagate as wrapped errors. An existing but empty ref tree returns ([]string{}, nil).
func MainCheckoutRoot ¶ added in v0.22.0
MainCheckoutRoot returns the absolute path to the main working tree's root for the repo at workdir — the parent of the shared git dir. When workdir is a linked worktree, this is the main checkout, not the worktree, so aiwf writes shared per-repo artifacts (the health file) there and a single copy serves every worktree.
func Mv ¶
Mv runs `git mv` to relocate a tracked file or directory. from and to are paths relative to workdir.
func ReadFromHEAD ¶
ReadFromHEAD returns the bytes of relPath as it exists in the HEAD commit. Returns (nil, nil) when the path is not present at HEAD (e.g., the file is new in the working tree but not yet committed) so callers can branch on "exists at HEAD" cleanly without parsing stderr. Real git errors (no HEAD, repo-not-found, transport failure) are wrapped and returned.
relPath must be repo-relative and forward-slashed; git's HEAD:<path> grammar requires that shape.
Used by `aiwf edit-body` (M-060 bless mode) to compare working- copy bytes against HEAD bytes for the no-diff and frontmatter- changed refusal paths. Two-step (exists check then content read) avoids parsing localized git stderr text — the existence probe is the canonical pattern for this question.
func ReconcilePaths ¶ added in v0.26.0
func ReconcilePaths(ctx context.Context, workdir string, removes []string, writes []PathWrite) error
ReconcilePaths stages exactly the given writes' content into the live index, so `git status` reports them clean against the commit CommitTree just built — without staging, unstaging, or otherwise touching any other path. Pairs with CommitTree: CommitTree builds a commit against a throwaway index and never reads or writes the live index or worktree; ReconcilePaths is the deliberately narrow follow-up step that syncs only the paths the commit actually wrote, leaving whatever else the caller has staged or modified untouched.
removes mirrors CommitTree's own removes: a rename's vacated old path must be evicted from the live index too, or `git status` would keep reporting it as present. A remove for a path the live index doesn't have is a no-op, not an error.
Each write's content is hashed into the object database (an identical hash-object call already ran inside CommitTree for the same content — git is content-addressed, so this is a cheap no-op repeat, not a duplicate write) and staged via `update-index --add --cacheinfo` against the real index, one path at a time — a failure partway through leaves every already-processed path reconciled rather than aborting the whole batch.
func RemoteTrackingRefs ¶ added in v0.20.0
RemoteTrackingRefs returns the full refnames of every remote-tracking branch (refs/remotes/*) in workdir's repository, across all remotes, e.g. []string{"refs/remotes/origin/feature", "refs/remotes/origin/main"}. The per-remote symbolic HEAD (refs/remotes/<remote>/HEAD) is excluded — it just points at that remote's default branch, whose tree another ref already covers. Returns nil when the repo has no remote-tracking refs. Wraps git failures.
Used by the allocator's broadened cross-branch view (M-0214) via trunk.RemoteRefIDs: an entity pushed to any remote branch is visible in the local remote-tracking refs, so the allocator reads them to skip past it (the remote-side mirror of LocalBranchRefs / M-0212).
func RenamesFromRef ¶ added in v0.8.0
RenamesFromRef returns the set of file renames committed on HEAD since it diverged from ref — i.e., renames in commits reachable from HEAD but not from ref. Keys are pre-rename paths, values are post- rename paths (both repo-relative, slash-separated).
Used by `aiwf check`'s ids-unique trunk-collision rule (G-0109) so a feature-branch slug rename of an existing entity is recognized as the same entity moved, not a duplicate id allocation. Without this, any rename-heavy cleanup on a feature branch produces a finding per renamed entity and blocks `git push` via the pre-push hook — the catch-22 the gap documents.
The scope is deliberately **merge-base(HEAD, ref)..HEAD**, not `ref..HEAD` or `ref` vs the working tree. The merge-base scoping matters for the G37 case the trunk-collision rule was originally designed to catch: two parallel clones each independently allocate the same id at different slug-derived paths. Comparing ref's tree to HEAD's tree (or to the working tree) sees both sides' add+delete pair and git's similarity heuristic matches them as a rename, even though no rename ever happened. Scoping to merge-base..HEAD only surfaces the renames *this branch* committed; the other clone's add isn't in this branch's history at all and can't be misread as a rename.
Returns an empty map (not nil) when no renames are detected. Returns (nil, nil) when ref does not resolve, when HEAD has no commits, or when ref and HEAD share no common ancestor — in each case the trunk-collision rule already degrades to "no cross-tree view" so the empty answer is the correct one.
`-z` is required for safe parsing: file paths can legally contain any byte except NUL, and the default newline-separated output breaks on paths with embedded tabs or newlines.
func RenamesInCommit ¶ added in v0.12.0
RenamesInCommit returns the file renames recorded by a single commit. Uses git's per-commit `-M` similarity at the default threshold; per-commit diffs are typically very high-similarity (the kernel verbs that move files don't simultaneously rewrite the body), so the default threshold reliably catches them.
Returns the rename map as old-path → new-path. Caller-owned error semantics: any subprocess failure propagates.
M-0160/AC-4 export: the unexported renamesInCommit had a duplicate at internal/check/id_rename_untrailered.go (per the same N-2 finding); RenamesInCommit is the shared surface both the gitops walker (renamesFromAiwfVerbTrailers) and the check rule's walker (WalkUntrailedIDRenames) consume.
func RunPostCommitHook ¶ added in v0.26.0
RunPostCommitHook invokes the repo's post-commit hook, if one is installed and executable, mirroring what `git commit`'s porcelain layer does automatically after landing a commit. CommitTree-based commits (M-0186) bypass git's hook machinery entirely — commit-tree and update-ref are plumbing, and plumbing never fires hooks — so a caller that wants commit-tree-based commits to behave like a normal `git commit` for hook purposes (the STATUS.md regeneration hook, G-0112, or any hook a user has chained into post-commit.local) must invoke this explicitly after a successful commit.
Matches git's own tolerance for this specific hook: per githooks(5), post-commit's exit status is informational only and never affects the outcome of the commit that already landed, so it is not surfaced here either. The only error this returns is a genuine environment problem resolving the hooks directory; a missing or non-executable hook file is a silent no-op, exactly as git itself treats it.
func ShortSHA ¶ added in v0.10.0
ShortSHA returns the first n hex characters of the commit SHA that ref resolves to, via `git rev-parse --short=n ref`. Returns "" (and a wrapped error) when the ref does not resolve or git fails. Used by the doctor binary-staleness check (G-0176) to compare a pseudo-version's 12-char SHA prefix against the trunk-ref HEAD.
func StagedPaths ¶
StagedPaths returns every path currently staged in the index whose content differs from HEAD. Order is git's order; duplicates are not produced by `git diff --cached --name-only`. Used by verb.Apply to detect overlap between the user's pre-existing staged changes and a verb's about-to-write paths (G34 conflict guard / stash isolation).
`-z` null-delimits the output so paths containing spaces, newlines, or other shell-hostile bytes round-trip safely. Empty output (clean index) returns a nil slice.
func TrunkRenamesFromRef ¶ added in v0.26.0
TrunkRenamesFromRef returns the set of file renames committed on ref since it diverged from HEAD — the trunk-side mirror of RenamesFromRef, scoped **merge-base(HEAD, ref)..ref** rather than merge-base(HEAD, ref)..HEAD. Keys are pre-rename paths, values are post-rename paths, both as observed at ref's tip (repo-relative, slash-separated).
Used by `aiwf check`'s ids-unique trunk-collision rule (G-0378) to recognize a rename committed *on trunk* (e.g. `aiwf retitle`) after a feature branch already forked away from it — the reverse direction RenamesFromRef's HEAD-scoped walk cannot see.
Trailer-driven only (ADR-0031) — deliberately NO git-diff-M content-similarity fallback, unlike RenamesFromRef's branch-side pass. This range can span trunk's entire history since an old branch forked, potentially large; a similarity heuristic over that range risks spuriously pairing two unrelated, boilerplate-similar entities and silently misclassifying a genuine id collision as "same entity, moved" — a false negative in a correctness gate, strictly worse than the false positive this function exists to fix. A trailer is exact, authored-intent ground truth; restricting to it makes that failure mode structurally impossible, at the cost that a manual, non-kernel-verb `git mv` performed directly on trunk is not auto-recognized (a safe, recoverable false positive, consistent with the existing id-rename-untrailered check).
Always returns a non-nil map — empty when ref doesn't resolve, when HEAD has no commits, when ref and HEAD share no common ancestor, or when no rename-shaped trailers exist in range. In every such case the trunk-collision rule correctly finds no exemption and fires, per ADR-0031's fail-closed-on-uncertainty default; unlike RenamesFromRef, no caller here needs to distinguish "no trunk view" from "trunk view, no renames," so there's no nil/empty distinction to preserve.
func ValidateTrailer ¶
ValidateTrailer enforces I2.5 write-time shape rules per known key. Returns nil for unknown keys (forward compatibility — future trailers don't break old binaries) and for keys whose semantic shape is "any non-empty string" (verb, entity, to, prior-entity, tests are all loose strings).
Identity-bearing trailers must match `<role>/<id>`; principal and on-behalf-of additionally require a `human/` role (per the "principal is always human" kernel rule). SHA-shaped trailers validate as 7–40 hex. Scope is a closed-set enum. Reason and force require a non-empty value after trim. Aiwf-audit-only follows the reason shape (non-empty, free text).
SHA-points-to-a-real-authorize-commit is verified at READ time, not here — write-time checks against historical SHAs would race with rebases and force-pushes. See provenance-model.md §"Trailer set".
func WorktreeAdd ¶ added in v0.26.0
WorktreeAdd creates a linked worktree at path checked out to the existing local branch. Any git failure (branch already checked out elsewhere, path already exists, etc.) is surfaced verbatim via run's combined-output wrap — the caller must not report success on error.
func WorktreeAddNewBranch ¶ added in v0.26.0
WorktreeAddNewBranch creates a linked worktree at path with a fresh local branch, starting from base. An empty base defers to git's own default (HEAD).
func WorktreeRemove ¶ added in v0.26.0
WorktreeRemove removes the linked worktree at path, force-removing even if it carries uncommitted or untracked changes. Used by `aiwf worktree add`'s rollback path when a step after worktree creation fails — a partially-materialized worktree must not stay registered.
Types ¶
type BlobReader ¶ added in v0.9.0
type BlobReader struct {
// contains filtered or unexported fields
}
BlobReader is a long-lived `git cat-file --batch` pump. One subprocess is launched at construction and reused for all Read calls, replacing N short-lived `git show <commit>:<path>` invocations elsewhere in the kernel (notably `internal/check/fsm_history_consistent.go:351`'s per-(commit, path) status reads).
Not safe for concurrent use — git's batch protocol is request / response one-at-a-time over a single stdin/stdout pair. Consumers serialize Read calls; a future M-NNN that wants concurrency adds a worker-pool front-end on top.
Lifetime: callers MUST defer Close to terminate the subprocess. Leaking a BlobReader leaves a long-lived `git cat-file --batch` process attached to the parent — observable in `ps`, eventual fd-exhaustion risk in long-running daemons.
func NewBlobReader ¶ added in v0.9.0
func NewBlobReader(ctx context.Context, root string) (*BlobReader, error)
NewBlobReader spawns the `git cat-file --batch` subprocess in root. Returns an error when root is empty, isn't a git repo, or the subprocess can't be started.
Callers MUST defer Close after a successful NewBlobReader. The subprocess inherits the parent's environment; identity / config considerations follow git's normal layering.
func (*BlobReader) Close ¶ added in v0.9.0
func (br *BlobReader) Close() error
Close terminates the subprocess and reaps the exit status. Subsequent Read calls return errBlobReaderClosed. Close is idempotent — a second call is a no-op.
func (*BlobReader) Read ¶ added in v0.9.0
func (br *BlobReader) Read(commit, path string) ([]byte, error)
Read fetches the blob content at the named commit:path.
Returns (nil, ErrBlobMissing) when the path doesn't exist at the commit, the commit doesn't exist, or the input string is malformed — the same skip-this-pair signal `internal/check/fsm_history_ consistent.go:statusAtCommitPath` returns "" for today.
Returns (blob, nil) on success — the bytes are exactly the file's content at that commit, with no trailing newline injected by git's batch output (the protocol's framing newline is consumed by the parser).
Returns (nil, err) for real failure modes: subprocess crash, protocol violation, post-Close call.
func (*BlobReader) ReadObject ¶ added in v0.21.0
func (br *BlobReader) ReadObject(sha string) ([]byte, error)
ReadObject fetches the object content named directly by its object id (a full 40-char or abbreviated blob/commit/tree SHA), rather than by `<commit>:<path>`. Resolving by object id is a direct object lookup — it skips the per-read tree walk `<commit>:<path>` forces git to perform from the commit root down to the blob — so callers that already hold the blob id (e.g. from `git log --raw`'s pre/post object-id columns, PathTouch.PreSHA / PostSHA) read content far more cheaply (E-0053 / M-0216 AC-2).
Returns (nil, ErrBlobMissing) when the id doesn't resolve to an object (malformed or unknown id), matching Read's missing-blob signal. git's all-zero id is one such missing case; callers that hold a raw-diff column guard it via BlobAllZero before calling, but a passed-through all-zero id still resolves to ErrBlobMissing here rather than a protocol error.
type CommitRecord ¶ added in v0.9.0
type CommitRecord struct {
Commit string
Parents []string
Paths []PathTouch
Trailers map[string]string
}
CommitRecord is one commit observed by BulkRevwalk: the commit's SHA, its parent SHAs in git's declared order (first-parent first), the paths it touched (with rename info when -M detected one), and the aiwf-* trailers parsed from the commit message.
For a multi-parent (merge) commit, Paths is always empty: `git log --raw` suppresses diff output for merge commits by default (the per-parent fan-out `-m` would force is deliberately not requested, G-0372 Fix 1 — every current consumer discards merge-commit observations unconditionally). The commit still gets exactly one record (Commit / Parents / Trailers, from the tformat pretty-print, which isn't diff-dependent).
Trailers is keyed by the bare trailer name (no "aiwf-" prefix stripping). Multi-value trailers collapse to the last value, matching internal/cli/history's existing single-value-per-key shape; consumers needing multi-value semantics use the Trailer slice form via HeadTrailers / ParseTrailers instead.
type PathTouch ¶ added in v0.9.0
PathTouch is one path touched by a commit. Status is the git --raw / --name-status code: "A" added, "M" modified, "D" deleted, "R" renamed (SrcPath set to the pre-rename path), "C" copied (SrcPath set to the source path). The "T" (type change) code is rare in the aiwf planning tree (no symlinks, no submodules) and passes through unchanged.
PreSHA and PostSHA are the pre-image and post-image blob object ids from `git log --raw` (the `:<srcmode> <dstmode> <presha> <postsha> <status>` prefix). PostSHA is the blob at this commit; PreSHA is the blob at the parent THIS diff record is against — the single parent for a non-merge commit (merge commits never carry a PathTouch at all, per CommitRecord's doc). An all-zero id ("000…0", which BlobAllZero reports) means "no blob on that side" — a delete has an all-zero PostSHA, an add has an all-zero PreSHA. Both are empty when BulkRevwalk's underlying diff format carried no object ids (defensive; the production walk always requests `--raw`).
Carrying the blob ids lets status-reading consumers (internal/check/fsm_history_walker) fetch the file content by object id — a direct object read — instead of resolving `<commit>:<path>` per read, which forces git to walk the tree from the commit root to the blob on every call. Measured on the kernel tree the direct-id read is ~3× faster, and ids dedupe across the walk (a commit's PostSHA equals its child's PreSHA at the same path), so the same blob is read once (E-0053 / M-0216 AC-2).
type PathWrite ¶ added in v0.26.0
PathWrite is a single repo-relative path and its full content, one entry in the set of writes CommitTree folds into a constructed commit.
type ReconcileError ¶ added in v0.26.0
ReconcileError reports that CommitVerbChange's commit landed but the post-commit reconciliation into the live index failed. SHA names the commit that landed — git history already exists — so a caller must treat this differently from an outright commit failure (nothing to roll back; the fix is re-running the reconciliation, not retrying the commit).
func (*ReconcileError) Error ¶ added in v0.26.0
func (e *ReconcileError) Error() string
func (*ReconcileError) Unwrap ¶ added in v0.26.0
func (e *ReconcileError) Unwrap() error
type TestMetrics ¶
type TestMetrics struct {
Pass int `json:"pass"`
Fail int `json:"fail"`
Skip int `json:"skip"`
Total int `json:"total,omitempty"`
}
TestMetrics is the parsed payload of an aiwf-tests trailer. The four integer counts (Pass / Fail / Skip / Total) are the recognized keys in the I3 plan §4. Total is optional in the on-wire format and is derivable from Pass+Fail+Skip; readers that need it should call TotalOrDerive rather than reading Total directly.
func ParseStrictTestMetrics ¶
func ParseStrictTestMetrics(value string) (TestMetrics, error)
ParseStrictTestMetrics parses an aiwf-tests trailer value with write-strict semantics. Unknown keys, malformed `key=value` shapes, non-integer values, and negative values all return errors with a usage-shaped message.
This is the validator the CLI uses on the `--tests` flag boundary. Read-side parsing is separately tolerant (see ParseTestMetrics) so future format extensions don't break old binaries reading new commits.
Empty input returns a zero TestMetrics with no error — callers that require at least one recognized key should check the result for emptiness.
func ParseTestMetrics ¶
func ParseTestMetrics(value string) (TestMetrics, bool)
ParseTestMetrics parses an aiwf-tests trailer value of the form `pass=12 fail=0 skip=0 total=12`. Tokens are whitespace-separated. Recognized keys: pass, fail, skip, total. Unknown keys are silently ignored (forward compat — future trailer extensions don't break old readers). Malformed tokens (non-`key=value` shape, non-integer values, negative values) are skipped without erroring; callers get the metrics that did parse.
Returns ok=true when at least one recognized key produced a value; ok=false when the input was empty after trim or contained no recognized keys. Read-side parser by design — write-time validation (which rejects malformed input) lives on the verb boundary that constructs the trailer.
func (TestMetrics) TotalOrDerive ¶
func (m TestMetrics) TotalOrDerive() int
TotalOrDerive returns the on-wire Total when present (>0), otherwise Pass+Fail+Skip. Useful for renderers that want a denominator without caring whether the writer recorded it.
type Trailer ¶
Trailer is a single key=value line emitted in the commit body. The key conventionally uses the `aiwf-*` prefix.
func HeadTrailers ¶
HeadTrailers returns HEAD's trailer key/value pairs (via `git log -1 --pretty=%(trailers...)`). Tests use this to assert aiwf's structured trailers landed correctly.
func ParseTrailers ¶ added in v0.8.1
ParseTrailers parses a `git log %(trailers:only=true,unfold=true)` block into structured Trailer values. The format is one trailer per line, `Key: value`, possibly followed by a trailing newline; empty lines and malformed lines (missing colon, or starting with colon) are skipped. This is the canonical exported home for trailer-line parsing across the module.
func SortedTrailers ¶
SortedTrailers returns a copy of trailers in canonical write order. Known keys come first in trailerOrder sequence; unknown keys come last in lexicographic order. Repeated keys (e.g. multiple aiwf-scope-ends entries on one commit) preserve their input order among themselves so callers can rely on stable per-key emission.
type Worktree ¶ added in v0.8.1
type Worktree struct {
Path string // absolute path to the worktree's working directory
Branch string // branch name without the `refs/heads/` prefix, or "" for detached HEAD
HeadSHA string // 40-char SHA at HEAD
}
Worktree describes one entry in a repo's worktree set: the working directory path, the currently-checked-out branch (empty for detached HEAD), and the HEAD commit SHA.
Used by `aiwf status --worktrees` (G-0122) and any future worktree- aware verb. Returned in the order `git worktree list --porcelain` emits them — main checkout first, linked worktrees in administrative-listing order.
func ListWorktrees ¶ added in v0.8.1
ListWorktrees enumerates every worktree linked to the repo containing workdir. Returns the main checkout plus every linked worktree. Parses `git worktree list --porcelain` per the documented format: each entry is a sequence of `key value` lines terminated by a blank line. Recognized keys: `worktree` (path), `HEAD` (sha), `branch` (full ref name).
A worktree with detached HEAD has `detached` in lieu of `branch`; Branch comes back empty in that case. Bare repos (no working tree) are skipped.
G-0122.