Owned, detached Git worktrees and explicit integration primitives for Common Lisp. ISC licensed, copyright Lambda Symbolics. Requires Git and ASDF/UIOP. The library has no process runner, task authority, sandbox, or integration-order policy.
Supply a function of (argv &key directory input) returning three values:
stdout string, stderr string, integer exit status. Arguments are an argv list,
never a shell command. Input is a string or NIL. The caller chooses execution
permissions, timeout, encoding, and process supervision. Nonzero status must be
returned, not raised by the runner, so the library can classify Git failures.
(cl-worktree:make-manager
:registry #P"/private/task-worktrees/"
:runner (lambda (argv &key directory input)
(uiop:run-program argv :directory directory
:input (if input (make-string-input-stream input) :interactive)
:output :string :error-output :string :ignore-error-status t)))Keep the registry private and serialize operations on the same Git repository. Git ref changes and clean-state checks are not atomic with external writers.
Use a runner with a controlled environment: clear inherited repository selectors
such as GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_COMMON_DIR,
GIT_OBJECT_DIRECTORY, GIT_ALTERNATE_OBJECT_DIRECTORIES, GIT_NAMESPACE,
GIT_CONFIG_PARAMETERS, and GIT_CONFIG_COUNT. Choose the executable and trusted
PATH in the runner. The sample above assumes an already controlled environment.
Every invocation uses per-command options to disable hooks, fsmonitor, optional index refresh locks, automatic maintenance, signing, rerere and submodule recursion. Configured external clean/smudge/process filters are discovered by key name and disabled without reading their values into results. Diffs disable external diff and textconv. These overrides do not edit global or repository configuration. Worktree checkout and comparison use raw repository contents rather than external filter transformations. Permission and timeout decisions belong to the runner.
create-worktree takes a manager and keyword arguments source, path,
baseline, owner, dirty-policy, and optional reserved-function. Baseline is a commit or commit-resolving
revision string. Paths are resolved relative to the process working directory.
The destination must not exist. A detached worktree avoids branch collisions.
The returned handle supplies worktree-id, worktree-owner, worktree-path,
worktree-source, worktree-baseline, and worktree-state.
Dirty policy defaults to :reject. Explicit :ignore uses only the chosen
committed baseline, excluding staged, unstaged, and untracked source edits.
Creation never checks out, resets, or stashes the source. A non-Git source
signals isolation-unavailable.
Creation publishes a :creating registry reservation before invoking Git. Use
reserved-function to record the pending handle in the caller's own durable state.
Creation revalidates the reservation after this callback; cancelling the handle
there prevents Git creation. Git creates the checkout with worktree add --lock --reason containing the invocation ID. If the runner fails or exits nonlocally,
retain the checkout and reservation for inspection. Call recover-worktree with
the owner to finish publication after Git has created the checkout. Recovery
verifies the reservation, Git registration, baseline, creation lock, and any
existing ownership marker before publishing ownership and releasing the lock.
cleanup-worktree also accepts a pending handle, including a reservation cancelled
before Git started or an absent checkout with its own locked Git metadata.
Unmarked or foreign metadata without this invocation's creation proof raises
ownership-error and retains the reservation. A pathname and baseline alone do
not prove ownership. Discover the published handle if interruption occurred after
ownership publication but before releasing the creation lock.
discover-worktrees returns two values: owned handles and absent orphan handles,
optionally filtered by owner. With source, it also scans private Git markers,
recovering handles if registry publication was interrupted. Discovery is read-only
and excludes unmarked worktrees. Registry records survive failed cleanup and
process restart. Externally pruned orphans can retire their own registry record
without pruning or removing unrelated metadata.
cleanup-worktree requires the owner and verifies the private registry and Git
directory identity marker. Dirty worktrees raise dirty-workspace; explicit
discard-dirty enables forced removal. Only this worktree is removed. Branches
and other worktrees are not deleted or pruned. Failed removal raises
cleanup-failure and retains the registry for retry.
Recoverable lifecycle failures expose explicit restarts. During dirty-source
creation, use-committed-baseline selects the same behavior as :ignore.
During cleanup, retain-worktree returns NIL without continuing removal;
retry-cleanup retries after repair and revalidates ownership. Successful cleanup
returns T. Restarts never authorize discarding child edits implicitly.
Ownership protects against accidental deletion, not hostile modifications by another process with access to the registry and Git metadata.
extract-patch requires the owner and returns an artifact including committed,
staged and unstaged tracked changes relative to the recorded baseline. Untracked
files are rejected; stage new files first. Git binary patches preserve binary
content and file modes. extract-commit-range requires a clean worktree and an
ancestor baseline; its content is an ordered list of commits. Both include a binary
patch and complete provenance through artifact-* accessors.
artifact->plist produces portable version-one data. plist->artifact validates
and restores it. The fields are:
:version(1),:kind("patch"or"commit-range");:worktree-id,:owner,:source,:common-git-directory,:isolated-path;:baselineand:tip(full commit identities);:patch(complete binary-capable patch content), optional:patch-reference;:commits(ordered identities for a commit range),:touched-paths.
The plist contains only portable scalar values and lists. Convert keyword keys
and lists using the caller's JSON serializer. Supply patch-reference to
artifact->plist to identify a separately secured patch. Content is retained.
Extraction never cleans up the worktree. Secure the portable artifact durably,
then explicitly call cleanup-worktree when task policy permits removal. A range
artifact includes a patch suitable for later integration even if its original
commit objects become unavailable after Git garbage collection.
check-patch checks applicability without mutation. apply-patch checks and
applies a patch to the clean target's index and working tree. Supply the patch
string, typically artifact-content. apply-commit-range cherry-picks a range
artifact into a clean target; commits must be available in its object database.
Integration targets are explicitly chosen by the caller, not inferred from an
artifact's source.
Integration also refuses targets with a pending merge, rebase, revert or cherry-pick
operation, even when their index and files are clean. The refusal preserves that
operation for its owner.
An integration-conflict supplies conflict-paths and failure-detail.
integration-conflicts returns the current unmerged paths. A failed cherry-pick
leaves Git's conflict state for inspection or resolution; abort-integration
explicitly restores Git's pre-cherry-pick state. Patch check failures leave the
target untouched. The caller controls acceptance, ordering and commits.
All domain conditions derive from worktree-error, with failure-operation
and failure-detail. command-failure additionally supplies the command and
exit code. Ownership and dirty-state refusals have their own condition types.
From this repository:
sbcl --noinform --non-interactive --eval '(require :asdf)' \
--eval '(asdf:load-asd (truename "cl-worktree.asd"))' \
--eval '(asdf:test-system "cl-worktree")'Tests create temporary real Git repositories and exercise source preservation, dirty indexes and untracked files, ownership and orphan recovery, binary patches, portable secured artifacts, range validation, conflict abort and cleanup failures. On Unix they exercise suppression of executable hooks, fsmonitor and external filters. Temporary repositories are removed after each run.
./script/bootstrap
./script/check
./script/check --fix./script/check runs the tests, then checks the source with
cheque as cheque.sexp
configures it. --fix skips the tests and repairs what cheque can repair.
cheque and its tools are development dependencies: checks/qlfile lists them,
and the check installs them on first use into ~/.cache/cheque/tools/.