gitworktree

package
v1.60.0 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package gitworktree performs the repository WRITES Quil needs to give a pane its own linked worktree, plus the listing that offers the existing ones.

It is deliberately a separate package from gitinfo rather than a few more functions in it. gitinfo's package documentation states that every call is a read and nothing there can modify a repository — and that is load-bearing rather than descriptive: the daemon runs gitinfo.Probe on a ticker against every pane's checkout, so a package that gains the ability to write is one careless refactor away from a ticker that writes. Keeping the two apart makes that mistake need an import to happen, which is a thing a reviewer can see.

Pure and stdlib-only, a sibling of gitinfo, gitdiscover and kubediscover. It shells out to git plumbing rather than manipulating .git by hand — creating a worktree means writing a gitdir file, an admin directory under worktrees/<name>, and a checked-out tree, and getting any of it subtly wrong produces a repository git itself cannot repair.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Add

func Add(ctx context.Context, repo, path, branch string) (string, error)

Add creates a linked worktree at path, checking out a NEW branch off the repository's default branch — see defaultBranch, and note that this is NOT the repository's current HEAD, which is what git would use if the start-point were omitted. repo is the directory the command runs in.

There is deliberately no force option and no --force in the argv. Every refusal git can raise here is a fact the user needs — the path is occupied, the branch already exists, the branch is checked out in another worktree, the repository has no commits to branch from — and forcing past any of them lands a pane on top of a checkout something else is using. The error is returned with git's own stderr attached, because "already used by worktree '/x/feat-y'" tells the user which pane to go look at and no message this package could invent would. The resolved start-point is RETURNED rather than kept private, and empty means "git's own HEAD default". A wrong base is invisible at create time by construction — that is the whole premise of this function — and surfaces days later as a PR whose diff is somebody else's work, so the caller logging what was actually used is the only place anyone can ever confirm it.

func DerivePath added in v1.52.0

func DerivePath(repoRoot, branch string) string

DerivePath returns where a worktree for branch belongs: a SIBLING of the repository, at <parent>/<repo><worktreesSuffix>/<flattened-branch>.

Sibling rather than nested so the repo tree stays clean — no .gitignore entry, and no tool that walks the tree (ripgrep, file watchers, the agent itself) finds a second full checkout inside the first.

Separators are flattened rather than preserved: feat/x is an ordinary branch name, and honouring it as a path would nest directories under the worktrees parent, so two branches could collide on an intermediate segment (feat/x and feat would want the same name to be both a file and a directory).

Callers must run ValidateBranch first — this function assumes the name cannot escape, and does not re-check.

func Remove added in v1.52.2

func Remove(ctx context.Context, repo, path string, branch string) error

Remove undoes an Add whose pane could not be created, and deletes the branch that Add made along with it.

This is the ONE place --force is right, and for a reason that does not generalise to Add: the worktree being removed was created by this daemon seconds ago and has never been handed to anyone, so there is no user work to protect — while without it git refuses to remove a worktree it considers dirty, which a fresh checkout can be on a repository with line-ending or filemode differences. Leaving it instead strands a full checkout on disk plus a branch pointing at it, and the next attempt at the same name then fails with "already exists" against a directory the user never made.

Best-effort by contract: the caller is already reporting a failure and this is cleanup, so the error is for the log, not for the user.

func RemoveWorktree added in v1.59.0

func RemoveWorktree(ctx context.Context, repo, path string) error

RemoveWorktree deletes a linked worktree's directory and git registration, and LEAVES ITS BRANCH ALONE. repo is the directory the command runs in.

The branch is the entire difference from Remove, and it is not a detail. The two callers are describing different situations: Remove undoes an Add whose pane could not be created — a checkout seconds old that nobody has touched, whose branch is empty and whose name must be free for the retry. This one runs when the user closes a pane they have been working in, so its branch can hold commits that exist nowhere else. Deleting it would destroy them with no warning and no undo, in a dialog whose stated subject is a directory.

--force is deliberate and is what the caller asked for: uncommitted and untracked files under the worktree go with it. The dialog counts them (see Status) and says so before the toggle can be armed. The `--` is the same belt-and-braces the dash guard in usableStartPoint is: the path reaches git in option position, and while current git rejects a dash-prefixed one on its own (`worktree remove` exposes only -f), that is a property of today's git rather than of this call. Terminating option parsing makes it a property of the call.

func Status added in v1.59.0

func Status(ctx context.Context, path string) (int, error)

Status counts everything the forced removal would destroy in the worktree at path: modified tracked files, untracked ones, AND IGNORED ones.

The ignored half is the one that was missing and it is the most dangerous to omit. `git status --porcelain` says nothing about ignored entries, so a worktree holding a `.env` and a `build/` reported ZERO — the dialog rendered "clean", which is the single answer that invites the toggle, and `--force` then deleted both. An ignored file is not in git at all, so unlike a committed change there is no branch to recover it from; it is simply gone. The earlier version of this comment claimed to cover exactly that case ("a whole unversioned build") and did not, because a build directory is normally ignored rather than merely untracked.

--ignored is the TRADITIONAL mode (the default when the flag carries no value), which respects -unormal and collapses an ignored DIRECTORY to one entry — so a node_modules costs one line. --ignored=matching would expand it and walk every file, which is the slow call this package otherwise avoids. The same collapsing applies to untracked directories, which is why the caller counts "files" loosely rather than promising an exact number.

--no-optional-locks because a plain `git status` refreshes and REWRITES the index: this runs against a checkout the user may be working in at that moment, and Quil is asking a question here, not doing work on their behalf.

A non-repository is an ERROR, unlike List, which folds that case into an empty answer. There it means "nothing to attach to" and is a real answer; here a 0 would mean "nothing to lose", which is a guess — and the dialog renders "clean" and "could not check" apart precisely so the guess is never made.

This is the one call gitinfo's ticker deliberately does not make: `git status` is the plumbing that can take seconds on a large repository without fsmonitor. It is affordable here because it runs ONCE, when the user opens a confirm dialog, against one worktree.

func ValidateBranch added in v1.52.0

func ValidateBranch(name string) error

ValidateBranch rejects a name that cannot safely be BOTH a git ref and a path segment.

Two grammars, deliberately, and neither subsumes the other. The name reaches `git worktree add -b <branch>` as argv — so a leading dash reads as a flag, and git's own ref rules bar the rest — and it also becomes a path segment via DerivePath, so it must not be able to reach outside its parent directory. A name legal as one can be dangerous as the other.

Slashes are ACCEPTED: feat/x is an ordinary branch name. DerivePath flattens them rather than this function rejecting them.

git itself would refuse most of these, and that is not a reason to skip the check: the refusal would arrive as a failed subprocess seconds later, after a permit and a single-flight slot have been spent, rather than as a message beside the field the user typed into.

Types

type Worktree

type Worktree struct {
	// Path is the working directory, absolute, as git reports it.
	Path string
	// Branch is the checked-out branch with the refs/heads/ prefix stripped.
	// Empty when Detached or Bare.
	Branch   string
	Detached bool
	// Main marks the repository's primary checkout — the first block git
	// prints. It is where a NEW worktree's path is derived from, and it is the
	// one entry that must never be offered as somewhere to attach.
	Main bool
	// Locked and Prunable mark entries git will refuse to operate on.
	Locked   bool
	Prunable bool
	// Bare marks a repository with no working tree. Only ever true on the main
	// entry, and it cannot host a pane.
	Bare bool
}

Worktree is one entry of `git worktree list --porcelain`.

Locked and Prunable are carried rather than filtered because both describe an entry that LOOKS attachable and is not: a prunable worktree's directory is gone, and a locked one refuses operations. Dropping them would hand the caller a shorter list with no way to explain the absence, so a pane could be offered a directory that cannot host it.

func List

func List(ctx context.Context, dir string) ([]Worktree, error)

List reports the worktrees of the repository containing dir, main checkout first. The caller supplies the timeout via ctx.

A directory outside any repository is NOT an error: the setup dialog asks about every directory the user browses to, and most are not repositories, so treating that as a failure would put an error on screen for the ordinary case. It returns no entries, which is the same shape the caller renders for a repository with nothing to attach to.

That collapse is deliberately narrow: a plain non-zero exit from `git worktree list` — the shape "not a repository" actually takes — is the only case folded into (nil, nil). A missing git binary (exec.ErrNotFound) and a call that ran out of time (ctx.Err() != nil) are returned as errors instead, or a missing binary, a corrupt repository and a permissions error would all render the same confidently-wrong "not a git repository" the caller shows for the genuine case.

Jump to

Keyboard shortcuts

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