worktree

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jun 24, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package worktree owns the git-worktree machinery yottacode layers on top of plain `git worktree`: name generation, path resolution under <repo>/.yottacode/worktrees/, .worktreeinclude file copying, and clean/dirty detection for end-of-session cleanup.

The package is intentionally free of agent / tool / TUI imports so the same primitives drive the --worktree CLI flag, the enter/exit agent tools, and the worktree admin subcommand.

Index

Constants

View Source
const BranchPrefix = "worktree-"

BranchPrefix is prepended to every yottacode-managed branch so the list in `git branch -a` is easy to filter.

View Source
const HomeSubdir = "worktrees"

HomeSubdir is the user-home subdirectory under which yottacode materializes worktrees: ~/.yottacode/worktrees/<repo-slug>/<name>/. Living outside the originating repo keeps the working tree clean (no nested worktree dirs the IDE / find / grep would walk into), keeps the repo's gitignore unchanged, and matches the existing ~/.yottacode/sessions/ convention.

Two repos with the same basename get distinct slugs (basename + an 8-char hash of the absolute repo path), so cross-repo name reuse (e.g. `feature-x` in two projects) does not collide.

View Source
const IncludeFile = ".worktreeinclude"

IncludeFile is the per-repo file (in the repo root) that lists gitignored files which yottacode should copy into each fresh worktree. Without it the new worktree is missing .env / IDE configs / build artifacts the user's project actually needs to run.

Format: one line per glob pattern, relative to the repo root. Lines starting with '#' and blank lines are comments. Patterns use filepath.Match semantics — sufficient for the common cases (.env, .env.*, .vscode/settings.json) without dragging in a full gitignore parser.

View Source
const OriginFile = ".origin"

OriginFile is the dotfile name stored in each slug dir whose contents is the absolute path of the originating repo. Lets the `yottacode worktree where` admin command answer "which repo does this slug belong to?" without needing a live worktree to query via git. Dot-prefixed so listings of the slug dir contents (e.g. for `worktree list`) can trivially skip it.

Variables

This section is empty.

Functions

func Branch

func Branch(name string) string

Branch returns the canonical branch name for a worktree.

func CopyIncluded

func CopyIncluded(repoRoot, worktreeDir string) error

CopyIncluded copies files matching .worktreeinclude patterns from repoRoot into worktreeDir, preserving relative paths and creating parent directories as needed. Patterns are matched against paths relative to repoRoot using filepath.Match for each segment, with '**' supported as "any number of intermediate segments".

Files that don't exist at the source are silently skipped (the user's .worktreeinclude may list optional files like .env.local). Symlinks and other non-regular files (devices, FIFOs, sockets) are skipped rather than followed: matchPatterns surfaces a symlink as an ordinary entry, and following one could copy a file from outside the repo (e.g. a symlinked ~/.ssh/id_rsa) into the worktree or hang on a device. Files that exist but are unreadable cause an error so the user isn't silently shipped a broken worktree.

func Dir

func Dir(repoRoot, name string) string

Dir returns ~/.yottacode/worktrees/<slug>/<name>, the materialized worktree directory for the given (repoRoot, name) pair.

func Generate

func Generate(ctx context.Context, repoRoot string) (string, error)

Generate returns a worktree name like "bright-running-fox" that does not collide with any existing entry under <repoRoot>/.yottacode/worktrees/. Falls back to a hex suffix if the adjective+noun namespace is exhausted (it won't be in practice, but the loop is bounded).

func IsAnyWorktreePath

func IsAnyWorktreePath(path string) (slug, name string, ok bool)

IsAnyWorktreePath is the repo-agnostic variant of IsWorktreePath: reports whether path is under any yottacode worktree dir at all and, if so, returns both the <slug> and <name>. Used by callers (TUI status-line chip, permission-rule rewriter) that need to recognize "this is inside SOME worktree" without first resolving the originating repo.

func IsWorktreePath

func IsWorktreePath(repoRoot, path string) (name string, ok bool)

IsWorktreePath reports whether path lives inside a worktree that belongs to the given repoRoot — i.e. under ~/.yottacode/worktrees/<slug(repoRoot)>/<name>/... — and returns the <name> segment. Cross-repo worktree paths return false: a session in repo A should not infer worktree names from repo B's worktree subtree.

func NormalizeForRule

func NormalizeForRule(p string) string

NormalizeForRule rewrites the worktree-name segment of a path inside a yottacode worktree to `*`, producing a worktree-agnostic glob pattern. Used by the permission rule deriver so an `[A]-always` click inside an auto-generated worktree (`noble-hopping-salmon`) doesn't pollute permissions.json with a rule that only matches that one ephemeral tree. Result is still per-repo-scoped via the slug.

Examples (with HOME=/home/me):

"/home/me/.yottacode/worktrees/proj-1a2b3c4d/noble-hopping-salmon/foo.go"
  → "/home/me/.yottacode/worktrees/proj-1a2b3c4d/*/foo.go"

"/home/me/.yottacode/worktrees/proj-1a2b3c4d/feature-x"
  → "/home/me/.yottacode/worktrees/proj-1a2b3c4d/*"

Paths that aren't inside a yottacode worktree pass through unchanged.

func OriginPath

func OriginPath(slugDir string) string

OriginPath returns the absolute path of the .origin file for a given slug dir. Exposed so callers can audit / unlink it without open-coding the filename constant.

func ReadIncludePatterns

func ReadIncludePatterns(repoRoot string) ([]string, error)

ReadIncludePatterns reads <repoRoot>/.worktreeinclude and returns the parsed list of patterns. A missing file is not an error — it just means no extra files get copied.

func ReadOrigin

func ReadOrigin(slugDir string) (string, error)

ReadOrigin returns the repo path recorded in `<slugDir>/.origin`, or an error if the file is missing or unreadable. The returned path is trimmed of trailing whitespace (so the writer's terminating newline doesn't leak into the value).

func Remove

func Remove(ctx context.Context, repoRoot, worktreeDir string, force bool) error

Remove deletes the worktree at worktreeDir and, if it was on a yottacode-managed branch (worktree-*), deletes that branch too. Pass force=true to override `git worktree remove`'s built-in dirty-tree check — callers should only do this after they have explicit user consent via the keep-or-remove prompt.

repoRoot is the main repository (not the worktree) so the branch delete runs in a context that still has the branch around.

func RepoRoot

func RepoRoot(ctx context.Context, cwd string) (string, error)

RepoRoot returns the absolute path of the git repository that contains cwd. Works from anywhere inside the work tree, including inside an existing yottacode worktree (git rev-parse --show-toplevel returns the worktree dir; --git-common-dir resolves the original).

Use ResolveRepoRoot if you need the main repository root even when called from inside a worktree.

func RepoSlug

func RepoSlug(absRepoRoot string) string

RepoSlug derives a stable, human-readable directory name from an absolute repo root: `<basename>-<short-hash>`. The hash (sha256 of the absolute path, first 8 hex chars) prevents collisions when two repos share a basename. Stable: the same repo always produces the same slug across invocations.

func ResolveRepoRoot

func ResolveRepoRoot(ctx context.Context, cwd string) (string, error)

ResolveRepoRoot returns the main repository root regardless of whether cwd is inside a worktree. Used by the CLI flag handler so `yottacode --worktree foo` from inside an existing worktree still creates the new sibling under the original repo's .yottacode/worktrees/, not nested.

func SamePath

func SamePath(a, b string) bool

SamePath reports whether two absolute paths name the same directory. Tries the cheap clean+compare first, then falls back to filepath.EvalSymlinks on both sides if that fails — on macOS, `/var` is a symlink to `/private/var`, so `git worktree list` may return one form while Dir() built the other from $HOME. Without the fallback, attach-detection misses the existing worktree and the caller falls through to `git worktree add -b`, which then fails because the branch is already there.

Returns false if either path is empty or if EvalSymlinks fails for both (e.g. the path no longer exists). Callers should treat SamePath == false as "no match", not as a hard error.

func SlugDir

func SlugDir(repoRoot string) string

SlugDir returns the per-repo container under user home: ~/.yottacode/worktrees/<slug>/. All worktrees for one repo live under this dir.

func ValidateName

func ValidateName(name string) error

ValidateName rejects names that would escape the worktrees dir or produce confusing paths. Allowed chars: ASCII letters, digits, hyphen, underscore. Length 1..64. No leading dot or dash.

func WriteOrigin

func WriteOrigin(slugDir, repoRoot string) error

WriteOrigin records the absolute repo path in `<slugDir>/.origin`, creating the slug dir if it doesn't exist. Idempotent: writing the same content over an existing file is a no-op from the user's perspective. Returns nil on a soft-fail when the slug dir simply cannot be created (e.g. permission denied) — `enter_worktree` soft-fails on this rather than rolling back the just-created worktree, since `.origin` is purely a discovery aid.

Types

type Info

type Info struct {
	Path   string
	Branch string // empty if detached
	Locked bool
}

Info is a single entry produced by `git worktree list --porcelain`. Only the fields we actually use are surfaced; the porcelain format also emits HEAD sha and detached state, which we don't need yet.

func List

func List(ctx context.Context, repoRoot string) ([]Info, error)

List parses `git worktree list --porcelain` into a slice of Info. The first entry is always the main worktree.

type State

type State struct {
	HasUncommitted bool
	HasUntracked   bool
	HasUnpushed    bool
}

State describes the tree state of a worktree as it bears on cleanup. Each flag corresponds to one of the three things `git worktree remove` would refuse to silently throw away:

HasUncommitted  - tracked files modified or staged
HasUntracked    - new files not yet `git add`-ed
HasUnpushed     - committed but ahead of the configured upstream

func DetectState

func DetectState(ctx context.Context, worktreeDir string) (State, error)

DetectState inspects worktreeDir and reports its cleanup-relevant state. The function does not modify anything.

Unpushed detection is best-effort: if the branch has no upstream configured (the common case for a fresh worktree-* branch), we treat any commits ahead of the merge-base with `origin/HEAD` as unpushed. If origin/HEAD itself isn't resolvable (no remote, detached HEAD, fresh init), we report unpushed=false rather than failing — the caller's other signals will still flag a dirty tree when it matters.

func (State) Clean

func (s State) Clean() bool

Clean reports whether nothing would be lost by removing the worktree.

func (State) Reasons

func (s State) Reasons() []string

Reasons returns a human-readable list of the dirty signals, for rendering in the keep-or-remove approval prompt.

Jump to

Keyboard shortcuts

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