git

package
v0.17.0 Latest Latest
Warning

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

Go to latest
Published: Oct 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

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

const CodeCommandFailed errs.Code = coregit.CodeCommandFailed

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.

const CodePathAbsent errs.Code = coregit.CodePathAbsent

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.

const CodeRepositoryUnresolved errs.Code = coregit.CodeRepositoryUnresolved

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.

type ChangedSet

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

type ChangedSet = coregit.ChangedSet

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

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

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.

type Include

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

type Include = svcgit.IncludeFunc

type LineRange

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

type LineRange = coregit.LineRangeValue

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

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.

Generated by gomarkdoc

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