feat: automatically recover repositories which fail to update#617
Merged
notheotherben merged 2 commits intoJul 7, 2026
Merged
Conversation
When a backup run is interrupted part-way through a fetch (e.g. by a power failure or the process being killed), the local repository can be left in a state which blocks every subsequent update - most commonly because of stale git lock files such as packed-refs.lock - and until now the only way out was manual intervention. The git engine now attempts automatic recovery whenever an existing local repository fails to update, staged from least to most invasive and controlled by a new per-policy 'recovery' property: - none: never attempt recovery, report the error as before. - non-destructive (default): remove stale git lock files (older than 15 minutes, so locks held by live operations are never touched) and retry the fetch. - destructive (opt-in): additionally clone a fresh copy of the repository into a hidden staging directory alongside the backup and, only if the clone succeeds, swap it into place. A failed clone - unreachable remote, bad credentials, full disk - leaves the existing backup completely untouched, so transient remote failures can never destroy local data. Backups which complete through recovery are annotated as such in the backup summary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s9x49j7QCzeqBLWku995F
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #617 +/- ##
==========================================
+ Coverage 73.25% 75.74% +2.48%
==========================================
Files 30 31 +1
Lines 2109 2420 +311
==========================================
+ Hits 1545 1833 +288
- Misses 564 587 +23 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Addresses the codecov patch-coverage report on the recovery change by exercising the failure paths which the end-to-end tests don't reach: - recovery disabled or cancelled: the original error is reported and the repository is left untouched - the retried fetch after stale-lock removal fails: the retry error is reported - the destructive-recovery canary clone fails: the original repository is preserved, leftover staging directories are cleaned up, and the original error remains in the causal chain - a backup target without a parent directory is rejected - the 'recovery' policy property is parsed by the repo/gist sources' load paths and validated by the gist source Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s9x49j7QCzeqBLWku995F
notheotherben
commented
Jul 7, 2026
| let recovery_mode: RecoveryMode = policy | ||
| .properties | ||
| .get("recovery") | ||
| .map(|mode| mode.parse().unwrap()) |
Member
Author
There was a problem hiding this comment.
@copilot Can we avoid using a .unwrap() here to ensure we don't panic if parsing fails for some reason? Create a new PR against main with the fix
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.
Summary
When a backup run is interrupted part-way through a fetch (power failure, OOM-kill, etc.), the local repository can be left in a state which blocks every subsequent update — most commonly stale git lock files such as
packed-refs.lock:Until now the only way out was manual intervention. The git engine now attempts automatic recovery whenever an existing local repository fails to update, staged from least to most invasive and controlled by a new per-policy
recoveryproperty (alongside the existingrefspecsproperty):nonenon-destructive(default)destructive(opt-in)The successful clone acts as a canary gating the destructive path: if the remote is unreachable, credentials are wrong, or the disk is full, the clone fails and the existing backup is left completely untouched — a transient remote failure can never destroy local data. The staging and discard directories live alongside the target so the swap uses same-filesystem (atomic) renames, and if moving the fresh clone into place fails, the original repository is restored.
Backups completed through recovery are annotated in the summary (e.g.
unchanged at abc123; recovered by removing stale lock files), and recovery steps are logged as warnings.Changes
src/engines/git.rs— recovery pipeline:recover()(staging/ordering logic),remove_stale_locks()(recursive*.locksweep of.gitwith a staleness threshold),reclone_and_replace()(clone → canary check → atomic swap → rollback on failure)src/entities/recovery.rs— newRecoveryModeenum with parsing/validationsrc/sources/github_repo.rs,src/sources/github_gist.rs— plumb therecoverypolicy property intoGitRepoentities, with validation of the value at policy-validation timedocs/advanced/recovery.md— new documentation page (registered in the VuePress nav/sidebar), plus an example inexamples/config.yamlTesting
RecoveryModeparsing, policy validation, stale-lock sweeping (threshold respected; non-lock files and fresh locks untouched), and state annotationtest_backup): stale-lock recovery retries the fetch successfully, and destructive recovery replaces a repository with a corrupted.git/configthen fetches normally afterwardscargo test --features pure_tests(134 passed),cargo test engines::gitincluding network tests (9 passed),cargo clippyandcargo fmt --checkclean🤖 Generated with Claude Code
https://claude.ai/code/session_017s9x49j7QCzeqBLWku995F
Generated by Claude Code