Documentation
¶
Overview ¶
Package git wraps the git commands treewright needs.
Everything here shells out to the git binary rather than using a git library: treewright's questions ("is this branch squash-merged?") are most precisely answered by the same plumbing commands a human would run, and staying close to those commands keeps the behavior auditable.
Index ¶
- func AddedPaths(dir string) ([]string, error)
- func ApplyPatch(dir, path string) error
- func CurrentBranch(dir string) (string, error)
- func DiffStat(dir string) (string, error)
- func DirtyFiles(dir string) int
- func IntentToAdd(dir string, paths []string) error
- func RestoreTracked(dir string) error
- func Unstage(dir string, paths []string) error
- func UntrackedFiles(dir string) ([]string, error)
- func WritePatch(dir, path string) error
- type Info
- type Repo
- func (r Repo) AddWorktree(dir, branch string) error
- func (r Repo) AddWorktreeNewBranch(dir, branch, startPoint string) error
- func (r Repo) AheadBehind(branch, base string) (ahead, behind int, ok bool)
- func (r Repo) BaseCheckout(base string) Info
- func (r Repo) BranchExists(branch string) bool
- func (r Repo) DefaultBranch() string
- func (r Repo) DeleteBranch(branch string) error
- func (r Repo) Fetch(remote, ref string) error
- func (r Repo) FetchPrune(remote string) error
- func (r Repo) HasRemote(name string) bool
- func (r Repo) Ignored(rel string) bool
- func (r Repo) IgnoredFiles() []string
- func (r Repo) Inspect(wt Worktree, base string) Info
- func (r Repo) IsMerged(branch, base string) bool
- func (r Repo) MainDir() (string, error)
- func (r Repo) Managed() ([]Worktree, error)
- func (r Repo) Name() string
- func (r Repo) RefExists(ref string) bool
- func (r Repo) RemoteBranchNamespaces(remote string) map[string]int
- func (r Repo) RemoveWorktree(dir string) error
- func (r Repo) TopLevel() (string, error)
- func (r Repo) Tracked(rel string) bool
- func (r Repo) Unpushed(branch string) int
- func (r Repo) UserEmail() string
- func (r Repo) Worktrees() ([]Worktree, error)
- type Status
- type Worktree
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AddedPaths ¶ added in v0.2.0
AddedPaths lists the paths a diff against HEAD would create — the untracked files just marked intent-to-add, and any the user had staged as new.
It is what a move deletes from the checkout it took the work out of, and it is read from the diff rather than from the untracked listing so that a file the user had already `git add`ed is not left behind as a second copy.
func ApplyPatch ¶ added in v0.2.0
ApplyPatch applies a patch in a checkout.
--3way is what makes it land rather than merely apply: it falls back to a three-way merge using the blobs the patch names, so a patch written against one commit still applies over another. It goes through the index, so the work arrives staged — which is why a caller checking that it worked has to ask `git diff HEAD` rather than `git diff`.
func CurrentBranch ¶
CurrentBranch reports the branch checked out in dir ("" when detached).
func DiffStat ¶ added in v0.2.0
DiffStat summarizes what a checkout holds that HEAD does not, staged included. Empty means nothing does.
func DirtyFiles ¶
DirtyFiles counts uncommitted changes in a worktree, staged or not, including untracked files. Returns 0 when the directory is clean or missing.
func IntentToAdd ¶ added in v0.2.0
IntentToAdd records paths in the index as files meant to be added, which is what puts an untracked file into `git diff HEAD` — without it a patch of the working tree carries changes to tracked files and nothing else.
An empty list is a no-op rather than a bare `git add -N`, which would mean every untracked file in the checkout.
func RestoreTracked ¶ added in v0.2.0
RestoreTracked puts every tracked file in a checkout back to HEAD, in the index and in the working tree both.
From HEAD rather than from the index, which is the difference between clearing a checkout and merely discarding the changes nobody had staged: work that was staged is in the patch that has already landed elsewhere, so leaving it here would be leaving a second copy.
func Unstage ¶ added in v0.2.0
Unstage puts the index entries for paths back to what HEAD says, which for a path HEAD does not have means removing the entry — undoing IntentToAdd exactly.
Named paths rather than a bare `git reset`, and that is the whole reason this exists as its own function. A pathless reset would also unstage whatever the user had staged themselves, which is theirs and none of a move's business; by path, only the entries treewright wrote are touched.
An empty list is a no-op for the same reason IntentToAdd's is.
func UntrackedFiles ¶ added in v0.2.0
UntrackedFiles lists the files git neither tracks nor ignores in a checkout, relative to its root.
--exclude-standard is what keeps the ignored files out, and they are kept out on purpose: a .env is carried into a new worktree by carry_files, and a move that swept it up would take it out of the checkout every other worktree is carried from.
-z because these paths are handed straight back to git and to os.Remove, and a filename with a newline in it would otherwise arrive as two.
func WritePatch ¶ added in v0.2.0
WritePatch writes a checkout's whole diff against HEAD to path — staged and unstaged alike, which is what "everything that is not committed" means.
It streams into the file rather than going through runIn, for two reasons that both matter here: a working tree's diff can be large, and runIn trims its output, which would take the final newline off a patch that has to end with one.
--binary so that a changed image or a compiled fixture crosses too. Without it git writes "Binary files differ" and the patch is one git apply refuses.
Types ¶
type Info ¶
type Info struct {
Worktree
Status Status
DirtyFiles int
Unpushed int
Ahead int
Behind int
Compared bool // false when ahead/behind could not be computed
}
Info is everything the ls table and the removal guards need about one worktree, gathered in a single pass.
type Repo ¶
type Repo struct {
Dir string
}
Repo is a git repository identified by its main checkout directory.
func (Repo) AddWorktree ¶
AddWorktree checks an existing branch out into a new worktree directory.
func (Repo) AddWorktreeNewBranch ¶
AddWorktreeNewBranch creates branch at startPoint and checks it out into dir.
func (Repo) AheadBehind ¶
AheadBehind counts commits branch is ahead of and behind origin/<base>. ok is false when the comparison cannot be made, which callers render as "?" rather than as 0/0 — an unknown is not a zero.
func (Repo) BaseCheckout ¶
BaseCheckout describes the main checkout as a row alongside the worktrees.
Two of the three columns mean the same thing here as anywhere: uncommitted files are work left lying in the window you investigate from, and the divergence is measured against origin/<base> exactly as it is for a worktree — which for the checkout parked on that branch is the "you need to pull" indicator, and the one number that says whether what you are reading is stale.
Only the status is different, and StatusBase says why.
The branch is read fresh rather than remembered, because the base checkout is the one place the user switches branches by hand, from inside the window.
func (Repo) BranchExists ¶
BranchExists reports whether a local branch of this name exists.
func (Repo) DefaultBranch ¶
DefaultBranch reports the branch a clone of this repo would check out, by reading the symbolic ref origin/HEAD that git records at clone time.
Falls back to whatever the main checkout currently has out, then to "main". The point is to guess a base branch well enough that a generated config is usually right, not to be authoritative: the value lands in a file the user can edit, and every command reports which branch it forked from.
origin/HEAD is only trusted when the branch it names still resolves. It is a symbolic ref recorded once, at clone time, from whatever the remote's own HEAD then said — so it can outlive a renamed default branch, or name a branch that was never pushed at all. Returning such a name would put a base_branch in a generated config that nothing can fork from.
func (Repo) DeleteBranch ¶
DeleteBranch removes a local branch.
-D rather than -d because a squash-merged branch does not look merged to git: its commits are not reachable from the base branch, so -d refuses. Callers have already established that the work landed.
func (Repo) Fetch ¶
Fetch updates one ref from a remote. An error usually means the network is unavailable, which callers treat as "work offline" rather than as fatal.
func (Repo) FetchPrune ¶
FetchPrune drops remote-tracking refs whose upstream branch is gone.
func (Repo) Ignored ¶ added in v0.2.0
Ignored reports whether the repository's ignore rules cover a path, given relative to the main checkout. A directory answers for itself, so ".claude/skills/treewright" is a question this can be asked.
--no-index asks the rules alone. Without it git answers "no" for anything in the index however the rules read, which is the right answer to check-ignore's usual question — why is this file not being ignored — and the wrong one here, where a committed path and an unmentioned one need telling apart. Tracked asks that half separately.
func (Repo) IgnoredFiles ¶
IgnoredFiles lists paths git is ignoring, relative to the main checkout.
--directory collapses a wholly ignored directory into a single entry ending in "/", which is what keeps node_modules from contributing thousands of paths while still listing an ignored file that sits in an otherwise tracked directory, such as apps/api/.env.
func (Repo) Inspect ¶
Inspect gathers a worktree's state relative to origin/<base>.
Uncommitted work outranks everything because it is the most easily lost, then merged, then unpushed; a pushed-but-unmerged branch is "active".
func (Repo) IsMerged ¶
IsMerged reports whether branch's work has landed in origin/<base>, by either route a pull request can take:
- Normal or rebase merge: every commit on the branch is reachable from origin/<base>, so the branch has no commits outside it.
- Squash merge: the branch's individual commits never land upstream at all; they are collapsed into one new commit. To recognize that, synthesize a single commit holding the branch's entire tree on top of its merge-base — exactly the patch a squash merge produces — and ask `git cherry` whether an equivalent patch is already upstream. A "-" prefix means yes.
Stricter than "has no unpushed commits": a branch that is pushed but whose pull request is still open is not merged.
The squash path writes one dangling commit object to the object database. Nothing ever points a ref at it and git's normal gc reaps it, but this is the reason treewright needs write access to .git even for read-only-looking commands.
func (Repo) MainDir ¶
MainDir returns the repo's main checkout path as git sees it. Callers use this to identify which repo they are standing in: git reports the same main path from inside any of the repo's worktrees.
func (Repo) Managed ¶
Managed lists only the worktrees treewright created: the siblings of the main checkout named "<repo>-<slug>". Anything else attached to the repo — the main checkout itself, or a worktree made by hand elsewhere — is left alone.
The prefix is built from git's own spelling of the main checkout rather than from r.Dir, so a caller that reached this repo through a symlinked path still matches: git always reports fully resolved paths, and comparing those against an unresolved prefix would match nothing at all.
A slug is allowed to contain a path separator so that worktrees created before `new` rejected such slugs remain visible and removable.
func (Repo) Name ¶
Name is the main checkout's directory name, e.g. "myrepo" for ~/code/myrepo. Managed worktrees are its siblings, named "<Name>-<slug>".
func (Repo) RemoteBranchNamespaces ¶
RemoteBranchNamespaces counts the leading namespace of every branch on a remote: "feature/eng-1" and "feature/eng-2" make "feature/" worth 2. Branches with no namespace contribute nothing.
Read from refs/remotes/<remote>, which a clone fills in for every branch the remote has, so this describes what the team does rather than what this checkout happens to have fetched by hand.
Only "/" delimits a namespace. A dashed convention ("feature-eng-1") is indistinguishable from an ordinary ticket key ("eng-142-white-screen") without already knowing the team's scheme, and reading "eng-" as a namespace would be wrong far more often than right.
func (Repo) RemoveWorktree ¶
RemoveWorktree deletes a worktree directory and detaches it from the repo.
--force is needed for the ordinary case, not to override treewright's own safety checks: every worktree carries untracked build output that git refuses to delete without it. Whether the work is safe to lose is decided before this is ever called.
func (Repo) TopLevel ¶ added in v0.2.0
TopLevel returns the root of the checkout Dir is inside, as git reports it — the worktree's own root, where MainDir answers with the repository's main checkout from any of its worktrees. It is how `signal` names the checkout the calling hook is standing in, and git's fully resolved spelling is what makes the answer comparable to a window's worktree stamp.
func (Repo) Tracked ¶ added in v0.2.0
Tracked reports whether git has anything under a path in the index. A directory is a legitimate argument for the same reason it is to Ignored: the question is about a tree of files, and one tracked file in it means the tree is in the repository's history.
func (Repo) Unpushed ¶
Unpushed counts commits on branch that are reachable from no origin ref — work that exists nowhere but this local branch. 0 means every commit is either pushed or already merged somewhere upstream.
func (Repo) UserEmail ¶
UserEmail reports the git identity in force for this repo, respecting any repo-local override of the global setting. Empty when none is configured.
func (Repo) Worktrees ¶
Worktrees lists every worktree attached to the repo, main checkout first (git guarantees that ordering).
The porcelain format is one blank-line-separated record per worktree:
worktree /path/to/checkout HEAD 1a2b3c... branch refs/heads/main
A detached HEAD emits "detached" in place of the branch line, which simply leaves Branch empty — the state every query here already treats as "no branch to reason about". Parsing this once yields both the path and the branch for every worktree, which is why nothing runs `git branch --show-current` per directory.
type Status ¶
type Status string
Status summarizes how safe a worktree is to throw away.
const ( StatusDirty Status = "dirty" // uncommitted changes present StatusMerged Status = "merged" // landed in origin/<base>; safe to remove StatusUnpushed Status = "unpushed" // commits exist only here StatusActive Status = "active" // pushed, not yet merged (open pull request) // StatusBase is the main checkout, which is not a worktree and is never // thrown away. It gets a status of its own because the others answer "how // safe is this to remove", and every one of their answers is wrong here: a // base checkout sitting level with origin has no commits outside it, which // reads as merged — the green that means "safe to delete" — about the one // directory in the repository that must never go. StatusBase Status = "base" )
The statuses `ls` reports, in the precedence it applies them: dirty outranks everything because it is the most easily lost, then merged, then unpushed, and a branch that is pushed but not merged is active.