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 ¶
- Variables
- func HooksDir(dataDir string) string
- func RepoDir(dataDir string, name domain.RepoName) string
- type Config
- type Git
- func (g *Git) Archive(ctx context.Context, repo domain.RepoName, commit domain.ObjectID, ...) error
- func (g *Git) BranchHead(ctx context.Context, repo domain.RepoName, branch string) (domain.ObjectID, error)
- func (g *Git) ChangedPaths(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, ...) ([]string, bool, error)
- func (g *Git) ChangedWorkflow(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, ...) (string, error)
- func (g *Git) CheckVersion(ctx context.Context) (Version, error)
- func (g *Git) Close(ctx context.Context) error
- func (g *Git) CommitTimes(ctx context.Context, repo domain.RepoName, head domain.ObjectID, ...) ([]time.Time, bool, error)
- func (g *Git) CommitTree(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (domain.ObjectID, error)
- func (g *Git) CommitsOffBranches(ctx context.Context, repo domain.RepoName, head domain.ObjectID, limit int) ([]domain.Commit, bool, error)
- func (g *Git) CountCommits(ctx context.Context, repo domain.RepoName, head domain.ObjectID) (int, error)
- func (g *Git) DefaultBranch(ctx context.Context, name domain.RepoName) (string, bool, error)
- func (g *Git) DeleteBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error
- func (g *Git) DiffCommits(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, ...) (domain.DiffStats, domain.Diff, error)
- func (g *Git) Discard(_ context.Context, id ports.StagedID) error
- func (g *Git) GetCommit(ctx context.Context, repo domain.RepoName, rev domain.Revision, ...) (domain.CommitDetail, error)
- func (g *Git) GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, ...) (domain.Tree, error)
- func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, error)
- func (g *Git) HasBranch(ctx context.Context, name domain.RepoName, branch string) (bool, error)
- func (g *Git) HasCommit(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (bool, error)
- func (g *Git) HeadBranch(_ context.Context, name domain.RepoName) (string, error)
- func (g *Git) IsAncestor(ctx context.Context, repo domain.RepoName, ...) (bool, error)
- func (g *Git) LatestTag(ctx context.Context, repo domain.RepoName) (domain.TagInfo, bool, error)
- func (g *Git) List(_ context.Context) (ports.RepoDirs, error)
- func (g *Git) ListCommits(ctx context.Context, repo domain.RepoName, q ports.CommitQuery) (ports.CommitPage, error)
- func (g *Git) ListPaths(ctx context.Context, repo domain.RepoName, rev domain.Revision, ...) (domain.PathList, error)
- func (g *Git) ListRange(ctx context.Context, repo domain.RepoName, exclude, head domain.ObjectID, ...) ([]domain.Commit, bool, error)
- func (g *Git) ListRefs(ctx context.Context, repo domain.RepoName) ([]domain.Ref, error)
- func (g *Git) Maintain(ctx context.Context, name domain.RepoName) error
- func (g *Git) MergeBase(ctx context.Context, repo domain.RepoName, a, b domain.ObjectID) (domain.ObjectID, error)
- func (g *Git) OnTrash(fn func(dir string))
- func (g *Git) OpenBlob(ctx context.Context, repo domain.RepoName, rev domain.Revision, ...) (domain.BlobInfo, io.ReadCloser, error)
- func (g *Git) Publish(ctx context.Context, id ports.StagedID, name domain.RepoName) error
- func (g *Git) PullRef(ctx context.Context, repo domain.RepoName, ref string) (domain.ObjectID, error)
- func (g *Git) PurgeStaging(ctx context.Context) error
- func (g *Git) ReadCommitObjects(ctx context.Context, repo domain.RepoName, ids []domain.ObjectID) (map[domain.ObjectID][]byte, error)
- func (g *Git) ResolveRefPath(ctx context.Context, repo domain.RepoName, path string) (domain.RefPath, error)
- func (g *Git) ResolveRevision(ctx context.Context, repo domain.RepoName, rev domain.Revision) (domain.ResolvedRevision, error)
- func (g *Git) SetDefaultBranch(ctx context.Context, name domain.RepoName, branch string) error
- func (g *Git) SetPullRef(ctx context.Context, repo domain.RepoName, ref string, id domain.ObjectID) error
- func (g *Git) Stage(ctx context.Context) (ports.StagedID, error)
- func (g *Git) TestMerge(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID) (domain.ObjectID, bool, error)
- func (g *Git) Trash(ctx context.Context, name domain.RepoName) error
- func (g *Git) UpdateBranch(ctx context.Context, repo domain.RepoName, branch string, ...) error
- func (g *Git) WriteCommit(ctx context.Context, repo domain.RepoName, object []byte) (domain.ObjectID, error)
- type Version
Constants ¶
This section is empty.
Variables ¶
var ErrClosed = errors.New("gitcli: reader is closed")
ErrClosed is returned by reads after Close.
var MinVersion = Version{Major: 2, Minor: 45}
MinVersion is the oldest supported git.
Functions ¶
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 (*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):
- 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.
- 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 ¶
CheckVersion runs `git version` and fails if git is missing, unreadable or older than MinVersion. It returns the version found.
func (*Git) Close ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
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
LatestTag runs `git for-each-ref --sort=-creatordate --count=1` on the tags.
func (*Git) List ¶
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 ¶
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) 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
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 ¶
func (g *Git) OpenBlob(ctx context.Context, repo domain.RepoName, rev domain.Revision, p domain.TreePath) (domain.BlobInfo, io.ReadCloser, error)
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 ¶
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 ¶
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 ¶
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 ¶
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) 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.
type Version ¶
type Version struct{ Major, Minor, Patch int }
Version is a git release number.
func ParseVersion ¶
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".