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
- func Branch(name string) string
- func CopyIncluded(repoRoot, worktreeDir string) error
- func Dir(repoRoot, name string) string
- func Generate(ctx context.Context, repoRoot string) (string, error)
- func IsAnyWorktreePath(path string) (slug, name string, ok bool)
- func IsWorktreePath(repoRoot, path string) (name string, ok bool)
- func NormalizeForRule(p string) string
- func OriginPath(slugDir string) string
- func ReadIncludePatterns(repoRoot string) ([]string, error)
- func ReadOrigin(slugDir string) (string, error)
- func Remove(ctx context.Context, repoRoot, worktreeDir string, force bool) error
- func RepoRoot(ctx context.Context, cwd string) (string, error)
- func RepoSlug(absRepoRoot string) string
- func ResolveRepoRoot(ctx context.Context, cwd string) (string, error)
- func SamePath(a, b string) bool
- func SlugDir(repoRoot string) string
- func ValidateName(name string) error
- func WriteOrigin(slugDir, repoRoot string) error
- type Info
- type State
Constants ¶
const BranchPrefix = "worktree-"
BranchPrefix is prepended to every yottacode-managed branch so the list in `git branch -a` is easy to filter.
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.
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.
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 CopyIncluded ¶
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 ¶
Dir returns ~/.yottacode/worktrees/<slug>/<name>, the materialized worktree directory for the given (repoRoot, name) pair.
func Generate ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
SlugDir returns the per-repo container under user home: ~/.yottacode/worktrees/<slug>/. All worktrees for one repo live under this dir.
func ValidateName ¶
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 ¶
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 ¶
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.
type State ¶
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 ¶
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.