gitops

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: May 11, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

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

View Source
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"
	TrailerScopeEnds    = "aiwf-scope-ends"
	TrailerReason       = "aiwf-reason"

	// I2.5 audit-only recovery (G24, plan step 5b).
	TrailerAuditOnly = "aiwf-audit-only"
)

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

View Source
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 Add

func Add(ctx context.Context, workdir string, paths ...string) error

Add stages paths in workdir.

func AddCommitSHA

func AddCommitSHA(ctx context.Context, workdir, relPath string) (string, error)

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 Commit

func Commit(ctx context.Context, workdir, subject, body string, trailers []Trailer) error

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

func CommitAllowEmpty(ctx context.Context, workdir, subject, body string, trailers []Trailer) error

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 CommitMessage

func CommitMessage(subject, body string, trailers []Trailer) string

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 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

func GitDir(ctx context.Context, workdir string) (string, error)

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

func HasAnyRemoteTrackingRefs(ctx context.Context, workdir string) (bool, error)

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

func HasRef(ctx context.Context, workdir, ref string) (bool, error)

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

func HasRemotes(ctx context.Context, workdir string) (bool, error)

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

func HeadBody(ctx context.Context, workdir string) (string, error)

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

func HeadSubject(ctx context.Context, workdir string) (string, error)

HeadSubject returns the subject line of HEAD's commit. Used by tests to verify a commit landed; not used at runtime.

func HooksDir

func HooksDir(ctx context.Context, workdir string) (string, error)

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 `<gitDir>/hooks` is returned. The result is always an absolute path with symlinks resolved, matching git's own canonicalization (via `--absolute-git-dir`).

`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." If git is genuinely broken, the GitDir call below surfaces the same underlying issue with a clearer message.

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 Init

func Init(ctx context.Context, workdir string) error

Init initializes a git repository at workdir. Used by tests; not invoked by `aiwf` verbs at runtime.

func IsAncestor

func IsAncestor(ctx context.Context, workdir, commit, ref string) (bool, error)

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 IsRepo

func IsRepo(ctx context.Context, workdir string) bool

IsRepo reports whether workdir is inside a git working tree.

func LsTreePaths

func LsTreePaths(ctx context.Context, workdir, ref string, prefixes ...string) ([]string, error)

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 Mv

func Mv(ctx context.Context, workdir, from, to string) error

Mv runs `git mv` to relocate a tracked file or directory. from and to are paths relative to workdir.

func ReadFromHEAD

func ReadFromHEAD(ctx context.Context, workdir, relPath string) ([]byte, error)

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 RenamesFromRef added in v0.8.0

func RenamesFromRef(ctx context.Context, workdir, ref string) (map[string]string, error)

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 Restore

func Restore(ctx context.Context, workdir string, paths ...string) error

Restore resets the index and worktree to HEAD for the given paths. Used by Apply to roll back partial verb mutations after a failure. Paths that don't exist at HEAD (brand-new files staged but never committed) produce a "pathspec did not match" warning that this function suppresses — the caller separately removes such files.

func StagedPaths

func StagedPaths(ctx context.Context, workdir string) ([]string, error)

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 StashPop

func StashPop(ctx context.Context, workdir string) error

StashPop restores the most recently stashed entry into the index, reversing StashStaged. Errors propagate verbatim — a pop failure after the verb's commit landed is recoverable by hand (`git stash list` / `git stash pop`); the kernel does not silently drop the stash.

func StashStaged

func StashStaged(ctx context.Context, workdir, message string) error

StashStaged sets aside the user's currently-staged changes so the verb's commit boundary is exactly the verb's mutation plus any hook-added files (notably the pre-commit STATUS.md regeneration). Pair with StashPop after the commit lands.

`git stash push --staged` (git ≥ 2.35) stashes only what's in the index; the worktree side of those paths is left alone. Untracked files and unstaged worktree edits are not affected. The message is stamped into the stash entry so a subsequent `git stash list` makes the source obvious if recovery becomes manual.

G34 background: switched from `git commit -- <paths>` (--only) to stash because pre-commit hooks that `git add` extra files (like the aiwf STATUS.md hook) interact poorly with --only — git records the hook's addition in HEAD but resets the post-commit index to only the explicitly-named paths, leaving a phantom staged-deletion behind. Stash gives the verb a clean index to commit against without disturbing hook semantics.

func ValidateTrailer

func ValidateTrailer(key, value string) error

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".

Types

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

type Trailer struct {
	Key   string
	Value string
}

Trailer is a single key=value line emitted in the commit body. The key conventionally uses the `aiwf-*` prefix.

func HeadTrailers

func HeadTrailers(ctx context.Context, workdir string) ([]Trailer, error)

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 SortedTrailers

func SortedTrailers(trailers []Trailer) []Trailer

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.

Jump to

Keyboard shortcuts

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