On this page
The single git-execution boundary: argv construction, the subcommand classification table, and the closed list of sites exempt from the repository-root pin.
#internal/gitexec
#internal/gitexec
Package gitexec is safegit's single git-execution boundary.
Three things live here and nowhere else:
1. Process construction. Every git subprocess safegit starts is built by Command: the argv prefix, the environment assembly and the context-carried execution overrides are applied in exactly one place. 2. The git argv vocabulary. The classification table in classify.go is the single authority over which git subcommands safegit may invoke and what each of them can do to a repository. 3. The declared directory-pin exemption table (exemptions.go): the closed list of sites that are NOT subject to the repo-root working-directory pin, each with the reason it cannot be.
The package is a leaf -- it imports only the standard library -- so both internal/git (safegit's git plumbing interface) and internal/submodule (which cannot import internal/git without a cycle) route through it.
#NoDoor
const NoDoor DoorID = ""NoDoor is the absence of a door: the call site does not let git author a commit, and an authoring argv from it is refused. It is the value every site but one passes.
#DoorRebasePassthrough
const DoorRebasePassthrough DoorID = "main.runRebase"DoorRebasePassthrough covers main.runRebase.
#MutatesObjects
const MutatesObjects Effect = 1 << iotaMutatesObjects: the invocation can add objects to, or remove objects from, the object store.
#MutatesRefs
const MutatesRefsMutatesRefs: the invocation can create, move or delete a ref (local or, for push, on a remote).
#MutatesIndex
const MutatesIndexMutatesIndex: the invocation can write a git index file.
#MutatesWorktree
const MutatesWorktreeMutatesWorktree: the invocation can create, change or delete a file in the working tree.
#MutatesConfig
const MutatesConfigMutatesConfig: the invocation can write git configuration.
#Network
const NetworkNetwork: the invocation can contact a remote.
#ObserveOnly
const ObserveOnly Effect = 0ObserveOnly is the empty effect set.
#KindExplicitDir
const KindExplicitDir ExemptionKind = "explicit-dir"KindExplicitDir: the site receives a git directory and work tree as its own arguments and targets that repository, so there is no operator working directory left for a pin to correct.
#KindOperatorCwd
const KindOperatorCwd ExemptionKind = "operator-cwd"KindOperatorCwd: the argv is the operator's own, forwarded to git verbatim. git must read it in the directory the operator typed it in; re-rooting it would silently change what the operator's pathspecs mean.
#KindEffectsHandle
const KindEffectsHandle ExemptionKind = "effects-handle"KindEffectsHandle: safegit does not construct this subprocess -- the strictcli effects handle does, so that --dry-run records the invocation instead of performing it. The pin cannot be applied to a process safegit does not start, so each such site is directory-independent (an argv naming refs and remotes, never paths) or DECLARES the working directory to the effects handle itself, which is the pin's own value stated at the site.
#ExemptRunWithGitDir
const ExemptRunWithGitDir ExemptionID = "internal/git.RunWithGitDir"ExemptRunWithGitDir covers git.RunWithGitDir.
#ExemptCatFileBatchAllWithDir
const ExemptCatFileBatchAllWithDir ExemptionID = "internal/git.CatFileBatchAllWithDir"ExemptCatFileBatchAllWithDir covers git.CatFileBatchAllWithDir.
#ExemptCatFileBatchSHAsWithDir
const ExemptCatFileBatchSHAsWithDir ExemptionID = "internal/git.CatFileBatchSHAsWithDir"ExemptCatFileBatchSHAsWithDir covers git.CatFileBatchSHAsWithDir.
#ExemptSubmoduleRunGit
const ExemptSubmoduleRunGit ExemptionID = "internal/submodule.runGit"ExemptSubmoduleRunGit covers internal/submodule's own git runner.
#ExemptAutoBumpParentPointer
const ExemptAutoBumpParentPointer ExemptionID = "main.autoBumpParent"ExemptAutoBumpParentPointer covers main.autoBumpParent's pointer read.
#ExemptGuardedPassthrough
const ExemptGuardedPassthrough ExemptionID = "internal/git.RunPassthrough"ExemptGuardedPassthrough covers git.RunPassthrough.
#ExemptGitMutation
const ExemptGitMutation ExemptionID = "main.runGitMutation"ExemptGitMutation covers main.runGitMutation.
#ExemptGitPush
const ExemptGitPush ExemptionID = "main.execGitPush"ExemptGitPush covers main.execGitPush.
#ExemptCommitRefUpdate
const ExemptCommitRefUpdate ExemptionID = "main.effectsRefUpdate"ExemptCommitRefUpdate covers main.effectsRefUpdate, the commit pipeline's ref update.
#ExemptHistoryRewriteRecord
const ExemptHistoryRewriteRecord ExemptionID = "main.recordHistoryRewrite"ExemptHistoryRewriteRecord covers main.recordHistoryRewrite.
#ExemptUndoRefUpdate
const ExemptUndoRefUpdate ExemptionID = "main.recordUndoRefUpdate"ExemptUndoRefUpdate covers main.recordUndoRefUpdate, undo's own ref move. It is a row of its own rather than the commit pipeline's: the two argv shapes differ (undo also deletes a ref), and reusing the commit row would make its identifier name a site it does not cover.
#ExemptBackupRestoreGit
const ExemptBackupRestoreGit ExemptionID = "main.runBackupGit"ExemptBackupRestoreGit covers main.runBackupGit, the invocations backup restore mints.
#ExemptDoctorRepair
const ExemptDoctorRepair ExemptionID = "main.runRepairGit"ExemptDoctorRepair covers main.runRepairGit, the git invocations doctor --action fix makes to repair git's own leftovers.
#Binary
const Binary = "git"Binary is the git executable safegit invokes.
#DoorID
type DoorID stringDoorID identifies one declared door through the single-authorship boundary. The value is the code site it covers, so a reader of the table can go straight there.
#AuthoringDoor
type AuthoringDoor structAuthoringDoor is one row of the declared table.
#Effect
type Effect uint16Effect is a set of things one git invocation can do to a repository.
The zero value, ObserveOnly, means the invocation changes nothing: no object is added or removed, no ref moves, no index or working-tree file is written and no remote is contacted.
#ConditionalEffect
type ConditionalEffect structConditionalEffect adds effects to a verb only when one of Tokens appears in the argv. Tokens are matched literally against whole argv elements, so both option spellings ("-w", "--hard") and subcommand words ("expire", "delete") work.
#Verb
type Verb structVerb is one git subcommand safegit is allowed to invoke, with what that invocation can do.
Base holds the effects every invocation of the verb has; Conditional adds the effects that depend on the argv. Where a verb's effects cannot be decided from a single token, Base is deliberately the WIDER of the possibilities: a consumer that over-quarantines or under-permits is safe, one that under-quarantines is not.
#ExemptionKind
type ExemptionKind stringExemptionKind says WHY a site is not subject to the repository-root pin.
#ExemptionID
type ExemptionID stringExemptionID identifies one declared exemption. The value is the code site it covers, so a reader of the table can go straight there.
#DirPinExemption
type DirPinExemption structDirPinExemption is one row of the table.
#Spec
type Spec structSpec describes one git invocation.
#Error
type Error struct{ Msg string }Error is the boundary's own error type, so a caller can tell a refusal by the boundary from a failure reported by git.
#AuthoringDoors
func AuthoringDoors() []AuthoringDoorAuthoringDoors returns the declared table, sorted by ID. It returns a copy: the table is the boundary's, not a caller's, to change.
#Authoring
func Authoring(args []string) boolAuthoring reports whether an argv would let GIT author a commit: a verb the table marks Authors, with none of that verb's suppressing tokens present.
An argv the table does not declare, and one that names no subcommand at all, report TRUE -- the same default-deny WritesObjects and WritesWorktree take. An unknown invocation is never assumed harmless. Validate runs its vocabulary check first, so neither case ever produces the authoring refusal in practice; the default is for any other reader of this view.
#Verbs
func Verbs() []VerbVerbs returns the declared vocabulary, sorted by name. It returns a copy.
#Lookup
func Lookup(name string) (Verb, bool)Lookup returns the declared verb by subcommand name.
#Subcommand
func Subcommand(args []string) (string, bool)Subcommand returns the git subcommand an argv names.
The argv may or may not carry the binary name and the global prefix; both are skipped. A global option that IS the whole invocation (git --version) is returned as the subcommand, because that is how the table declares it.
#Validate
func Validate(door DoorID, args []string) errorValidate errors when an argv names no subcommand at all, when it names one the classification table does not declare, and when its shape would let git author a commit from a call site that declares no door for it (see authoring.go).
door is the call site's declared permission to let git author. Every site but one passes NoDoor.
#EffectsOf
func EffectsOf(args []string) (Effect, error)EffectsOf returns the effects a specific argv can have. It errors on an argv Validate would refuse.
#WritesObjects
func WritesObjects(args []string) boolWritesObjects reports whether an argv can change the object store. This is the view Phase 3.1's object quarantine reads. An argv the table does not declare reports true: an unknown invocation is never assumed harmless.
#WritesWorktree
func WritesWorktree(args []string) boolWritesWorktree reports whether an argv can create, change or delete a file in the working tree. This is the view the guarded passthroughs' uncommitted-work check reads: a command that cannot touch the working tree has no reason to be refused over uncommitted work, and one that can must be refused whichever way it is spelled.
Deriving it is the point. Two hand-written approximations of git's vocabulary lived in the handlers and both were wrong -- reset guarded only --hard, and bisect kept a subcommand list missing skip, run and replay. The table is the single authority over what a git invocation does, so the guard reads it rather than restating it.
An argv the table does not declare reports TRUE, the same default-deny WritesObjects takes: an unknown invocation is never assumed harmless.
#IsObserveOnly
func IsObserveOnly(args []string) boolIsObserveOnly reports whether an argv changes nothing at all. An argv the table does not declare reports false.
#ObservePrefixes
func ObservePrefixes() [][]stringObservePrefixes renders the table's read view as the argv PREFIXES the framework's proc-observe allowlist takes: an effects-handle invocation whose argv starts with one of them is an observe, which executes even in a dry run and is never written to the would-do log.
The list is GENERATED from the classification table rather than written out beside it, so a verb cannot become observe-authorized without the table saying it changes nothing. Two properties make the generation safe:
- A verb with any CONDITIONAL effect is excluded, however read-only its base is. The allowlist matches a PREFIX, so reflog (observe-only until expire follows it) would admit reflog expire --all -- a ref deletion executing in the middle of a dry run. Only verbs that are observe-only whatever follows them are admitted. - Each prefix carries the binary and the global prefix, because that is what an effects-handle argv literally starts with (see ArgvAny). A two-element prefix of "git" plus the global option would match every git invocation safegit makes, mutations included.
#DirPinExemptions
func DirPinExemptions() []DirPinExemptionDirPinExemptions returns the declared table, sorted by ID. It returns a copy.
#MustBeExempt
func MustBeExempt(id ExemptionID, kind ExemptionKind)MustBeExempt asserts that id is declared with the given kind. It panics otherwise: an undeclared exemption is a programming error in safegit, not a runtime condition, and the identifiers are compile-time constants.
#GlobalPrefix
func GlobalPrefix() []stringGlobalPrefix returns the argv prefix safegit puts on every git invocation. It returns a copy: the prefix is the boundary's, not a caller's, to change.
#WithDir
func WithDir(ctx context.Context, gitDir, workTree string) context.ContextWithDir returns a context that targets a specific repository. Every git subprocess built from it sets GIT_DIR, GIT_WORK_TREE and its working directory accordingly, regardless of the process's own directory.
#DirOverride
func DirOverride(ctx context.Context) (gitDir, workTree string, ok bool)DirOverride reports the context-carried repository override, if any.
#WithRoot
func WithRoot(ctx context.Context, root string) context.ContextWithRoot returns a context carrying the repository-root pin: every git subprocess safegit itself constructs from it runs with its working directory set to root.
The pin exists because a large part of git's plumbing vocabulary is scoped to the process working directory -- ls-files defaults to the pathspec ".", ls-tree prefixes the cwd path onto the tree it reads, apply resolves the paths inside a patch against the cwd -- so an operator invoking safegit from a subdirectory would otherwise silently narrow what safegit sees and what it rewrites. safegit's own argv is built from repo-relative or absolute paths, so pinning it to the root is what makes it mean the same thing from everywhere.
An empty root returns ctx unchanged: a repository with no work tree has no root to pin to.
#Root
func Root(ctx context.Context) (string, bool)Root reports the context-carried repository-root pin, if any.
#WithoutRootPin
func WithoutRootPin(ctx context.Context, id ExemptionID) context.ContextWithoutRootPin returns a context whose repository-root pin does not apply, recording which declared exemption suspends it. It panics when id is not declared in the exemption table with kind KindOperatorCwd: the table is the only way a site can escape the pin.
#WithPreview
func WithPreview(ctx context.Context) context.ContextWithPreview marks a context as a preview: the --dry-run dispatch, which promises to change nothing on disk. It is what makes the quarantine ENFORCEABLE rather than merely available -- see Command, which refuses an object-writing invocation on a previewing context that carries no quarantine.
The mark is separate from the quarantine itself precisely so the refusal has something to fire on: a previewing command that forgot to install a quarantine is exactly the case worth catching.
#InPreview
func InPreview(ctx context.Context) boolInPreview reports whether this context belongs to a preview.
#WithObjectQuarantine
func WithObjectQuarantine(ctx context.Context, dir string, alternates ...string) context.ContextWithObjectQuarantine returns a previewing context whose git subprocesses write every object they create into dir instead of into the repository, while still reading the object stores named by alternates.
A preview that promises to change nothing cannot make that promise while git add, git write-tree and git commit-tree deposit blobs, trees and commits in the repository's own store, where they stay as unreferenced loose objects. Pointing GIT_OBJECT_DIRECTORY at a throwaway directory keeps the preview's arithmetic exact -- the tree SHA it computes is the tree SHA the real run would compute -- and leaves nothing behind when the directory goes.
It marks the context as a preview too: a quarantine exists for no other reason.
One constraint the caller has to know: the only object store Command derives for itself is the one behind Spec.GitDir. A spec that targets another repository through Spec.Dir or Spec.WorkTree ALONE gets the quarantine but no alternate for its own objects, so under this context it could not read them -- where a work tree keeps its object store cannot be derived from a directory without guessing (a linked worktree, a bare repository and a redirected store all break the .git/objects join). No site reaches that combination today, and TestQuarantineLeavesADirOnlySpecUnableToReadItsOwnObjects pins it; a site that needs to must name GitDir alongside Dir.
#ObjectQuarantine
func ObjectQuarantine(ctx context.Context) (dir string, alternates []string, ok bool)ObjectQuarantine reports the context-carried object quarantine, if any.
#Command
func Command(ctx context.Context, s Spec) (*exec.Cmd, error)Command builds the git subprocess for one spec.
It errors when the argv names a subcommand the classification table does not declare, and when the spec's directory targeting is not backed by a declared exemption. It never starts the process; the caller owns that. A spec carries no single-authorship door, and cannot: the argv Command builds is one safegit constructs and starts ITSELF, and none of those may let git author a commit. The one declared door is on the effects-handle path (ArgvAny), because the command that has it -- the rebase passthrough -- runs there. A site that ever needs a door here has to add the field deliberately, which is the point.
#ArgvAny
func ArgvAny(exempt ExemptionID, door DoorID, args ...string) ([]interface{}, error)ArgvAny returns the full git argv -- binary, global prefix, then args -- as []interface{}, the shape the strictcli effects handle takes.
The invocation is not built by Command because safegit does not start it: the effects handle does, in the process working directory. exempt names the declared reason that is acceptable.
It takes no context, so the context-carried overrides Command applies -- the object quarantine among them -- cannot reach these invocations. That is not a hole, because no argv built here can write objects during a preview:
- An argv matching an allowlisted observe prefix EXECUTES even in dry mode, but ObservePrefixes admits only verbs the classification table declares observe-only unconditionally, which by definition write nothing. - Every other argv is RECORDED in dry mode and never started, whatever it names -- which is how repack and prune appear in a rewrite preview's would-do log without ever running.
Outside a preview there is no quarantine to reach in the first place: an executing run writes its objects into the repository on purpose.
door is the call site's declared permission to let git author a commit. Every site but the rebase passthrough passes NoDoor; see authoring.go.
#Effect.Has
func (e Effect) Has(want Effect) bool { return e&want == want }Has reports whether every effect in want is present in e.
#Effect.String
func (e Effect) String() stringString renders an effect set for diagnostics.
#Error.Error
func (e *Error) Error() string { return e.Msg }