This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
The source of git-workspace, a "Workspace Projection Layer" CLI: a declarative git-workspace.yaml assembles real git worktrees into a complete project tree. The entire engine is the single-file ./git-workspace script (~900 lines of Python 3.8+, depends only on PyYAML and the git CLI). This repo is the software, not an active workspace — example.yaml / example.lock.yaml are bundled sample data (to try them: cp example.yaml git-workspace.yaml && ./git-workspace sync; the assembled worktrees are gitignored artifacts).
Design constraints (from the contributing guidelines, don't violate):
- Keep the single-file design — no build step, stdlib + PyYAML only; all heavy lifting delegates to native git.
- Keep it declarative — new capabilities belong in
git-workspace.yaml, not in CLI flags.
./git-workspace init # scaffold git-workspace.yaml + .githooks/pre-commit + core.hooksPath in the CWD
./git-workspace sync # fetch/update sources, materialize worktrees, refresh git filters, rewrite lock (= make setup / make sync)
./git-workspace sync --locked # strict mode: fail if resolved SHAs differ from git-workspace.lock.yaml (CI / reproducibility)
./git-workspace status # per-source SHA, dirty state, checkout filter, read-only lock state
./git-workspace outdated # lock drift + upstream newer tags (fetches each cache first)
./git-workspace guard # commit-protection check; invoked by the generated pre-commit hook
./git-workspace clean # remove worktrees (--all also deletes .workspace/git-cache)
./git-workspace update # self-update the installed CLI to the latest upstream release tag
./git-workspace version # print version (also -V/--version)
./install.sh [--prefix DIR] [--uninstall] # install CLI to ~/.local/bin (Linux/macOS/Git-Bash); install.ps1 for native WindowsThere is no test suite. Verify changes by running the CLI: -h, version, init in a scratch dir, and the end-to-end smoke test against the bundled example (network access to GitHub required):
cp example.yaml git-workspace.yaml && ./git-workspace sync && ./git-workspace status
./git-workspace clean --all && rm git-workspace.yaml git-workspace.lock.yaml # teardownThe Makefile wraps the same commands (make setup / make sync / make locked / make status / make clean-all / make install) for use inside an actual workspace.
The CLI works both in-repo (./git-workspace) and globally installed. find_root() walks up from the CWD until it finds git-workspace.yaml, then init_paths(root) binds the module globals (ROOT, CONFIG_PATH, LOCK_PATH, DOT, CACHE, STATE_PATH). Exceptions handled in main(): version and update need no root; init operates on the CWD itself; guard is lenient — no config found → exit 0 (nothing to protect) rather than blocking commits.
UPSTREAM_URL points at the canonical GitHub repo. Release flow is tag-driven: install.sh / install.ps1 in standalone (curl|sh) mode resolve the latest tag via git ls-remote --tags --refs --sort=-v:refname and shallow-clone that (default-branch fallback + warning if no tags exist); cmd_update compares _ver_tuple(f"v{__version__}") against the latest tag and overwrites the running script file in place (preserving its mode). update refuses when the script lives in a git checkout (.git next to the script → use git pull), so it only touches installed copies. When adding features: bump __version__, commit, then tag v<version> and push the tag — installers and update pick it up automatically.
git-workspace.yaml— declarative source list (checked in). Each source:url,revision,path(assembly location, may be nested), mutually-exclusiveinclude/excludecheckout filters, optionalreadonly, optionalcachekey.git-workspace.lock.yaml— generated by sync: revision + resolved 40-char SHA per source. Checked in sosync --lockedcan reproduce exact snapshots. SHAs are force-quoted via the_QuotedYAML representer because all-digit SHAs would otherwise round-trip as integers..workspace/(gitignored) —git-cache/<key>.gitbare mirror clones andstate.json(last-sync SHA/filter/readonly per source;state.json'sshais the sync baseline, not the config).
cmd_sync runs two phases:
- Fetch + resolve — every source is resolved against a mirror cache.
cache_key()defaults to a slug of the URL, so multiple sources from the same URL (e.g.fastjson2-core/fastjson2-ext) automatically share one object store; thefetchedset ensures one fetch per cache per sync.--lockedvalidation happens after resolution, before any worktree is touched. - Materialize — sources ordered by path depth (parents first), each becoming a real detached worktree via
git worktree addfrom the cache. Filtered sources use--no-checkout→apply_filter→read-tree -mu HEAD(the empty index from--no-checkoutneeds one materialization pass).readonlysources are chmod-locked (set_locked) after sync; sync itself is the only writer (unlock → checkout → relock).
include→sparse-checkout setin cone mode (directory whitelist; top-level files always kept).exclude→ non-cone mode:sparse-checkout set --no-cone '/*' '!/a/b'(gitignore-style negation, supports nested paths).- neither →
sparse-checkout disable.
Filter changes between syncs are detected by comparing source_filter() against state.json, and trigger re-application.
ensure_git_filters writes managed blocks (# git-workspace managed (begin/end)) that are rewritten wholesale on every sync and are self-healing:
- outer repo's
.git/info/exclude— ignores each assembly path (reduced to aminimal_cover, so nested paths aren't double-ignored) plus/.workspace/. Anything not under an assembly path is local code and is tracked by the outer repo automatically. - parent worktree's
info/exclude— located viagit rev-parse --git-common-dir(linked worktrees keep exclude in the commondir, not the per-worktree dir) — ignores nested child sources likeflow-engine/web.
Hooks are managed by write_hook(), which writes .githooks/pre-commit (created by init, rewritten by ensure_hooks on every sync; this repo tracks one such hook, dogfooding it — guard is lenient when no config exists, so it never blocks commits here). The hook prefers git-workspace from PATH and falls back to ./git-workspace, then runs guard, which rejects staged files under any assembly path and any staged gitlinks (mode 160000). Protected paths are computed dynamically from the config via protected_paths().
In materialize_source, when a worktree's HEAD ≠ target SHA, sync refuses if the tree is dirty or if HEAD is ahead of the previous sync point (state.json SHA) — deliberately not ahead of the target SHA, because switching between tags/branches means HEAD legitimately contains upstream commits the target lacks. load_lock defends against numeric-SHA corruption by rejecting any SHA that isn't exactly 40 chars.
Enforced invariants a new source must satisfy: name matches ^[A-Za-z0-9][A-Za-z0-9._-]*$; include/exclude mutually exclusive lists of path strings with no ..; path not inside RESERVED_TOPS (.git, .workspace), not colliding with RESERVED_FILES, no duplicates; and no source may be nested inside a readonly source.