Skip to content

Run every git process through one builder with an explicit repository policy - #29

Merged
hmirin merged 3 commits into
mainfrom
fix/git-process-isolation
Oct 29, 2026
Merged

hmirin merged 3 commits into
mainfrom
fix/git-process-isolation

Conversation

@hmirin

@hmirin hmirin commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Step 5a of the refactoring plan, covering safety: Opus 1.8, 3.2, 3.3, 3.5; Astra Category 3 mediums on recover --repo and on carrier commits inheriting user Git behaviour. Performance batching is step 5b and is not in this PR.

Reproduced on the base branch, then fixed

  1. Recovery's scratch repository inherited GIT_DIR. Under git --git-dir=… e2ee recover, its init --bare and index-pack wrote pack files into the user's repository, so the connectivity check could pass for the wrong reason.
    Test: git_isolation::recovery_scratch_repository_ignores_an_inherited_git_dir. Before the fix: "recovery wrote into the repository GIT_DIR named".
  2. Carrier commits ran the user's hooks and signing. A global commit.gpgSign=true or core.hooksPath made carrier init and push fail. Carrier commits are now write-tree plus commit-tree --no-gpg-sign with a fixed identity, followed by update-ref, and pushes use --no-verify.
    Test: carrier_publication_ignores_user_hooks_and_signing, with failing hooks and a failing gpg.program set in GIT_CONFIG_GLOBAL. Before the fix, init failed.
  3. recover --repo B resolved its target from the current directory. It now uses B for the URL, key and state. --repo is optional, with no . default.
    Test: recover_resolves_its_target_through_the_named_repository, run from another repository and from outside any repository. Before the fix: "no Git remote named origin".
  4. A failed git ls-tree counted as "no copy". A new CarrierListing { Absent, Malformed, Chunks } separates what the storage contains from Git failures, which now propagate. git_rev_parse uses --verify --quiet.
    Test: storage::tests::a_failed_carrier_listing_is_an_error_not_a_missing_copy.
  5. Any failed carrier fetch, even offline, ran git fsck --full, and replaced cache generations were never removed. FetchError { Remote, Local } now skips the check when the remote is unreachable, and rebuild deletes the generations it replaces, under the lock.
    Tests: an_unreachable_carrier_does_not_trigger_a_cache_check (checked with GIT_TRACE), plus a new assertion in deleted_and_corrupt_carrier_caches_are_rebuilt. Before the fix, that assertion found two generations left.

The builder (src/gitcmd.rs)

  • GitRepository::from_environment() and GitRepository::open(path) resolve the user's repository once, to an absolute git dir. repo.git() runs git --git-dir <abs> and drops inherited repository-selection variables, including GIT_OBJECT_DIRECTORY, GIT_ALTERNATE_OBJECT_DIRECTORIES and GIT_COMMON_DIR.
  • For repositories the program owns (the carrier cache, the carrier checkout and recovery's scratch repository), every command drops repository selection, injected config and GIT_AUTHOR_*/GIT_COMMITTER_*. It adds -c core.hooksPath=/dev/null commit.gpgSign=false tag.gpgSign=false push.gpgSign=false core.fsmonitor=false.
  • transport() handles commands that talk to the carrier remote: fetch, ls-remote and push. It keeps config injected with git -c, such as proxies, credential helpers and insteadOf, and appends the overrides after it, so hooks and signing stay disabled.
    Test: carrier_transport_keeps_config_injected_with_dash_c_but_not_its_hooks. It fails with "Could not resolve hostname example-carrier" if transport drops injected config.
  • EncryptedRepository and src/git.rs take &GitRepository instead of &Path. git::ensure_repository and git_state_directory are deleted. About 150 test call sites were migrated.

Behaviour changes

  • Inherited GIT_OBJECT_DIRECTORY, GIT_ALTERNATE_OBJECT_DIRECTORIES and GIT_COMMON_DIR no longer apply to the user's repository. Otherwise --repo B could still read A's object store.
  • Outside a repository, a remote name no longer falls back to a remote defined in global config.

Verification

cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test --all-targets (189 passed). ~/.local/state/git-e2ee and ~/.config/git-e2ee did not exist before or after the suite.

Left open

  • A process whose checkout still borrows a generation that rebuild removed sees the carrier as unavailable. It fails closed at compare-and-swap.
  • create_carrier_checkout failures still run fsck for any cause.
  • resolve_tag_chain still treats a failed cat-file -t as "missing". Step 5b rewrites tag parsing.

… policy

git processes were spawned in many places, relying on whatever environment
and configuration they inherited.

- Recovery's scratch repository inherited GIT_DIR, so under
  git --git-dir=... e2ee recover its init and index-pack wrote packs into the
  user's repository and its connectivity check could pass for the wrong
  reason.
- Carrier commits ran the user's hooks and signing, so a global
  commit.gpgSign or core.hooksPath made carrier init and push fail.
- recover --repo B resolved its target from the current directory, not B.
- A failed git ls-tree on a historical carrier commit counted as "no copy".
- Any failed carrier fetch, even offline, ran git fsck on the whole cache,
  and generations replaced by rebuild were never removed.

src/gitcmd.rs now builds every git command. The user's repository is
resolved once to an absolute git dir and named explicitly. Repositories the
program owns drop repository-selecting variables, injected config and commit
identity, and add core.hooksPath=/dev/null and gpgSign=false. Commands that
talk to the carrier remote keep config injected with git -c (proxies,
credential helpers, insteadOf) and append those overrides after it. Carrier
commits use write-tree and commit-tree --no-gpg-sign; pushes use --no-verify.
Carrier listings return a typed absent/malformed/chunks result, fsck runs
only when the remote was reachable, and rebuild removes the generations it
replaces.
native_git_fetch_reimports_packs_after_local_objects_are_pruned failed twice
on Linux CI and never locally: once with "fatal: object ... not found" on the
second fetch, once because the feature commit survived gc --prune=now. Both
fit Git's detached auto-maintenance after a fetch repacking concurrently with
the test's own gc, but that is not confirmed.

The test now disables auto-maintenance, auto-gc and commit-graph writes in
its destination repository and replaces gc --prune=now with an explicit
repack -a -d and prune --expire=now, so nothing outside the test changes the
object store between the prune and the assertion.
@hmirin
hmirin merged commit ae32588 into main Oct 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant