Repository navigation
Keep one continuity floor per repository root and remove the unpinned paths - #26
Merged
Merged
Conversation
… paths The machine kept two disconnected pins: device-add/revoke/list advanced an administrator pin, while the remote helper validated only the checkout's pin. After a revocation seen only by device-revoke, storage could replay the pre-revocation state and a git push from the same machine published a generation the revoked device could still decrypt. Continuity now lives in one floor per repository root per machine, <state_dir>/<root>/continuity.json, guarded by one lock. Every operation that validates or publishes storage state (helper list, fetch, push and dry run, init, device-add, device-revoke, device-list, verify, recover) validates the chain against it and advances it after success. Per-checkout progress.json records only import progress (imported packs, imported generation, verified refs) and is never a continuity authority. The unpinned publish and fetch paths are removed: the hidden push and fetch commands, push_ref, push_update, delete_ref, the *_for_remote and validate_* variants and the admin pin. One push(repo, dest, RefChange) and check_push remain. EncryptedRepository takes an explicit state directory; only the binaries resolve the XDG default. Benchmarks keep only the native helper.
With one floor per machine, a process holding an older carrier snapshot failed with "rolled back" after another process on the same machine had published, although nothing was rolled back. pinned_chain now asks the storage to refresh once when validation against the floor fails, and validates the refreshed chain again; only a still-stale or forked view is an error. GitStorage::refresh re-fetches the carrier branch and resets the checkout; the directory backend reads live and needs no refresh. A push now validates before packing and then hits the usual compare-and-swap, which reports fetch first on a race. recover keeps unrefreshed validation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 2 of the refactoring plan. It fixes a confirmed high-severity bug from the Astra review, and removes the unpinned path flagged by both reviews (Opus 2.1).
Bug (reproduced on main)
The machine kept two disconnected continuity pins:
device-add,device-revokeanddevice-listadvanced an administrator pin;Reproduction, directory backend, real binaries:
device-revoke D.git pushfrom the clone.The push succeeded and published a generation where D was still an active reader. D's key decrypted the post-revocation commit.
device-listdetected the same rollback. The helper was protected only if the clone had fetched after the revocation.Change
src/continuity.rsstores one floor per repository root per machine at<state_dir>/<root>/continuity.json(default$XDG_STATE_HOME/git-e2ee), behind a single lock. Every operation that validates or publishes uses it: helper list, fetch, push and dry run,init,device-add,device-revoke,device-list,verifyandrecover. Each one validates the chain against the floor and advances it after success.advancenever moves backwards, and rejects a different head at the same generation (a fork) and a lower policy generation..git/git-remote-e2ee/<root>/progress.jsonrecords only import progress: imported packs, imported generation and verified refs. It is never a continuity authority. Rejected-heads evidence stays per checkout.git-e2ee pushandfetchcommands,push_ref,push_update,delete_ref, the*_for_remoteandvalidate_*variants,push_update_inner,change_ref_inner, the admin pin andClientState.push(repo, dest, RefChange)andcheck_push.ReadOnlyDeviceerror.EncryptedRepository::new(storage, key, state_dir). Only the two binaries resolve the XDG default, so every test uses a temporary state directory.pinned_chainnow callsStorage::refresh()once when validation fails and validates again. Only a view that is still stale or forked is an error. The carrier re-fetches its branch; the directory backend reads live. A push validates before packing, so a later race still hits the compare-and-swap and reportsfetch first.recoverkeeps unrefreshed validation.benchmark-comparison.shlosesBENCH_E2EE_PUSH_MODEand its staleverify --storagecall.docs/benchmarks.mdkeeps its historical numbers, with a note that direct mode is gone.Behaviour changes to review
initrefuses when this machine already has a floor for the key's root, before publishing.current_manifestvalidates against the floor and advances it.carrier_git_allows_exactly_one_concurrent_writerkeeps its two writers on separate machines. With one shared state directory, the floor lock serializes the pushes, so both succeed by design.Tests
tests/continuity_floor.rs::push_after_rollback_of_a_revocation_seen_only_by_device_revoke_is_rejecteddrives the real binaries. Before the change, the push over the replayed pre-revocation state succeeded. Now it fails with "rolled back" and storage HEAD is unchanged.stale_carrier_views_on_one_machine_refresh_instead_of_reporting_rollback. Before, it failed with "remote manifest rolled back from generation 1 to 0". Now the stale fetch sees the new generation, a stale push tomaingetsfetch first, and a new ref publishes.RefChange. Two-machine scenarios get separate state directories.cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --all-targets(169 passed).~/.local/state/git-e2eeand~/.config/git-e2eedid not exist before or after the suite.scripts/benchmark-local.shon a one-commit repository exits 0.Known flake (not from this PR)
native_git_fetch_reimports_packs_after_local_objects_are_pruned, added in #24, failed once on Linux CI for #25 withfatal: object … not foundand passed on a rerun. It is not investigated yet.