git

package
v0.59.0 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 4 Imported by: 0

README

git

import "github.com/kitsunium/sdk/framework/git"

Package git answers what a branch changed and what a working tree is at, by shelling out to the git binary.

It is the framework's public package over framework/internal/service/git (ADR 0158): the value types are aliases of the core git contract's types and the functions delegate straight to the service implementation. What a running program was BUILT from is not here — that is pkg/v1/proc/process's Build and Self, which stay in the SDK; this package asks git about a working tree.

Usage

Resolve the changed set, then ask about it at whichever granularity matters:

import (
	"github.com/kitsunium/sdk/framework/git"
)

res := git.Resolve(ctx, git.Config{Root: "/path/to/repo"})
if res.Degraded() {
	// res.Reason says why; treat EVERYTHING as in scope.
	return
}
if res.Set.ContainsLine(file, line) { /* this line moved */ }
Degrading is not failing, and an empty set is not a degrade

Resolve returns no error. Any condition that prevents a trustworthy answer — no repository, a shallow clone, an unresolved default branch, no merge-base, a git invocation that failed or timed out — produces a ResolutionValue with FullFallback set and a Reason, never a partial set and never an empty one.

The distinction is the reason this package exists in this shape. "Nothing changed" and "I could not tell what changed" are opposite instructions to the caller, and a resolver that answered the second with an empty set would make a scoped review silently pass on a branch it never examined. Check Degraded before reading Set.

What counts as changed

The set spans four sources, because a review that ignored uncommitted work would contradict what the author sees: the three-dot merge-base delta to HEAD, the index, the working tree, and untracked files. Renames and copies are detected (-M -C); a pure rename or a deletion marks the file and its directory touched while contributing no line range, so ContainsFile answers true where ContainsLine answers false for every line.

Config.Include is the caller's policy for which files belong. A nil Include admits everything; a Go tool passes a ".go" suffix test and a generated-file probe. The source implementation hard-coded exactly that pair, and this package deliberately does not: it is one caller's policy, not the domain's.

Which spelling of a path answers

Queries are compared lexically, so two spellings of one file do not match each other — with one exception, and it is the spelling a caller does not choose. git canonicalises the repository root, so pointing Config.Root at a symbolic link records every path under the link's target. Resolve therefore records every entry under BOTH the root you gave and the one git reports, so either answers and a query costs exactly what it did before. An indirection anywhere else in a path you query is still lexical and still does not match.

What a working tree is at

Head answers the question a build description asks about a local module: which commit its working tree is on, when that commit was made, and whether tracked files differ from it.

head, err := git.Head(ctx, "/path/to/module")
if err != nil {
	// no repository, no git, or no commit yet: go without.
}
fmt.Println(head.Revision, head.Time, head.Modified)

Modified counts TRACKED files only, staged or not; an untracked file does not make a tree modified. That is narrower than the vcs.modified stamp Go writes into a binary, which counts untracked files too. Nothing is cached, because modified is the one fact here that changes without a commit.

Running against a repository you do not control

Every invocation is hardened against a hostile `.git/config`, which travels with a clone. `core.fsmonitor` and `core.hooksPath` are neutralised with -c (which beats every config file), and `--no-ext-diff` is injected into the subcommands that honour `diff.external` — setting that key empty does not disable it, it makes git try to execute "" and abort. Both were demonstrated executing an attacker-chosen command on a read-only query before the guard, and not after.

A second group executes nothing and is guarded for a different reason. `diff.srcPrefix`, `diff.dstPrefix`, `diff.mnemonicPrefix` and `diff.noprefix` rename the `a/` and `b/` prefixes of a diff header, which files the line ranges under a path nobody queries: measured, ContainsFile stayed true and ContainsLine went false for a line that had just changed. All four are pinned to git's defaults. See the service package's CLAUDE.md for what was deliberately NOT hardened, and why.

Index

Constants

const CodeCommandFailed errs.Code = 139522

CodeCommandFailed identifies a version-control command that exited non-zero — a corrupted object store, a permission error, a missing binary.

Initialised with github.com/kitsunium/sdk/framework/internal/core/git.CodeCommandFailed.

Code 0.2.33.2

const CodePathAbsent errs.Code = 139523

CodePathAbsent identifies a path that does not exist at the requested commit, which is deliberately distinct from a successful read of an empty file. It is what ShowFile returns when the commit resolves and its tree holds no object at the path.

Initialised with github.com/kitsunium/sdk/framework/internal/core/git.CodePathAbsent.

Code 0.2.33.3

const CodeRepositoryUnresolved errs.Code = 139521

CodeRepositoryUnresolved identifies a path that is not inside a readable repository. Match it with errs.HasCode: it is the one refusal a caller can act on by falling back to a non-VCS path.

Initialised with github.com/kitsunium/sdk/framework/internal/core/git.CodeRepositoryUnresolved.

Code 0.2.33.1

func GitDir

func GitDir(ctx context.Context, root string) (gitDir string, err error)

GitDir resolves the git directory governing root — the one holding HEAD and the index — and memoizes it per root.

It never assumes `.git` is a directory: in a linked worktree or a submodule it is a `gitdir:` pointer FILE, and this follows it. The result is absolute and cleaned, and failures are never memoized.

The memo is re-validated on every call against two lstats — roughly a thousandth of the subprocess they stand in for — so a repository that moved, one deleted and re-created, and a root that becomes its own repository under a parent one each get a fresh answer. A repository created at a directory BETWEEN root and the worktree top level is not noticed.

func Head

func Head(ctx context.Context, dir string) (head HeadState, err error)

Head reports what the working tree containing dir is at: its head commit, that commit's time, and whether tracked files differ from it.

A dir outside any repository — or absent, or on a machine without git — is CodeRepositoryUnresolved. A repository whose HEAD names no commit yet, a bare repository and a failed read are CodeCommandFailed. The three git commands it runs are hardened like every other one here, and the commit time is read from the raw commit object rather than through `git log`, so a planted signature program has nothing to verify.

func Resolve

func Resolve(ctx context.Context, cfg Config) Resolution

Resolve computes the changed set for the repository containing cfg.Root, returning a degraded Resolution rather than an error when it cannot be trusted. Check Resolution.Degraded before reading Set.

func ShowFile

func ShowFile(ctx context.Context, repoRoot string, sha string, relPath string) (content string, err error)

ShowFile returns the content of relPath at commit sha, verbatim.

Its two refusals are different instructions. CodePathAbsent means the commit is readable and holds nothing at that path, which is what tells "deleted" from "emptied" — an empty file is a successful read of "". CodeCommandFailed means anything else: a commit that does not resolve, an unreadable object store, no git. Match with errs.HasCode.

type ChangedSet

ChangedSet is what a branch changed, queryable by line, file or directory. It aliases the core git port.

type ChangedSet = coregit.ChangedSet

ChangedSet is an alias of github.com/kitsunium/sdk/framework/internal/core/git.ChangedSet, declared as:

type ChangedSet interface {
	// ContainsDir reports whether absDir directly encloses any touched file.
	// It is not recursive: a parent of a touched directory is not itself
	// touched.
	ContainsDir(absDir string) bool
	// ContainsFile reports whether absFile appears anywhere in the diff,
	// including as the old side of a rename or a deletion.
	ContainsFile(absFile string) bool
	// ContainsLine reports whether line on absFile falls inside a changed hunk.
	// Lines are 1-based and the bounds are inclusive.
	ContainsLine(absFile string, line int) bool
	// IsEmpty reports whether nothing at all was touched — a clean branch, or
	// one whose every change the caller's own filter excluded.
	IsEmpty() bool
}

func (ChangedSet) ContainsDir
func (ChangedSet) ContainsDir(absDir string) bool

ContainsDir reports whether absDir directly encloses any touched file. It is not recursive: a parent of a touched directory is not itself touched.

func (ChangedSet) ContainsFile
func (ChangedSet) ContainsFile(absFile string) bool

ContainsFile reports whether absFile appears anywhere in the diff, including as the old side of a rename or a deletion.

func (ChangedSet) ContainsLine
func (ChangedSet) ContainsLine(absFile string, line int) bool

ContainsLine reports whether line on absFile falls inside a changed hunk. Lines are 1-based and the bounds are inclusive.

func (ChangedSet) IsEmpty
func (ChangedSet) IsEmpty() bool

IsEmpty reports whether nothing at all was touched — a clean branch, or one whose every change the caller's own filter excluded.

type Config

Config is where the repository is and which files the caller counts as changed. It aliases the service type: these are one implementation's construction parameters, which is what ADR 0074 says belongs with the engine.

type Config = svcgit.Config

Config is an alias of github.com/kitsunium/sdk/framework/internal/service/git.Config, declared as:

type Config struct {
	// Root is a directory inside the repository to resolve from. Empty resolves
	// from the process working directory.
	Root    string
	// Include decides which files belong in the changed set. Nil admits all.
	//
	// This is where a caller states the policy the source implementation had
	// hard-coded: a Go linter passes a test for a ".go" suffix and a
	// generated-file probe; a documentation tool passes something else; a tool
	// that wants everything passes nil.
	Include IncludeFunc
}

type HeadState

HeadState is what a working tree is at: the commit HEAD names, its committer date in the offset it was recorded with, and whether a tracked file differs from it. It aliases the service type — the core git port models no commit, so this is one engine's value (ADR 0074).

type HeadState = svcgit.HeadValue

HeadState is an alias of github.com/kitsunium/sdk/framework/internal/service/git.HeadValue, declared as:

type HeadState struct {
	// Time is the commit's committer date, in the offset it was recorded
	// with — what `git log --format=%cI` prints.
	Time     time.Time
	// Revision is the full object name of the commit HEAD resolves to.
	Revision string
	// Modified reports that a TRACKED file differs from HEAD, staged or not.
	// An untracked file does not count: it is not part of what the commit
	// describes, and a build stamp that turned dirty whenever an editor left
	// a swap file would say nothing. This is narrower than the vcs.modified
	// Go stamps into a binary, which counts untracked files too.
	Modified bool
}

type Include

Include decides which files belong in the changed set. A nil Include admits every file.

type Include = svcgit.IncludeFunc

Include is an alias of github.com/kitsunium/sdk/framework/internal/service/git.IncludeFunc, declared as:

type Include func(absPath string) bool

type LineRange

LineRange is an inclusive, 1-based run of changed lines. It aliases the core git value type.

type LineRange = coregit.LineRangeValue

LineRange is an alias of github.com/kitsunium/sdk/framework/internal/core/git.LineRangeValue, declared as:

type LineRange struct {
	// Start is the first changed line, inclusive and 1-based.
	Start int
	// End is the last changed line, inclusive.
	End   int
}

func (LineRange) Contains
func (LineRange) Contains(line int) bool

Contains reports whether line falls inside the range. Both bounds are inclusive, which is what makes a single-line hunk (Start == End) match.

type Resolution

Resolution is the outcome of resolving a changed set, including the degraded outcome where none could be trusted. It aliases the core git value type.

type Resolution = coregit.ResolutionValue

Resolution is an alias of github.com/kitsunium/sdk/framework/internal/core/git.ResolutionValue, declared as:

type Resolution struct {
	// Set is what the branch changed, or nil when FullFallback is true.
	Set          ChangedSet
	// BaseRef is the default-branch ref the comparison used, e.g. "origin/main".
	// Empty when degraded.
	BaseRef      string
	// BaseSHA is the merge-base commit between BaseRef and HEAD. Empty when
	// degraded.
	BaseSHA      string
	// HeadSHA is the commit HEAD resolved to. Empty when degraded.
	HeadSHA      string
	// FullFallback reports that no trustworthy changed set could be computed and
	// the caller must fall back to considering everything in scope.
	FullFallback bool
	// Reason is the human-readable cause of the fallback, meant to be surfaced
	// rather than swallowed. Empty when not degraded.
	Reason       string
}

func (Resolution) Degraded
func (*Resolution) Degraded() bool

Degraded reports whether the resolution failed to produce a trustworthy set and the caller must treat everything as in scope.

It is the predicate callers want, and it reads from FullFallback rather than from a nil Set so the two can never disagree.

The receiver is a pointer because the value is 88 bytes — four strings, an interface and a bool — and copying all of it to read one bool is what KTN-VAR-BIGSTRUCT exists to catch. Assign the resolution to a variable and call it on that; the chained form Resolve(...).Degraded() does not compile, which is a fair trade for not copying 88 bytes per query.

Generated from docs/api by tools/genindex.

Documentation

Overview

Package git answers what a branch changed and what a working tree is at, by shelling out to the git binary.

It is the framework's public package over framework/internal/service/git (ADR 0158): the value types are aliases of the core git contract's types and the functions delegate straight to the service implementation. What a running program was BUILT from is not here — that is pkg/v1/proc/process's Build and Self, which stay in the SDK; this package asks git about a working tree.

Usage

Resolve the changed set, then ask about it at whichever granularity matters:

import (
	"github.com/kitsunium/sdk/framework/git"
)

res := git.Resolve(ctx, git.Config{Root: "/path/to/repo"})
if res.Degraded() {
	// res.Reason says why; treat EVERYTHING as in scope.
	return
}
if res.Set.ContainsLine(file, line) { /* this line moved */ }

Degrading is not failing, and an empty set is not a degrade

Resolve returns no error. Any condition that prevents a trustworthy answer — no repository, a shallow clone, an unresolved default branch, no merge-base, a git invocation that failed or timed out — produces a ResolutionValue with FullFallback set and a Reason, never a partial set and never an empty one.

The distinction is the reason this package exists in this shape. "Nothing changed" and "I could not tell what changed" are opposite instructions to the caller, and a resolver that answered the second with an empty set would make a scoped review silently pass on a branch it never examined. Check Degraded before reading Set.

What counts as changed

The set spans four sources, because a review that ignored uncommitted work would contradict what the author sees: the three-dot merge-base delta to HEAD, the index, the working tree, and untracked files. Renames and copies are detected (-M -C); a pure rename or a deletion marks the file and its directory touched while contributing no line range, so ContainsFile answers true where ContainsLine answers false for every line.

Config.Include is the caller's policy for which files belong. A nil Include admits everything; a Go tool passes a ".go" suffix test and a generated-file probe. The source implementation hard-coded exactly that pair, and this package deliberately does not: it is one caller's policy, not the domain's.

Which spelling of a path answers

Queries are compared lexically, so two spellings of one file do not match each other — with one exception, and it is the spelling a caller does not choose. git canonicalises the repository root, so pointing Config.Root at a symbolic link records every path under the link's target. Resolve therefore records every entry under BOTH the root you gave and the one git reports, so either answers and a query costs exactly what it did before. An indirection anywhere else in a path you query is still lexical and still does not match.

What a working tree is at

Head answers the question a build description asks about a local module: which commit its working tree is on, when that commit was made, and whether tracked files differ from it.

head, err := git.Head(ctx, "/path/to/module")
if err != nil {
	// no repository, no git, or no commit yet: go without.
}
fmt.Println(head.Revision, head.Time, head.Modified)

Modified counts TRACKED files only, staged or not; an untracked file does not make a tree modified. That is narrower than the vcs.modified stamp Go writes into a binary, which counts untracked files too. Nothing is cached, because modified is the one fact here that changes without a commit.

Running against a repository you do not control

Every invocation is hardened against a hostile `.git/config`, which travels with a clone. `core.fsmonitor` and `core.hooksPath` are neutralised with -c (which beats every config file), and `--no-ext-diff` is injected into the subcommands that honour `diff.external` — setting that key empty does not disable it, it makes git try to execute "" and abort. Both were demonstrated executing an attacker-chosen command on a read-only query before the guard, and not after.

A second group executes nothing and is guarded for a different reason. `diff.srcPrefix`, `diff.dstPrefix`, `diff.mnemonicPrefix` and `diff.noprefix` rename the `a/` and `b/` prefixes of a diff header, which files the line ranges under a path nobody queries: measured, ContainsFile stayed true and ContainsLine went false for a line that had just changed. All four are pinned to git's defaults. See the service package's CLAUDE.md for what was deliberately NOT hardened, and why.

Index

Constants

View Source
const CodeCommandFailed errs.Code = coregit.CodeCommandFailed

CodeCommandFailed identifies a version-control command that exited non-zero — a corrupted object store, a permission error, a missing binary.

View Source
const CodePathAbsent errs.Code = coregit.CodePathAbsent

CodePathAbsent identifies a path that does not exist at the requested commit, which is deliberately distinct from a successful read of an empty file. It is what ShowFile returns when the commit resolves and its tree holds no object at the path.

View Source
const CodeRepositoryUnresolved errs.Code = coregit.CodeRepositoryUnresolved

CodeRepositoryUnresolved identifies a path that is not inside a readable repository. Match it with errs.HasCode: it is the one refusal a caller can act on by falling back to a non-VCS path.

Variables

This section is empty.

Functions

func GitDir

func GitDir(ctx context.Context, root string) (gitDir string, err error)

GitDir resolves the git directory governing root — the one holding HEAD and the index — and memoizes it per root.

It never assumes `.git` is a directory: in a linked worktree or a submodule it is a `gitdir:` pointer FILE, and this follows it. The result is absolute and cleaned, and failures are never memoized.

The memo is re-validated on every call against two lstats — roughly a thousandth of the subprocess they stand in for — so a repository that moved, one deleted and re-created, and a root that becomes its own repository under a parent one each get a fresh answer. A repository created at a directory BETWEEN root and the worktree top level is not noticed.

func ShowFile

func ShowFile(ctx context.Context, repoRoot, sha, relPath string) (content string, err error)

ShowFile returns the content of relPath at commit sha, verbatim.

Its two refusals are different instructions. CodePathAbsent means the commit is readable and holds nothing at that path, which is what tells "deleted" from "emptied" — an empty file is a successful read of "". CodeCommandFailed means anything else: a commit that does not resolve, an unreadable object store, no git. Match with errs.HasCode.

Types

type ChangedSet

type ChangedSet = coregit.ChangedSet

ChangedSet is what a branch changed, queryable by line, file or directory. It aliases the core git port.

type Config

type Config = svcgit.Config

Config is where the repository is and which files the caller counts as changed. It aliases the service type: these are one implementation's construction parameters, which is what ADR 0074 says belongs with the engine.

type HeadState

type HeadState = svcgit.HeadValue

HeadState is what a working tree is at: the commit HEAD names, its committer date in the offset it was recorded with, and whether a tracked file differs from it. It aliases the service type — the core git port models no commit, so this is one engine's value (ADR 0074).

func Head(ctx context.Context, dir string) (head HeadState, err error)

Head reports what the working tree containing dir is at: its head commit, that commit's time, and whether tracked files differ from it.

A dir outside any repository — or absent, or on a machine without git — is CodeRepositoryUnresolved. A repository whose HEAD names no commit yet, a bare repository and a failed read are CodeCommandFailed. The three git commands it runs are hardened like every other one here, and the commit time is read from the raw commit object rather than through `git log`, so a planted signature program has nothing to verify.

type Include

type Include = svcgit.IncludeFunc

Include decides which files belong in the changed set. A nil Include admits every file.

type LineRange

type LineRange = coregit.LineRangeValue

LineRange is an inclusive, 1-based run of changed lines. It aliases the core git value type.

type Resolution

type Resolution = coregit.ResolutionValue

Resolution is the outcome of resolving a changed set, including the degraded outcome where none could be trusted. It aliases the core git value type.

func Resolve

func Resolve(ctx context.Context, cfg Config) Resolution

Resolve computes the changed set for the repository containing cfg.Root, returning a degraded Resolution rather than an error when it cannot be trusted. Check Resolution.Degraded before reading Set.

Jump to

Keyboard shortcuts

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