Documentation
¶
Overview ¶
Package git reads what a repository says about a project's working copy, and writes it in a deliberately short list of places: Commit, Push (plain, or force-with-lease, setting the upstream where the branch has none), Fetch, the fast forward Pull, the two branch moves Checkout and CreateBranch, Tag and the PushTag that sends one tag, Clone into a directory that holds nothing yet, and Revert, which takes one path back to HEAD. Staging, stashing, merging and everything else that rewrites a repository stays with a coder or the command line, and a refused write leaves the working copy as it was.
Every call goes through run, which is the one place the safety rules live, because a status poll runs next to a coder that may be committing right now:
- GIT_OPTIONAL_LOCKS=0 on every single call, so a read never takes the index.lock away from that coder.
- no shell anywhere, the process is started with an argument list.
- "--" before any path, so a file named like a flag stays a file.
- core.quotepath=false and -z where git offers it, so paths arrive as bytes instead of escapes.
- a timeout on every process and a cap on how much output is kept, and the timeout ends the whole process group with a bounded pipe wait (WaitDelay), so a helper git leaves behind — a credential helper waiting for input, a signer's pinentry — can neither survive it nor hold the answer open.
- every prompt fails in seconds instead of waiting for that timeout: GIT_TERMINAL_PROMPT=0 for git's own questions, and for ssh's an askpass forced to /bin/false, which turns a passphrase question into an immediate denial while the host's choice of ssh (core.sshCommand, GIT_SSH_COMMAND) stays untouched and agent keys keep working.
- GIT_ALLOW_PROTOCOL names the transports a URL may use, because the dangerous ones are schemes and not options: ext:: runs the command in the URL, and no "--" in front of it changes that.
A directory that is not a repository is not an error: the calls answer "no repo" and the editor keeps looking exactly like it does without git.
Index ¶
- Constants
- Variables
- func CheckProxyArgs(args []string) error
- func Subcommand(args []string) string
- type Blame
- type BranchInfo
- type Changes
- type Commit
- type CommitInfo
- type CommitResult
- type ExecResult
- type FileStatus
- type Fingerprint
- type LogPage
- type NewWorktree
- type Prompt
- type Ref
- type RefMatches
- type RefSearch
- type Repo
- func (r *Repo) AddWorktree(ctx context.Context, w NewWorktree) error
- func (r *Repo) Blame(ctx context.Context, file string) (Blame, error)
- func (r *Repo) Changes(ctx context.Context) (Changes, error)
- func (r *Repo) Checkout(ctx context.Context, name string) error
- func (r *Repo) Clone(ctx context.Context, url string) error
- func (r *Repo) Commit(ctx context.Context, message string, paths []string, amend bool) (CommitResult, error)
- func (r *Repo) CommitInfo(ctx context.Context) (CommitInfo, error)
- func (r *Repo) CreateBranch(ctx context.Context, name string) error
- func (r *Repo) DeleteRemoteTag(ctx context.Context, name string) error
- func (r *Repo) DeleteTag(ctx context.Context, name string) error
- func (r *Repo) Exec(ctx context.Context, args []string) (ExecResult, error)
- func (r *Repo) Fetch(ctx context.Context) (bool, error)
- func (r *Repo) FetchIfStale(ctx context.Context, maxAge time.Duration) (bool, error)
- func (r *Repo) FileAt(ctx context.Context, rev, file string) ([]byte, bool, error)
- func (r *Repo) Fingerprint(ctx context.Context) (Fingerprint, bool)
- func (r *Repo) Log(ctx context.Context, file string, skip, limit int) (LogPage, error)
- func (r *Repo) Pull(ctx context.Context) error
- func (r *Repo) Push(ctx context.Context, force bool) error
- func (r *Repo) PushTag(ctx context.Context, name string) error
- func (r *Repo) Refs(ctx context.Context, search RefSearch) (RefMatches, error)
- func (r *Repo) Revert(ctx context.Context, path string) error
- func (r *Repo) Tag(ctx context.Context, name, rev, message string) error
- func (r *Repo) WithPrompt(p *Prompt) *Repo
- func (r *Repo) WorkingCopy(ctx context.Context) (string, bool, error)
- func (r *Repo) Worktrees(ctx context.Context) ([]Worktree, error)
- type Worktree
- type WorktreeChange
Constants ¶
const ( KindBranch = "branch" KindRemote = "remote" KindTag = "tag" KindCommit = "commit" )
The kinds a picker may ask for. The first three are names and come out of for-each-ref; the fourth is the history and comes out of git log, which is why it answers commits and not refs.
const DefaultTimeout = 5 * time.Second
DefaultTimeout caps one git process. Status on a normal repository answers in milliseconds; this is the ceiling for a repository on a slow or stalled disk, after which the caller gets an error instead of a hanging request.
const MaxOutput = 8 << 20
MaxOutput caps what one call keeps in memory. A repository with an implausible number of changed files truncates instead of filling the heap. It is exported because truncation is silent, and a caller that needs a complete answer, reading a whole file at a revision, can only know it by the size: an answer that reaches this may be the head of a larger one.
Variables ¶
var ErrNoAnswer = errors.New("no answer")
ErrNoAnswer marks a call that produced no result at all: the process could not be started, the deadline ended it, or the caller dropped it. That is not the same as git having decided something, which is what an exit code is, and the difference matters exactly once: a directory that answers "not a repository" said so, while a call that never ran says nothing about the directory. Everything that only reports is free to read both as "no repo"; Fingerprint is the one caller that may not, because publishing a move nobody made costs every open editor a round.
var ErrRevision = errors.New("The revision is not known to this repository.")
ErrRevision says the revision the caller named cannot be resolved, which is a wrong name and not a failing repository: the handler answers it as the caller's mistake, not as a bad gateway.
Functions ¶
func CheckProxyArgs ¶
CheckProxyArgs is the one rule a proxied command line has to keep: the caller's arguments may describe an operation, and they may never name a program for this server to run.
It holds through one shape. The git subcommand comes first, its own options behind it, and the options of git itself, everything that stands in front of a subcommand, are not proxied at all. That list is where the whole danger sits and none of it is needed here: `-c core.sshCommand=…` and `-c credential.helper=…` point git at a program of the caller's choosing, which then inherits the askpass environment and can ask the person in the browser for the passphrase itself; `--exec-path` moves where git looks for its own subcommands; `-C` and `--git-dir` move the call out of the working copy the dialog names. Refusing the position instead of the option names is what makes that complete: every one of them is only valid in front of the subcommand, so a first argument that is a plain subcommand word leaves none of them a place to stand.
Behind the subcommand it names programOptions, the transport's own program on the operations this proxy is for, and it names nothing else: what a subcommand does is the subcommand's, and a list trying to enumerate every git option that ends in a command would be a list that is never complete.
That is the honest bound of this function. It is what the cockpit itself accepts, and it is no wall between the cockpit and a coder that means harm: a coder runs under the same user account as this server, so it can read the bridge token out of the git child's environment in /proc and ask the browser whatever it likes, and a repository it can write carries hooks that run on push. The user account is the trust boundary here. What this whole path is for is that the passphrase never travels into a coder session, and what this function keeps is that the caller's arguments cannot rename the action the dialog shows or point the call at a program of their own.
func Subcommand ¶
Subcommand is the word a proxied command line is about: its first argument when that is a subcommand, git itself otherwise. The failure sentences carry it, and the proxy's route names its askpass action with it, so the dialog says "push" and not the whole line.
It reads the first argument and nothing behind it. Walking past an option to find a word further back cannot tell an option from an option's value, so `-c core.sshCommand=/tmp/x push` would name the action after the value: that word is what the dialog shows above ssh's line as this server's own truth, the one line there that is not the caller's, and it may not be a string the caller chose. CheckProxyArgs is what keeps a proxied line in that shape; this only falls back for a line that never went through it.
Types ¶
type Blame ¶
type Blame struct {
Repo bool `json:"repo"`
Path string `json:"path"`
Commits []Commit `json:"commits"`
Lines []int `json:"lines"`
// Large marks a file whose blame outgrew what one call keeps in memory
// (MaxOutput). The lines are empty then, because half a blame is worse
// than none: the head of the file would carry its commits and everything
// past the cut would read like a part nobody ever touched.
Large bool `json:"large,omitempty"`
}
Blame is who last touched each line of a file. The commits are listed once and Lines carries an index into that list per line, in order: a file of a few thousand lines usually comes from a handful of commits, and repeating the whole entry per line would be the same answer over and over.
type BranchInfo ¶
type BranchInfo struct {
Name string `json:"name"`
Detached bool `json:"detached,omitempty"`
Upstream string `json:"upstream,omitempty"`
Ahead int `json:"ahead"`
Behind int `json:"behind"`
Counted bool `json:"counted,omitempty"`
}
BranchInfo is what the status headers say about where HEAD stands: the branch, its upstream, and how far the two have drifted apart. Counted says whether git could count at all, which it cannot without an upstream or with an upstream whose ref is gone. A detached HEAD has no branch name and carries its abbreviated commit instead.
type Changes ¶
type Changes struct {
Repo bool `json:"repo"`
Branch BranchInfo `json:"branch"`
Worktree []WorktreeChange `json:"worktree"`
}
Changes is what the working copy carries on top of HEAD, one entry per changed path, which is what feeds the marks in the editor's file tree. The branch rides along because it comes out of the same status call: one round, one answer, nothing to disagree about.
type Commit ¶
type Commit struct {
SHA string `json:"sha"`
Short string `json:"short"`
Author string `json:"author"`
Time int64 `json:"time"`
Summary string `json:"summary"`
// Tags are the tag names pointing at this commit, in git's own order, and
// nothing else the commit is decorated with: a branch says where the
// repository stands, a tag says what this commit is.
Tags []string `json:"tags,omitempty"`
Pending bool `json:"pending,omitempty"`
}
Commit is one commit as the editor shows it: enough to say who wrote a line and what for, and nothing more. Pending marks the commit that does not exist yet, which is what blame answers for a line that is only in the working copy.
type CommitInfo ¶
type CommitInfo struct {
Repo bool `json:"repo"`
Branch string `json:"branch"`
HasCommit bool `json:"hasCommit"`
LastMessage string `json:"lastMessage,omitempty"`
}
CommitInfo is what the commit panel shows before anything is committed: where the commit would go, and what the last one said, which is what an amend starts from. A directory without a repository answers Repo false and nothing else, like Changes does.
type CommitResult ¶
CommitResult names the commit that was just made, in the words the log prints: the abbreviated hash and the subject line.
type ExecResult ¶
ExecResult is one proxied git call's answer, one to one: both streams as git wrote them, capped like every call of this package, and the exit code git ended with. A killed process answers the exec package's -1; the caller decides what to exit with for a code no shell could carry.
type FileStatus ¶
type FileStatus struct {
Path string `json:"path"`
Index string `json:"index"`
Worktree string `json:"worktree"`
From string `json:"from,omitempty"`
}
FileStatus is one path git reports as changed. Index and Worktree are the two status codes of the porcelain format, one character each ("." for unchanged, "M", "A", "D", "R", "C", "T", "U", and "?" for untracked). From carries the path a rename or a copy came from.
type Fingerprint ¶
Fingerprint is what the poller compares between two rounds. It carries two parts, because they answer two different questions and one of them is expensive to answer wrongly.
Base is the commit HEAD points at, the one thing the editor's diff is built against. It moves on a commit and on nothing cheaper.
Worktree is what the working copy looks like, the status output itself. It moves on every keystroke that reaches the disk, this editor's own saves included.
Both empty means the directory is no repository, which is a state like any other and stays that way between rounds. "git could not be asked" is not that state, and Fingerprint says so with its second return value instead of answering the zero value: a round that failed knows nothing, and treating nothing as a change publishes a move that never happened, twice, once on the failure and once when the next healthy round finds the old value again.
func (Fingerprint) Moved ¶
func (f Fingerprint) Moved(other Fingerprint) bool
Moved reports whether anything at all changed between two rounds.
type LogPage ¶
type LogPage struct {
Repo bool `json:"repo"`
Commits []Commit `json:"commits"`
More bool `json:"more"`
}
LogPage is one page of history: the commits, and whether older ones exist beyond it. A repository without a first commit answers an empty page, like every history question here.
type NewWorktree ¶ added in v1.59.0
NewWorktree describes one worktree to add: the directory that becomes the working copy, the branch it stands on, and where that branch begins.
Start empty means Branch exists already and is only checked out there; with a Start the branch is created at that point, which is also how a branch that so far only exists on a remote gets a local one that follows it. Dir must be an absolute path that is empty or not there yet, git refuses anything else and says so.
type Prompt ¶
type Prompt struct {
Env []string
Asked <-chan struct{}
Answered <-chan struct{}
}
Prompt is one action's line to the person in front of the browser: the environment that points the call's helpers at the bridge, and the two signals the watchdog stretches its deadline on — a question grants the person their own window, an answer grants the action its budget again.
type Ref ¶
type Ref struct {
Name string `json:"name"`
Kind string `json:"kind"`
Branch string `json:"branch,omitempty"`
Head bool `json:"head,omitempty"`
}
Ref is one name the repository can be asked about: a local branch, a remote one, or a tag. Branch carries the local name a checkout of a remote branch would create, which is the remote branch's name without the remote in front. Head marks the branch HEAD is on right now.
type RefMatches ¶
RefMatches is what one round answers. Names and commits stay two lists, because they are two things: a name is a place the repository keeps, a commit is a point in its history, and only one of them can be checked out.
type RefSearch ¶
RefSearch is one round of a picker's autocomplete: the text somebody typed, which kinds to look through, and the cap per kind. An empty text is the picker as it opens and answers the recently moved names, never commits: a list of the newest commits is what the sheet's history is for, and the picker's job before anything is typed is to show where the repository stands.
type Repo ¶
type Repo struct {
// contains filtered or unexported fields
}
Repo reads one repository, addressed by a directory inside it. The directory is usually the project root, which may sit below the repository root; the paths git reports are always relative to the repository root, so they are cut back to the project in the calls that report them.
func New ¶
New returns a reader for the repository the given directory belongs to. Nothing runs yet, and the directory does not have to be a repository.
func (*Repo) AddWorktree ¶ added in v1.59.0
func (r *Repo) AddWorktree(ctx context.Context, w NewWorktree) error
AddWorktree adds a linked worktree to this repository. The registration is always written in the main repository, whether this reader was opened on it or on one of its worktrees, so a worktree of a worktree is a sibling and not a chain.
Everything git decides stays git's: a branch that another working copy holds, a name it does not accept, a directory that is not empty. Those come back in its words, and nothing on disk is left changed by a refusal.
func (*Repo) Blame ¶
Blame reads who last changed each line of the file on disk, so lines that are only in the working copy answer as pending, which is the honest answer while somebody is typing. A directory without a repository answers empty and no error, like every other call here.
func (*Repo) Changes ¶
Changes lists what the working copy changed. A directory without a repository answers an empty list and no error, like Status does.
This is the one read the whole git surface hangs on, so unlike the reads that only decorate a file it does not flatten "git could not be asked" into "no repository": that answer takes the branch out of the statusbar, the marks out of the tree and puts the clone where the repository's actions were, and a single stalled call must not do that to a repository that is there. A call that answered nothing travels as an error, and the client keeps what it had.
func (*Repo) Checkout ¶
Checkout switches the working copy to a branch. The name is a local branch, or through git's own guessing a remote one, and then the local tracking branch is created on the way; an ambiguous name, one git does not know, or local changes the switch would overwrite all come back in git's words, and the working copy stands as it was. There is deliberately no stash and no merge behind this.
func (*Repo) Clone ¶
Clone fills the directory from a repository: straight into it, never into a subdirectory, because the directory is the project. git refuses a directory that already holds anything, and that refusal comes back in git's words like every other one; authentication is whatever git on this host can do on its own, an SSH key or a credential helper, and a remote that wants more answers in git's words too, there is no prompt to give it.
func (*Repo) Commit ¶
func (r *Repo) Commit(ctx context.Context, message string, paths []string, amend bool) (CommitResult, error)
Commit records the picked paths as a commit, and it is the one write this package makes. It is a pathspec commit: the commit takes the working copy content of exactly these paths, and what is staged for any other path stays staged and stays out, so a coder that is halfway through preparing its own commit keeps its index. Three things have to travel along for that mode to mean what the panel showed: an untracked path needs an intent-to-add entry before a pathspec commit may take it, the source of a rename has to be in the pathspec or the commit would record the copy and keep the deletion pending, and both are found by asking status here rather than trusting the caller's list. Amend rewrites the tip instead of adding to it, and an amend with no paths at all rewrites only the message, the everyday typo fix.
Every pathspec is built as :(top,literal) from the repository relative path: top so a rename source outside the project keeps addressable, literal so a name that looks like a glob stays a name.
func (*Repo) CommitInfo ¶
func (r *Repo) CommitInfo(ctx context.Context) (CommitInfo, error)
CommitInfo reads what the panel needs. The branch is the symbolic name HEAD carries, which exists on an unborn branch too; a detached HEAD has none and answers its abbreviated commit instead.
func (*Repo) CreateBranch ¶
CreateBranch creates a branch at the current HEAD and switches to it. Whether the name is one git accepts is git's own question, and its answer comes back in its words.
func (*Repo) DeleteRemoteTag ¶
DeleteRemoteTag takes the tag off the remote, which is the half of a deletion everybody else sees, so it is never implied by the local one and always asked for. The remote is the same unambiguous one the tag was pushed to; a tag the remote does not hold is git's answer and not ours.
func (*Repo) DeleteTag ¶
DeleteTag takes the name away here and says nothing about what a remote holds: a tag that was pushed stays where it was pushed until somebody says otherwise, which is DeleteRemoteTag's own call. A name this repository does not have comes back in git's words.
func (*Repo) Exec ¶
Exec runs one git command line as it was typed, in the repository directory, for the cockpit's git proxy (`dev-cockpit git`). The arguments travel unchanged, nothing is injected in front of them, and output and exit code travel back as they came: an exit code, whatever it is, is git's answer and no error. What stays on is the safety net every call of this package runs under: no shell, the fail-fast prompt environment unless a bridge is attached (WithPrompt), the protocol whitelist, the breathing deadline while a question stands, and the group kill with the bounded pipe wait. The budget is the remote one, because push and pull are what the proxy exists for.
The error is the runner's own no-answer case (ErrNoAnswer: deadline, dropped call, a process that never ran) and never a git refusal.
func (*Repo) Fetch ¶
Fetch brings what the remotes have up to date, which is what the ahead and behind counts are read against, and reports whether one ran at all. A repository without a remote has nothing to fetch and answers false and no error, that is a state and not a failure, and the caller that would tell every open editor about a move needs to know the difference.
func (*Repo) FetchIfStale ¶
FetchIfStale fetches when the last fetch lies further back than maxAge, and reports whether one ran. The age is FETCH_HEAD's, which every fetch rewrites; a repository that never fetched has none and counts as stale. It is what the surfaces call that want fresh remotes without fetching on every glance: listing remote branches, opening the git sheet.
This is the quiet one, so it runs on the short budget: nobody started it, nothing on the page says it is running, and a remote that does not answer must not keep it alive for minutes.
func (*Repo) FileAt ¶
FileAt returns a file's bytes at a revision, HEAD when rev is empty. The second value is false when the path simply does not exist there, which is what a new file looks like and no error at all. Any revision but HEAD is verified first: HEAD not resolving is the ordinary unborn repository, while a name the repository cannot resolve is the caller's mistake and answers ErrRevision instead of reading as "no file here".
func (*Repo) Fingerprint ¶
func (r *Repo) Fingerprint(ctx context.Context) (Fingerprint, bool)
Fingerprint reads both parts in one pass. Splitting them is what lets a client tell "somebody saved a file" from "the commit I am comparing against moved": the first needs a fresh status and nothing else, the second is the only reason to fetch the revision again.
func (*Repo) Log ¶
Log lists the commits that touched a path, newest first, or the whole repository's when the path is empty. skip and limit page through it; one commit more than the limit is asked for, so More is an answer and not a guess. A directory without a repository answers empty and no error.
func (*Repo) Pull ¶
Pull brings the current branch up to its upstream, fast forward only: a branch that drifted apart needs a merge or a rebase, which stay with a coder or the command line, so git's refusal comes back in git's words and the working copy stands untouched.
func (*Repo) Push ¶
Push sends the current branch to its upstream: where it goes, and whether it may, is the repository's own configuration, and what git refuses comes back in git's words. The one thing it answers itself is the branch that has no upstream yet, which is every branch the sheet's New branch just created: git refuses that one with the line about setting one, and asking somebody who has just tapped Push to go to a command line for it is a dead end, so the push sets it on the way. force is --force-with-lease and nothing stronger: it overwrites an upstream that moved away, and still refuses when the remote holds work this repository has never seen.
func (*Repo) PushTag ¶
PushTag sends one tag to the remote, and only that tag: a push of everything the repository happens to hold locally is somebody else's decision to make on the command line. Which remote is the same question the upstream of a new branch asks, so it has the same answer, the single configured one or origin among several; with nothing to name this says so instead of guessing a destination for something as public as a release.
func (*Repo) Refs ¶
Refs answers the names and commits one round of a picker's search found. Without a text it is the plain listing it always was, ordered by how recently each name was made or moved, which is what an autocomplete wants at the top. With one, git is asked with it and only the hits come back; the limit applies per kind either way. The remotes' HEAD pointers are symbolic and stay out, they name a branch that is already in the list. A directory without a repository answers empty and no error.
func (*Repo) Revert ¶
Revert takes the working copy under one path back to HEAD, recursively for a directory, and it is the one deliberate discard this package offers. What HEAD has is restored, staged edits included, so one revert leaves the path clean: a modification goes back, a deleted file comes back, and a path without a state in HEAD, an untracked file or a staged addition, is deleted, which the caller's confirmation has to say before anything runs. Ignored files are not changes and stay untouched.
The two sides are found by asking status here, never by trusting the caller: what HEAD knows goes through restore, what it does not goes through clean, and a side with nothing under it runs nothing, because restore refuses a pathspec that matches nothing it knows. The source of a rename joins the restore like it joins the commit, or reverting the target would delete the file and leave its old name pending as a deletion. Every pathspec is built :(top,literal) for the same two reasons as the commit's.
func (*Repo) Tag ¶
Tag names a commit. A message makes it an annotated tag, which is what a release is: it carries who tagged and when, and git refuses it without an identity, in git's words like every other refusal here. Without a message it is the lightweight kind, a name on a commit and nothing else. A name git does not accept, or one that is already taken, comes back the same way; this call never moves an existing tag, because a tag that quietly moves is a release that means two different things to two people.
func (*Repo) WithPrompt ¶
WithPrompt answers a copy of the reader whose calls may ask.
func (*Repo) WorkingCopy ¶
WorkingCopy names the working copy this directory belongs to, for a caller that has to let one write at a time through it. The name is the absolute git directory, and that is the working copy and not the repository: a linked worktree has one of its own, while two projects inside the same checkout, a project below the repository root included, resolve to the same one and are the same working copy, which is exactly what a checkout and a commit may not do to each other.
It keeps the third state (`resolveErr`, `ErrNoAnswer`) instead of flattening it, and it is the second caller after Fingerprint that may not do without it: false says "this directory holds no working copy", which a caller may key around, while the error says "nobody knows", which it may not. Guessing there produces two names for one working copy, and two names are no lock at all. It is the one rev-parse every other call starts with and nothing on top of it.
func (*Repo) Worktrees ¶ added in v1.59.0
Worktrees answers every working copy this repository has, the main one first. It is the only place that knows which branch is currently taken where, which is what a form offering branches for a new worktree has to say before git refuses one. A directory that is no repository answers an empty list and no error, like every other reader here.
type Worktree ¶ added in v1.59.0
Worktree is one working copy of a repository: the main one and every linked worktree, in the order git lists them. Branch is the short name of what is checked out there and empty for a detached head or a bare repository, and a branch that stands in one working copy cannot be checked out in a second.
type WorktreeChange ¶
type WorktreeChange struct {
FileStatus
Added int `json:"added"`
Removed int `json:"removed"`
Binary bool `json:"binary,omitempty"`
}
WorktreeChange is a path the working copy changed, the status entry plus the two numbers a diff carries: how many lines came and how many went. It stays a status entry on purpose: the two porcelain codes carry what a status carries and nothing else does, a conflict for instance. Binary says git counted no lines because there are none to count.