Skip to main content

Module vcs

Module vcs 

Source
Expand description

Per-mem version control via gix. Each mem owns a gix repository whose gitdir and worktree are resolved from the mem’s config at Engine::init time — isolated from any outer project repo and from the developer’s ~/.gitconfig.

Gitdir defaults to <mem>/.git/ and worktree to the mem root; the optional vcs block in .memstead/config.json overrides either (notably the shared-gitdir idiom { "../.git", ".." }). On first init the repo is bootstrapped and its per-repo config is patched with core.logallrefupdates = true + commit.gpgsign = false; core.worktree is written only when the declared worktree disagrees with the gitdir’s natural parent (shared idiom).

On every init we re-apply commit.gpgsign = false so a developer with global signing enabled does not hang every mutation waiting for a passphrase.

The committer (engine <noreply@memstead.io>) is set explicitly per commit via commit_as and is therefore independent of any user.name/user.email config — global or per-repo. The author is derived per commit from a CommitContext so provenance (agent / cli / external drift) is visible in git log without storing PII.

Trailer contract: callers pass prose only. The engine appends trailers (Tool:, Actor:, Client:) after a single \n\n separator. Callers MUST NOT write those keys themselves — duplicates would confuse git interpret-trailers consumers.

§In-process serialization: per-branch mutex

A mem’s commits race on a single git ref (today: HEAD’s symref target on the disk adapter, an explicit branch name on the git-tree adapter). Without serialization two concurrent commits against the same ref would either produce an orphan parent chain or fail gix’s reference-transaction check. The mutex’s job is to keep the tree-build + commit + ref-advance window atomic for one ref.

A process-wide registry (see [acquire_branch_mutex]) maps canonical ref-name strings (e.g. refs/heads/main) to Arc<Mutex<()>>. Both adapters acquire the mutex for the ref they are about to advance: the disk adapter resolves HEAD’s symref to a concrete refs/heads/<name> first so a future git-tree writer committing onto the same branch shares the same key. Different ref names hold different mutexes and proceed in parallel.

Cross-process contention is out of scope for this layer. A second process committing against the same ref hits gix’s own lockfile discipline (<gitdir>/index.lock, <gitdir>/HEAD.lock, …), which this module surfaces as VcsError::GitVCS_ERROR-coded envelopes. The human-readable message includes retry guidance; the industry norm (libgit2, GitHub Desktop) is to propagate lockfile errors back to the caller rather than introduce a custom flock layer.

Lock-order rule. When a code path holds more than one per-branch mutex simultaneously (e.g. a cross-mem move that touches two branches under one shared multi-root gitdir), the mutexes MUST be acquired in lexicographic ref-name order to prevent deadlock. [acquire_branch_mutexes_in_order] enforces this by sorting before acquisition; ad-hoc multi-mutex code in debug builds is caught by the assertion inside [acquire_branch_mutex].

Structs§

ClientId
Identity of the process speaking to the engine. For MCP, this is the clientInfo from the initialize handshake (e.g. ClientId { name: "claude-code", version: "2.1.0" }). For CLI-direct mutations, the crate populates it with its own name and version.
CommitContext
Provenance bundle for a single commit. Produced at the caller boundary (memstead-mcp tool handler, memstead-cli subcommand, engine-internal drift flush) and threaded through to the VCS commit path.
NoopVcs
Test-only VCS that records nothing and never errors. Used by engine tests that exercise mutation paths without touching a real repo. The returned SHAs are deterministic monotonic sentinels (noop-0, noop-1, …) distinguishable from real SHAs by prefix — changes_since relies on that distinction to produce empty deltas for noop mems.

Enums§

Actor
Caller categories for the Actor: trailer and for picking an author signature. Agent, Cli, and App get their author from the paired ClientId when one is present; External always uses the synthetic external <external@memstead.io> identity (no client is known); Unknown falls back to the committer identity.
VcsError

Traits§

Vcs
VCS operations trait — minimal surface. Only commit is needed by the engine today. changes_since goes straight to gix::diff::tree without expanding this trait.

Functions§

author_identity
Build the per-commit author (name, email) pair from the context. None means “fall back to the committer identity” — adapters then reuse the committer signature for the author slot.
create_vcs
Open or initialize the per-mem gix repository.
format_commit_message
Append the trailer block to the caller’s prose, separated by exactly one blank line. Normalises trailing newlines so "subject" and "subject\n" both produce "subject\n\nActor: …\n…".
sanitise_client_name
Sanitise a raw client name to a git-safe local-part matching [a-z0-9._-]+. Empty/whitespace-only input falls back to "unknown".