Help us shape the future of Go! Take the 2026 Go Developer Survey

gitexec

package
v0.1.23 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: AGPL-3.0 Imports: 12 Imported by: 0

Documentation

Overview

Package gitexec wraps the system git binary. gitdr shells out to real git for faithful clone/bundle semantics (and later git-lfs). Auth is injected via GIT_CONFIG_* env, scoped to the clone host, so tokens never reach argv, and git gets no other part of gitdr's environment than what passedThrough names.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func LFSAvailable

func LFSAvailable() bool

LFSAvailable reports whether the git-lfs binary is installed.

func Withheld added in v0.1.21

func Withheld() []string

Withheld names the variables in gitdr's environment that would change what git does and that git does not get: every GIT_* variable passedThrough leaves out, XDG_CONFIG_HOME and SSH_ASKPASS, which git reads, and LD_PRELOAD and LD_LIBRARY_PATH, which change how it runs. Up to v0.1.20 git got all of them, so a run that relied on one now behaves differently, and this is how it gets to say so. Names only, sorted: a value can carry a credential, as GIT_CONFIG_PARAMETERS's can.

Not the locale. Nearly every environment sets one, and all it changes is the language of git's messages. Nor the variables gitdr sets on git itself (commandEnv), whose own values git gets whatever the environment says, as it did before: the image sets GIT_TERMINAL_PROMPT=0, and a warning about it on every run would teach operators to ignore the warning.

Types

type BundleRef added in v0.1.13

type BundleRef struct {
	// Name is a full ref name such as "refs/heads/main" or "refs/tags/v1", or the literal
	// "HEAD". A bundle BundleAll writes carries a HEAD entry when HEAD resolves where it was
	// written, and it is not a ref any repository stores under refs/; callers normalise it.
	Name string
	// OID is the object the bundle declares for Name, exactly as recorded. For an annotated
	// tag this is the tag object, not the commit it peels to.
	OID string
}

BundleRef is one entry from a bundle's header: a name the bundle declares and the object it points at.

type Git

type Git struct {
	// contains filtered or unexported fields
}

Git runs git subcommands.

func New

func New(logger *slog.Logger) *Git

New returns a Git runner. A nil logger falls back to slog.Default().

func (*Git) BundleAll

func (g *Git) BundleAll(ctx context.Context, repoDir, bundlePath string) error

BundleAll bundles every ref in repoDir, and HEAD when it resolves there, so `git clone <bundle>` checks out the default branch on restore.

HEAD only when it resolves. Changed in v0.1.23. In a mirror whose HEAD names a branch that does not exist, a default branch renamed or deleted, or an empty default beside other branches, git finds no revision called HEAD and, run in the mirror, takes it for the path of the mirror's own HEAD file. No commit touches that path, so every branch was left out: the bundle held the annotated tags alone, or nothing, which git refuses to write. Such a repository failed every night, and up to v0.1.22 the tags-alone bundle was stored as a good copy. Without HEAD the bundle says what the source says, that there is no default branch: it declares every ref and no HEAD, which is what the copy records of the mirror (MirrorRefs), and a clone of it checks no branch out. Restore and drill take it as it is (SPEC §7).

The "--" ends the revisions, so nothing here is ever read as a path: a HEAD that stopped resolving after the check would fail the bundle as a bad revision instead of narrowing it.

func (*Git) BundleHeads added in v0.1.13

func (g *Git) BundleHeads(ctx context.Context, bundlePath string) ([]BundleRef, error)

BundleHeads returns the ref-to-object map the bundle itself declares.

This is the other half of the restore proof. The signed manifest fixes the bundle's bytes; this says which refs those bytes claim to carry. Because git is content-addressed, a commit id transitively covers its tree, its blobs and its whole ancestry, so comparing these against the refs a restored repository actually has is not a sample of the history, it is exact equality of it.

func (*Git) BundleVerify

func (g *Git) BundleVerify(ctx context.Context, bundlePath string) error

BundleVerify runs `git bundle verify bundlePath` from a scratch repository.

It reads the bundle's header — format, prerequisites, refs — and stops, so it is a structural check and not an integrity one. Integrity is the SHA-256 the restore path compares against the sidecar and the signed manifest before git is asked anything.

func (*Git) CloneFromBundle

func (g *Git) CloneFromBundle(ctx context.Context, bundlePath, dir string) error

CloneFromBundle restores a repo by cloning from a bundle file.

--origin is pinned rather than left to default. `clone.defaultRemoteName` in an operator's ~/.gitconfig renames the remote, and with it the whole refs/remotes/<name>/* namespace that a clone files every branch under. That would make the shape of a restore depend on the machine it ran on, and the ref comparison — which looks for a declared branch under refs/heads/X or refs/remotes/origin/X — report a perfectly good restore as missing every branch but one. Same reasoning as the local LFS filters: in a disaster the machine is new, and a restore must not depend on how it happens to be configured.

func (*Git) CloneMirror

func (g *Git) CloneMirror(ctx context.Context, repoURL, dir string, opts Options) error

CloneMirror runs `git clone --mirror url dir`.

func (*Git) HasRefs added in v0.1.7

func (g *Git) HasRefs(ctx context.Context, repoDir string) (bool, error)

HasRefs reports whether repoDir contains at least one ref.

A repository created and never pushed to has none, and `git bundle create` refuses to write an empty bundle: "fatal: Refusing to create empty bundle." Without this check that refusal reads as a failed repository, and one unused project in an organisation is enough to make every backup of it fail for ever.

Asked of the local mirror rather than the provider's API, because it is the state that actually matters: a clone that produced no refs is what bundling has to cope with, whatever the API said. `git clone --mirror` fails loudly on a partial fetch, so zero refs after a successful clone means the remote genuinely has none.

func (*Git) HeadOID added in v0.1.13

func (g *Git) HeadOID(ctx context.Context, repoDir string) (string, error)

HeadOID resolves repoDir's HEAD to the object it points at. It fails on a HEAD that does not resolve, which a clone of a bundle has when the bundle declares no HEAD and no branch was checked out (BundleAll); HeadResolves tells that apart from a failure.

func (*Git) HeadResolves added in v0.1.23

func (g *Git) HeadResolves(ctx context.Context, repoDir string) (bool, error)

HeadResolves reports whether repoDir's HEAD resolves to an object. It does not in a repository with no commits, in one whose HEAD names a branch that does not exist, and in a clone of a bundle that declares no HEAD, which checks no branch out.

func (*Git) LFSCheckout

func (g *Git) LFSCheckout(ctx context.Context, repoDir string) error

LFSCheckout materializes LFS files in the working tree from local objects (no network).

func (*Git) LFSFetchAll

func (g *Git) LFSFetchAll(ctx context.Context, repoDir, repoURL string, opts Options) error

LFSFetchAll downloads all LFS objects referenced by any ref into repoDir, reusing the clone's host-scoped auth.

func (*Git) LFSInstallLocal added in v0.1.5

func (g *Git) LFSInstallLocal(ctx context.Context, repoDir string) error

LFSInstallLocal writes the lfs clean/smudge filters into repoDir's own config.

A repository cloned from a bundle has no filter.lfs.* configuration, and without it "git lfs checkout" exits 0 and does nothing — the working tree keeps 130-byte pointer files. That makes a successful restore depend on whether the operator had already run "git lfs install" on the machine, which in a disaster is exactly the machine that is new.

--skip-repo, because a restored copy needs the filters, not the pre-push hook.

func (*Git) ListRefs added in v0.1.13

func (g *Git) ListRefs(ctx context.Context, repoDir string) (map[string]string, error)

ListRefs returns every ref in repoDir mapped to the object it points at.

%(objectname) is the ref's own target and not the peeled one. A bundle header records an annotated tag as its tag object, so the two compare directly; peeling here (%(*objectname)) would compare a tag against the commit under it and accept a repository whose tag object had been replaced with a different one over the same commit.

func (*Git) LsRemote added in v0.1.13

func (g *Git) LsRemote(ctx context.Context, repoURL string, opts Options) (map[string]string, error)

LsRemote asks the remote what refs it has, without cloning anything.

This is the cheap half of not rewriting a repository that has not changed. One network round trip, no objects transferred, no working tree, and the answer is the same shape ListRefs returns for a local repository.

`--symref` is deliberately absent: the symbolic target of HEAD is a property of the remote's configuration and not of its history, so a repository whose default branch was renamed with no commits since has not changed in any sense that matters to a backup, and including it would force a full rewrite of every repository in an organisation the day somebody renames master to main.

The peeled `^{}` lines are dropped for the reason ListRefs does not peel: an annotated tag compares as its tag object, so a tag object swapped over the same commit is a change.

func (*Git) MirrorRefs added in v0.1.23

func (g *Git) MirrorRefs(ctx context.Context, repoDir string) (map[string]string, error)

MirrorRefs reads back the refs a clone received: every ref in repoDir, and HEAD when it resolves. That is the shape LsRemote returns for the remote the clone was made from, so the two compare directly, and what a backup records of a copy is what the next run's listing of the source is compared with.

HEAD is read as the object it resolves to, as ls-remote reports it, and left out when it names a branch that does not exist, as ls-remote leaves it out. A repository with no commits reads as no refs at all rather than as an error. BundleAll leaves HEAD out of the bundle on the same condition, so the bundle declares exactly this.

type Options

type Options struct {
	// AuthHeader, if set, is sent as an HTTP Authorization header (e.g.
	// "Authorization: Basic ...") scoped to the clone host, via env not argv.
	AuthHeader string
}

Options configures a git invocation.

Jump to

Keyboard shortcuts

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