Skip to content

Latest commit

 

History

History
187 lines (147 loc) · 14.1 KB

File metadata and controls

187 lines (147 loc) · 14.1 KB

Architecture

Pattern Overview

Overall: Binary CLI with subcommand dispatch (sync/emit) and layered internal Go packages for a git-worktree artifact materialization tool.

Key Characteristics:

  • Single binary (git-worktree-include) with two subcommands: sync and emit
  • Internal packages isolate concerns: parsing, intersection logic, filesystem ops, plan resolution, execution, and worktree discovery
  • Plan-driven execution: sync resolves a Plan of actions, executes them via syncengine, then writes a Report
  • Report-reader pattern: emit is a pure read-only consumer of the report sync writes — never recomputes
  • Designed as a post-checkout git hook: auto-triggered on git worktree add
  • Two-gate security model: an entry must appear in both .worktreeinclude and .gitignore to be materialized

Layers

Entrypoint (cmd/git-worktree-include/):

  • Purpose: Thin binary entrypoint that calls cli.Run()
  • Location: cmd/git-worktree-include/main.go
  • Contains: func main() — delegates to cli.Run(os.Args), propagates its exit code
  • Depends on: internal/cli
  • Used by: End-user shell (or contrib/post-checkout hook)

CLI Dispatch (internal/cli):

  • Purpose: Subcommand dispatch, flag parsing, --version/--help handling, logger helper for stderr output with -quiet/-verbose support
  • Location: internal/cli/cli.go (dispatch + Run()), internal/cli/sync.go (full sync implementation), internal/cli/emit.go (full emit implementation)
  • Contains: Run(args []string) int — parses flags, dispatches to runSync() or runEmit(). Version variable (link-time overridable). resolveSrc() for auto-discovering the main repo via worktree.FindMainRepo() or explicit -src flag. realProbe() for hardlink-feasibility check via fs.SameFilesystem()
  • Depends on: internal/worktree, internal/fs, internal/worktreeinclude, internal/intersect, internal/syncengine, internal/report
  • Used by: cmd/git-worktree-include/main.go

Worktree Discovery (internal/worktree):

  • Purpose: Resolve the main repository root from a linked worktree's .git file chain without shelling out to git
  • Location: internal/worktree/worktree.go
  • Contains: FindMainRepo(dest) — reads <dest>/.gitgitdir:commondir → main repo root. Returns ErrNotLinkedWorktree when dest is not a linked worktree (e.g. main repo)
  • Depends on: Standard library (os, path/filepath)
  • Used by: internal/cli (sync.go, emit.go)

Filesystem Helpers (internal/fs):

  • Purpose: Low-level filesystem primitives
  • Location: internal/fs/fs.go
  • Contains: SameFilesystem(path, other) (device comparison via syscall.Stat_t.Dev), DeviceOf(fi) (extracts device number), EnsureParentDir(path) (creates parent with os.MkdirAll)
  • Depends on: Standard library + syscall
  • Used by: internal/intersect (indirectly via probe), internal/syncengine

.worktreeinclude Parser (internal/worktreeinclude):

  • Purpose: Parse .worktreeinclude file into typed entries with directives
  • Location: internal/worktreeinclude/worktreeinclude.go
  • Contains: Entry and Directive types, regex-based # wti=symlink/# wti=copy extraction. Parse(r io.Reader) and ParseFile(path) — returns []Entry. Entries may be literal paths or gitignore-style globs (containing *, ?, [). Missing file is detected via errors.Is(err, os.ErrNotExist).
  • Depends on: Standard library (regexp, strings, bufio)
  • Used by: internal/cli/sync.go

Intersection + Plan Resolution (internal/intersect):

  • Purpose: Gate .worktreeinclude entries against .gitignore, resolve materialization operations
  • Location: internal/intersect/intersect.go
  • Contains: Build(src, dest, entries, probe) — returns *plan.Plan plus []Warning for skipped entries. Two-phase resolution: literal entries (no wildcards) resolve directly via their exact path; glob entries (containing *, ?, [) collect during iteration and resolve via a single filepath.WalkDir of src that matches each regular file against compiled gitignore matchers. ProbeFunc type abstracts the cross-filesystem check for testability. materialize(absSrc, isDir, directive, probe) produces actions for one concrete path — wraps the resolution table (probed per-file hardlink/symlink, single DirSymlink, per-file DirCopy). loadIgnore() reads <src>/.gitignore (missing → empty matcher → no entries pass the intersection gate). isGlobPattern(pattern) detects wildcards. isIgnored(ignore, path) tests with trailing-slash robustness. walkRegularFiles(root, fn) skips non-regular files.
  • Depends on: github.com/sabhiram/go-gitignore, internal/plan, internal/worktreeinclude
  • Used by: internal/cli/sync.go

Action Plan (internal/plan):

  • Purpose: Define the resolved materialization actions that syncengine executes and report serializes
  • Location: internal/plan/plan.go
  • Contains: Plan struct wrapping []Action. Action struct with Type (OpType string: OpHardlink/OpSymlink/OpCopy), SrcPath, DestPath (absolute), IsDir (internal, excluded from JSON)
  • Depends on: Nothing external
  • Used by: internal/intersect, internal/syncengine, internal/report

Sync Engine (internal/syncengine):

  • Purpose: Execute a Plan against the filesystem — hardlink, symlink, copy with overwrite policy and hardlink→symlink fallback
  • Location: internal/syncengine/syncengine.go
  • Contains: Execute(p) — performs every action in p, returns Result{Actual, Errors}. Per-action apply(a) removes existing destination first (idempotent re-runs), creates parent dirs, then performs the operation. Hardlink failures fall back to symlink (recorded as symlink in Actual). Continue-on-error: all actions are attempted even if some fail. copyFile() preserves source mode.
  • Depends on: internal/fs, internal/plan
  • Used by: internal/cli/sync.go
  • Naming: Package is syncengine (not sync) to avoid shadowing the standard library's sync package for importers

Report Read/Write (internal/report):

  • Purpose: Versioned JSON persistence of sync results; the file that emit reads
  • Location: internal/report/report.go
  • Contains: Report struct (Version int + Entries []plan.Action), SchemaVersion constant (1). Write(src, dest, actions) — creates report directory, sorts entries by DestPath, writes indented JSON. ReadFor(src, dest) and ReadFile(path) — returns *Report or ErrNotFound. Dir(src) and Path(src, dest) compute the filesystem location: <src>/.git/worktree-include-reports/<basename(dest)>.json
  • Depends on: Standard library (encoding/json, os), internal/plan
  • Used by: internal/syncengine (via cli/sync.go), internal/cli/emit.go

Data Flow

Sync Pipeline (git-worktree-include sync):

  1. Auto-discover main repo root from dest worktree's .git file chain — internal/worktree.FindMainRepo(), or use explicit -src flag
  2. Parse .worktreeinclude into entries with directives — internal/worktreeinclude.ParseFile()
  3. Parse .gitignore at repo root — github.com/sabhiram/go-gitignore (via internal/intersect.loadIgnore())
  4. Build intersection: literal entries (no wildcards) are resolved directly — checked against .gitignore, checked on disk, then materialized. Glob entries (containing *, ?, [) are collected as compiled gitignore matchers, then resolved in a single walk of src — every regular file matched by a glob pattern and also git-ignored is materialized. Skipped entries are returned as []Warninginternal/intersect.Build()
  5. Resolve operations per concrete path via materialize(): no-directive files get probed hardlink/symlink; no-directive directories get walked per-file with probed hardlink/symlink; DirSymlink → single symlink (walk skipped); DirCopy → per-file copy. Glob entries always resolve individual files (never whole directories) — internal/intersect.materialize()
  6. Execute actions against filesystem: remove existing dest first (idempotent), create parent dirs, perform operation. Hardlink failures fall back to symlink — internal/syncengine.Execute()
  7. Write report of actual operations performed to <src>/.git/worktree-include-reports/<basename(dest)>.jsoninternal/report.Write()

Emit Pipeline (git-worktree-include emit):

  1. Auto-discover main repo root (from -src flag or <dest>/.git via worktree.FindMainRepo())
  2. Locate report file: <src>/.git/worktree-include-reports/<basename(dest)>.json
  3. Read and print indented versioned JSON to stdout; exit 1 if no report exists — internal/report.ReadFor() + json.MarshalIndent()

Key Abstractions

Directive (internal/worktreeinclude):

  • Purpose: Controls materialization mode per entry (hardlink-with-fallback, symlink, or copy)
  • Location: internal/worktreeinclude/worktreeinclude.go
  • Pattern: iota enum (DirNone, DirSymlink, DirCopy) parsed from # wti=... comments. Has a String() method returning lowercase directive name.

Entry (internal/worktreeinclude):

  • Purpose: A single parsed line from .worktreeinclude — repo-relative path + directive + source provenance
  • Location: internal/worktreeinclude/worktreeinclude.go
  • Pattern: Struct with Path, Directive, LineNo, Raw fields

OpType (internal/plan):

  • Purpose: Discriminated string union for operation kinds
  • Location: internal/plan/plan.go
  • Pattern: type OpType string with const values OpHardlink, OpSymlink, OpCopy. Serializes naturally in JSON.

Action (internal/plan):

  • Purpose: A resolved materialization operation (type + absolute source/dest paths)
  • Location: internal/plan/plan.go
  • Pattern: Struct with Type (OpType), SrcPath, DestPath, IsDir (internal, JSON-omitted). IsDir true marks a whole-directory symlink entry whose paths carry a trailing slash.

Plan (internal/plan):

  • Purpose: Ordered list of Action values to execute
  • Location: internal/plan/plan.go
  • Pattern: Struct wrapping []Action

Result (internal/syncengine):

  • Purpose: Outcome of executing a Plan, recording what actually happened (post-fallback) and any errors
  • Location: internal/syncengine/syncengine.go
  • Pattern: Struct with Actual []Action (operations actually performed, post-fallback, in plan order) and Errors []ActionError (action + error pairs). Non-empty Errors means partial failure (exit code 2).

Report (internal/report):

  • Purpose: Versioned JSON document recording actual operations performed
  • Location: internal/report/report.go
  • Pattern: Struct with Version int (always SchemaVersion, currently 1) and Entries []plan.Action sorted by DestPath. Serialized with json:"version" and json:"entries".

Warning (internal/intersect):

  • Purpose: Describes an entry that was skipped and why (not git-ignored or missing on disk)
  • Location: internal/intersect/intersect.go
  • Pattern: Struct with Entry worktreeinclude.Entry and Reason string. Returned as []Warning from Build() for caller reporting.

ProbeFunc (internal/intersect):

  • Purpose: Abstraction for the cross-filesystem check, enabling test stubs
  • Location: internal/intersect/intersect.go
  • Pattern: type ProbeFunc func(srcPath, destRoot string) bool. The production implementation (cli.realProbe) delegates to fs.SameFilesystem(). Used by materialize() to decide hardlink vs symlink for no-directive entries.

Entry Points

Binary entrypoint:

  • Location: cmd/git-worktree-include/main.go
  • Triggers: Shell invocation (directly or via contrib/post-checkout hook)
  • Responsibilities: Call cli.Run(os.Args), propagate exit code

sync subcommand:

  • Location: internal/cli/sync.go (runSync())
  • Triggers: git-worktree-include sync [-src <path>] [-dest <path>] [-verbose] [-quiet]
  • Responsibilities: Discover src/dest, read .worktreeinclude (missing → no-op, exit 0), build intersection plan via intersect.Build() (literal entries direct + glob entries via tree walk), execute via syncengine.Execute(), write report via report.Write(). No-op when not in a linked worktree (exit 0). Exit 2 on partial failure.

emit subcommand:

  • Location: internal/cli/emit.go (runEmit())
  • Triggers: git-worktree-include emit [-src <path>] [-dest <path>]
  • Responsibilities: Auto-discover src, read report via report.ReadFor(), print indented JSON to stdout. Exit 1 if no report exists.

contrib/post-checkout hook:

  • Location: contrib/post-checkout
  • Triggers: git worktree add — git invokes hook with all-zero null-ref as $1 for worktree checkouts
  • Responsibilities: Guard on all-zeros $1 (null-ref of at least 40 chars), call git-worktree-include sync. Generic, user-extensible: wti integration is one self-contained guarded block.

Error Handling

Strategy: Three-tier exit codes: 0 success, 1 hard error (bad usage, unreadable config, report write failure, no report for emit), 2 partial failure (some sync entries failed — continue-on-error, record successes in report). The post-checkout hook runs under set -e, so exit code 2 aborts subsequent hook sections.

Sentinel errors:

  • worktree.ErrNotLinkedWorktree — signals the path is not a linked worktree (benign no-op)
  • cli.errNothingToDo — wraps no-op causes (not a worktree, src==dest)
  • report.ErrNotFound — no report exists for the given worktree
  • worktreeinclude.ParseFile() returns errors wrapping os.ErrNotExist for missing file (callers treat missing as "no entries")

Cross-Cutting Concerns

Logging:

  • internal/cli.logger struct with three levels: errf() (always prints), sayf() (prints unless -quiet), dbgf() (prints only when -verbose and not -quiet)
  • Binary writes human-facing messages to stderr; machine-readable output (the report JSON) goes to a file
  • -v is a shorthand for -verbose

Caching: None. The tool is stateless — each invocation reads files from disk, computes, and reports.

Storage: Reports written to <src>/.git/worktree-include-reports/<basename(dest)>.json. Versioned JSON schema (current version 1). Sort entries by DestPath for stable, diff-friendly output. No stale report garbage collection in v1.