gitcli

package
v0.27.0 Latest Latest
Warning

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

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

Documentation

Overview

Package gitcli implements the Git ports over the git binary. Every invocation runs in an isolated environment that is built here and never inherits the caller's.

Index

Constants

This section is empty.

Variables

View Source
var ErrClosed = errors.New("gitcli: reader is closed")

ErrClosed is returned by reads after Close.

View Source
var MinVersion = Version{Major: 2, Minor: 45}

MinVersion is the oldest supported git.

Functions

func HooksDir

func HooksDir(dataDir string) string

HooksDir is the directory, below the data root, that holds the hook scripts every receive-pack runs.

func RepoDir added in v0.3.0

func RepoDir(dataDir string, name domain.RepoName) string

RepoDir returns the directory of the repository, repos/<name>.git below the data directory. It does not check that it exists.

Types

type Config

type Config struct {
	// Bin is the git executable: a name looked up in the process PATH, or a path.
	// It is required; the default belongs to the configuration.
	Bin string
	// DataDir is the data root (absolute). repos/, repos/.staging/ and
	// trash/ below it must exist; run/git-home/ is the HOME of git.
	DataDir string
	// Path is the PATH given to git, which it uses to find its helpers.
	Path string
	// MaxReceiveBytes is the largest pack a push may send
	// (receive.maxInputSize); zero leaves Git unlimited.
	MaxReceiveBytes int64
	// CatFileIdle is how long a repository's long-lived `git cat-file`
	// process may stay unused before it is stopped. Zero means 60 seconds.
	CatFileIdle time.Duration
	// MaxCatFiles bounds the long-lived cat-file processes. Zero means 16.
	MaxCatFiles int
	// MaxExec bounds the one-shot git processes that run at once (every
	// command but the long-lived cat-file processes and the blob streams);
	// callers beyond it wait for a slot until their context ends. Zero means
	// max(4, 2*GOMAXPROCS).
	MaxExec int
	// MaxBlobStreams bounds the open blob streams, each of which holds a git
	// process until its reader is closed. Streams beyond it are refused with
	// domain.ErrResourceExhausted rather than queued: a slow reader could hold
	// a slot for long. Zero means 32.
	MaxBlobStreams int
}

Config configures a Git adapter. The adapter never reads the process environment: the caller passes what it needs.

type Git

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

Git is the git CLI adapter for ports.GitRepos.

func New

func New(cfg Config) *Git

New builds an adapter. It does not run git: call CheckVersion.

func (*Git) Archive added in v0.9.0

func (g *Git) Archive(ctx context.Context, repo domain.RepoName, commit domain.ObjectID, prefix string, w io.Writer) error

Archive streams the tree of the commit as a gzip-compressed tar, every path below prefix ("<prefix>/..."), with `git archive`. The tar is compressed here, not by a gzip binary. A client that goes away ends ctx, which kills git. Nothing is written to w when the commit does not exist (domain.ErrNotFound).

func (*Git) BranchHead added in v0.2.0

func (g *Git) BranchHead(ctx context.Context, repo domain.RepoName, branch string) (domain.ObjectID, error)

BranchHead resolves refs/heads/<branch>^{commit} with `cat-file`.

func (*Git) ChangedPaths added in v0.7.0

func (g *Git) ChangedPaths(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, limit int) ([]string, bool, error)

ChangedPaths runs `git diff-tree -r --name-only -z` from base to head, with renames not detected (a rename lists both paths, as GitHub's push filters see it), and stops reading after max paths: truncated tells there were more. A zero base diffs against the empty tree.

func (*Git) ChangedWorkflow added in v0.12.0

func (g *Git) ChangedWorkflow(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, objects ports.PushObjects) (string, error)

ChangedWorkflow tells whether a push changes the workflows of a repository. It returns the path of the first change, "" for none. Deleting a ref changes nothing, and a workflows directory that disappears is not a change.

Two things are judged, as GitHub does for the token of a job (it refuses a push when the commits pushed change workflow files, not when a new branch is cut from a commit that has them):

  1. Every commit the push brings (`rev-list <new> --not --all`, so the commits no ref has yet) must leave .github/workflows as it found it: it is compared with each of its parents (a root commit with nothing) and is a change when it is present and differs from every one of them. A merge of existing commits is thus no change; a side that brings a change is itself a new commit, and judged as one.
  2. The directory a ref ends with must not be one it did not have: for an update the final .github/workflows must be the old one or absent, for a new ref the one of the default branch (HEAD) or absent. This keeps a job token from moving a ref onto existing commits that carry other workflows (an older commit, a commit of a branch with different ones).

Presence is the directory the workflow loader lists (CI.workflowFiles, on both git readers): .github and workflows both entries of mode 040000 and type tree. A path through a gitlink (160000), a symbolic link (120000, which the loader does not follow) or a file is no workflows, so the same tree id that is no workflows as a gitlink and becomes workflows as a directory is a change, which the object id alone would not tell. Mode and id of "<commit>:.github/workflows" are what is compared: equal means the same content, whatever it holds, so nothing is read.

Nothing is walked recursively: the trees of a push are the pusher's, and a recursive diff of trees that share subtrees fans out into more paths than the push has bytes (git builds the whole diff before it prints a line, so reading it as a stream would not help). The shallow diff that names the path reads one directory, as large as its tree object, as a stream, up to its first name, and git is stopped and reaped then; a name longer than maxWorkflowPath fails closed.

The ends of each update are peeled first (`cat-file --batch-check` on `<oid>^{}`): a commit or a tree is compared, a blob (a tag may point to one) holds no workflows; a missing or malformed object fails closed. Git reads the objects of a push still in quarantine through the environment of the hook (ports.PushObjects), accepted only inside the repository's objects directory.

func (*Git) CheckVersion

func (g *Git) CheckVersion(ctx context.Context) (Version, error)

CheckVersion runs `git version` and fails if git is missing, unreadable or older than MinVersion. It returns the version found.

func (*Git) Close

func (g *Git) Close(ctx context.Context) error

Close stops the long-lived git processes of the read side and waits for them, until ctx ends. Reads after Close fail with ErrClosed. Close is idempotent; the adapter's write side keeps working.

func (*Git) CommitTimes added in v0.24.0

func (g *Git) CommitTimes(ctx context.Context, repo domain.RepoName, head domain.ObjectID, since time.Time) ([]time.Time, bool, error)

CommitTimes runs `git rev-list --since-as-filter --format=%ct`. The filter looks at every reachable commit: --since stops the walk at the first older one, and misses newer commits behind it when dates are out of order.

func (*Git) CommitTree added in v0.2.0

func (g *Git) CommitTree(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (domain.ObjectID, error)

CommitTree runs `git rev-parse <commit>^{tree}`.

func (*Git) CommitsOffBranches added in v0.8.1

func (g *Git) CommitsOffBranches(ctx context.Context, repo domain.RepoName, head domain.ObjectID, limit int) ([]domain.Commit, bool, error)

CommitsOffBranches lists the commits reachable from head and from no branch (refs/heads/*), newest first, at most limit. It runs `git log --not --branches --not <head>`: tags do not count, so the commit of another tag that no branch has is listed.

func (*Git) CountCommits added in v0.23.0

func (g *Git) CountCommits(ctx context.Context, repo domain.RepoName, head domain.ObjectID) (int, error)

CountCommits runs `git rev-list --count`, from the commit graph when the repository has one.

func (*Git) DefaultBranch

func (g *Git) DefaultBranch(ctx context.Context, name domain.RepoName) (string, bool, error)

DefaultBranch reads HEAD and checks the branch with `git for-each-ref`.

func (*Git) DeleteBranch added in v0.4.0

func (g *Git) DeleteBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error

DeleteBranch runs `git update-ref -d refs/heads/<branch> <old>`, which takes the ref lock and refuses when the ref is not at old. A failure is told from a moved or deleted branch by reading the branch again.

func (*Git) DiffCommits added in v0.2.0

func (g *Git) DiffCommits(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error)

DiffCommits reads the diff between two commits like GetCommit does.

func (*Git) Discard

func (g *Git) Discard(_ context.Context, id ports.StagedID) error

Discard removes a staged repository.

func (*Git) GetCommit

func (g *Git) GetCommit(ctx context.Context, repo domain.RepoName, rev domain.Revision, limits domain.DiffLimits) (domain.CommitDetail, error)

GetCommit returns a commit with its diff against its first parent (the empty tree for a root commit), read in one pass of

git diff-tree -r -M -z --raw --numstat -p <parent> <commit>

which prints the changed files, then their line counts, then the patch. Statistics come from the line counts, binary files from their "-" counts. The patch is parsed as it streams: nothing beyond the limits is kept, and git is stopped once nothing more is needed.

func (*Git) GetTree

func (g *Git) GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLast bool) (domain.Tree, error)

GetTree lists a directory with `git ls-tree -z -l`.

func (*Git) HTTPBackend

func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, error)

HTTPBackend resolves git-http-backend through `git --exec-path` of the configured git (once), and builds its environment: the isolated one, GIT_PROJECT_ROOT=<data>/repos, GIT_HTTP_EXPORT_ALL and the configuration passed by GIT_CONFIG_COUNT/KEY/VALUE, so that nothing is written to the repositories:

core.hooksPath=<data>/run/hooks
receive.fsckObjects=true
uploadpack.allowFilter=true
pack.threads=1
pack.windowMemory=256m
http.receivepack=true            (push only)
receive.hideRefs=refs/pull/      (push only: ermite writes them, git refuses updates)
receive.maxInputSize=<max>       (push only, when configured)

A push also gets REMOTE_USER=owner.

func (*Git) HasBranch

func (g *Git) HasBranch(ctx context.Context, name domain.RepoName, branch string) (bool, error)

HasBranch checks refs/heads/<branch> with `git for-each-ref` after `git check-ref-format`, which rejects the glob characters for-each-ref would otherwise interpret.

func (*Git) HasCommit added in v0.2.0

func (g *Git) HasCommit(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (bool, error)

HasCommit asks `cat-file` for the type of the object.

func (*Git) HeadBranch

func (g *Git) HeadBranch(_ context.Context, name domain.RepoName) (string, error)

HeadBranch reads the HEAD file of the bare repository (files ref backend). It is empty when HEAD is detached.

func (*Git) IsAncestor added in v0.2.0

func (g *Git) IsAncestor(ctx context.Context, repo domain.RepoName, ancestor, descendant domain.ObjectID) (bool, error)

IsAncestor runs `git merge-base --is-ancestor`, which exits 1 for "no".

func (*Git) LatestTag added in v0.23.0

func (g *Git) LatestTag(ctx context.Context, repo domain.RepoName) (domain.TagInfo, bool, error)

LatestTag runs `git for-each-ref --sort=-creatordate --count=1` on the tags.

func (*Git) List

func (g *Git) List(_ context.Context) (ports.RepoDirs, error)

List returns the repository directories. Entries that are not "<valid name>.git" directories are reported as invalid; .staging is not an entry.

func (*Git) ListCommits

func (g *Git) ListCommits(ctx context.Context, repo domain.RepoName, q ports.CommitQuery) (ports.CommitPage, error)

ListCommits pages through `git log`, which stops after Skip+Limit+1 commits.

func (*Git) ListPaths added in v0.6.0

func (g *Git) ListPaths(ctx context.Context, repo domain.RepoName, rev domain.Revision, maxPaths, maxBytes int) (domain.PathList, error)

ListPaths lists the files and symbolic links of a revision with `git ls-tree -r -z`, in its order, up to the bounds of the port.

func (*Git) ListRange added in v0.2.0

func (g *Git) ListRange(ctx context.Context, repo domain.RepoName, exclude, head domain.ObjectID, limit int) ([]domain.Commit, bool, error)

ListRange runs `git log <head> ^<exclude>`.

func (*Git) ListRefs

func (g *Git) ListRefs(ctx context.Context, repo domain.RepoName) ([]domain.Ref, error)

ListRefs lists HEAD, the branches and the tags with `git for-each-ref`. An annotated tag is followed to its commit; a tag of a tag is followed with one `cat-file` request each.

func (*Git) Maintain

func (g *Git) Maintain(ctx context.Context, name domain.RepoName) error

Maintain runs `git commit-graph write --reachable --changed-paths --split`.

func (*Git) MergeBase added in v0.2.0

func (g *Git) MergeBase(ctx context.Context, repo domain.RepoName, a, b domain.ObjectID) (domain.ObjectID, error)

MergeBase runs `git merge-base`, which exits 1 when there is none.

func (*Git) OnTrash added in v0.3.0

func (g *Git) OnTrash(fn func(dir string))

OnTrash registers fn, which Trash calls with the directory the repository had, after it moved it away. Another reader of the repository (the ermitegit adapter) uses it to drop what it holds open. Call it once while wiring, before the adapter serves.

func (*Git) OpenBlob

OpenBlob describes the file at path and returns its content as a stream.

A blob up to 256 KiB is read through the repository's long-lived cat-file process. A larger one gets a `git cat-file blob` process that lives until the reader is closed, or ctx ends, or the adapter is closed; the memory used is a few buffers, whatever the size. The reader fails with io.ErrUnexpectedEOF if git stops before the announced size.

func (*Git) Publish

func (g *Git) Publish(ctx context.Context, id ports.StagedID, name domain.RepoName) error

Publish renames the staged repository to repos/<name>.git without replacing anything.

func (*Git) PullRef added in v0.14.0

func (g *Git) PullRef(ctx context.Context, repo domain.RepoName, ref string) (domain.ObjectID, error)

PullRef reads a ref that domain.IsPullRef accepts with `git rev-parse`.

func (*Git) PurgeStaging

func (g *Git) PurgeStaging(ctx context.Context) error

PurgeStaging removes everything under repos/.staging/.

func (*Git) ReadCommitObjects added in v0.26.0

func (g *Git) ReadCommitObjects(ctx context.Context, repo domain.RepoName, ids []domain.ObjectID) (map[domain.ObjectID][]byte, error)

ReadCommitObjects reads the raw content of commits with the cat-file pool: one `info` per id, then `contents` for the commits of acceptable size. Ids that are missing, are not commits, or are larger than domain.MaxCommitObjectBytes are left out. The read ends at the first commit that would take the total over domain.MaxCommitObjectsBytes (known from `info`, before any content is read, so the stream stays consistent).

func (*Git) ResolveRefPath

func (g *Git) ResolveRefPath(ctx context.Context, repo domain.RepoName, path string) (domain.RefPath, error)

ResolveRefPath tries the leading segments of path as a revision (branch, tag, full ref name, HEAD; a single segment may also be a commit id), longest first, with up to one `cat-file` request per candidate on the repository's process. At most domain.MaxRefPathTries segments are tried.

func (*Git) ResolveRevision

func (g *Git) ResolveRevision(ctx context.Context, repo domain.RepoName, rev domain.Revision) (domain.ResolvedRevision, error)

ResolveRevision resolves a revision with `cat-file`.

func (*Git) SetDefaultBranch

func (g *Git) SetDefaultBranch(ctx context.Context, name domain.RepoName, branch string) error

SetDefaultBranch runs `git symbolic-ref HEAD refs/heads/<branch>`.

func (*Git) SetPullRef added in v0.14.0

func (g *Git) SetPullRef(ctx context.Context, repo domain.RepoName, ref string, id domain.ObjectID) error

SetPullRef runs `git update-ref` on a ref that domain.IsPullRef accepts, or `update-ref -d` for a zero id (deleting a ref that is not there is fine).

func (*Git) Stage

func (g *Git) Stage(ctx context.Context) (ports.StagedID, error)

Stage runs `git init --bare` in a new directory under repos/.staging/ and sets the repository defaults. The initial branch is main.

func (*Git) TestMerge added in v0.14.0

func (g *Git) TestMerge(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID) (domain.ObjectID, bool, error)

TestMerge runs `git merge-tree --write-tree`, which merges in the object database and touches no ref or index: it prints the tree of the result and exits 0, or exits 1 when the merge conflicts. Commits with no common history do not merge (GitHub makes no test merge for them either).

func (*Git) Trash

func (g *Git) Trash(ctx context.Context, name domain.RepoName) error

Trash moves repos/<name>.git to trash/<name>-<unix>.git.

func (*Git) UpdateBranch added in v0.2.0

func (g *Git) UpdateBranch(ctx context.Context, repo domain.RepoName, branch string, old, newID domain.ObjectID) error

UpdateBranch runs `git update-ref refs/heads/<branch> <new> <old>`, which takes the ref lock and refuses when the ref is not at old. A failure is told from a moved branch by reading the branch again.

func (*Git) WriteCommit added in v0.2.0

func (g *Git) WriteCommit(ctx context.Context, repo domain.RepoName, object []byte) (domain.ObjectID, error)

WriteCommit runs `git hash-object -t commit -w --stdin`, which also checks that the object is a well-formed commit.

type Version

type Version struct{ Major, Minor, Patch int }

Version is a git release number.

func ParseVersion

func ParseVersion(out string) (Version, error)

ParseVersion parses the output of `git version`, such as "git version 2.45.0", "git version 2.39.5 (Apple Git-154)" or "git version 2.46.0.rc1".

func (Version) Less

func (v Version) Less(o Version) bool

Less reports whether v is older than o.

func (Version) String

func (v Version) String() string

String returns "major.minor.patch".

Jump to

Keyboard shortcuts

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