How to get your repos back from a gitdr backup, one repo or a whole org gone dark. Practice this before you need it.
You need:
- The gitdr binary or container, same major version that wrote the backup.
- Read access to the destination bucket. A read-only credential is enough, restore never writes to the destination.
- A config file pointing at that destination (
--config), same as for backup. - For
verify, the manifest public key (manifest.publicKeyPathin config). - If backups were encrypted, the encryption key in
GITDR_ENCRYPTION_KEY.verifydoesn't need it, checksums cover the stored ciphertext.
{host}/{org}/{repo}/{YYYY-MM-DD}/{repo}.bundle git data (full mirror)
{host}/{org}/{repo}/{YYYY-MM-DD}/{repo}.sha256 checksum of the bundle
{host}/{org}/{repo}/{YYYY-MM-DD}/{repo}.meta.json metadata (audit/reference)
{host}/{org}/{repo}/{YYYY-MM-DD}/{repo}.lfs.tar LFS objects (if any)
{host}/{org}/manifests/{timestamp}.manifest.json signed run-manifest (plus .sig)
So a backup of github.com/acme/api from 2026-06-01 lives under
github.com/acme/api/2026-06-01/.
List the manifests and pick the run you want (use your cloud's CLI, like aws s3 ls,
gcloud storage ls, az storage blob list):
aws s3 ls s3://my-worm-bucket/github.com/acme/manifests/gitdr verify --config config.yaml \
--manifest github.com/acme/manifests/2026-06-01T02:00:00Z.manifest.jsonThis checks the ed25519 signature and re-downloads every artifact to confirm its SHA-256.
Exit code is non-zero on any signature or checksum mismatch. A clean signature valid: true, artifacts N/N ok means the run is intact.
gitdr restore --config config.yaml \
--host github.com \
--repo acme/api \
--date 2026-06-01 \
--out ./restore/apigitdr downloads the bundle, re-checks its checksum, runs git bundle verify, clones it into
--out, and then compares the refs of what it just cloned against the ref-to-commit map the
bundle itself declares. If encryption was used, set GITDR_ENCRYPTION_KEY first.
That last check is the one to keep. It prints a line like:
refs: 8 of 8 declared by the bundle present at the same commit
Because git is content-addressed, a matching commit id covers that commit's tree, its files and its entire ancestry. So this line is not a spot check, it is the statement that the restored history is the backed-up history. A ref that is missing or points somewhere else fails the command with a non-zero exit and names the first one that differs, so a restore that did not reproduce the repository cannot be mistaken for one that did.
If the counts differ, gitdr says which refs it could not account for. Refs outside
refs/heads/* and tags — refs/notes/*, refs/merge-requests/*, refs/keep-around/* — are
reported separately and are not a failure: git clone does not create them, so their objects
are restored with nothing pointing at them. To get them back, fetch them from the bundle by
name.
Keep the output. SOC 2 A1.3.2, CIS v8.1 11.5, ISO 27001 A.8.13, CSA CCM BCR-08 and NIS2 implementing regulation (EU) 2024/2690 Annex 4.2.3 and 4.2.6 all want a tested restore with a documented result, and this is that result without a screenshot.
gitdr has already compared every ref against the bundle. This is the independent look.
cd ./restore/api
git log --oneline -5
git fsck --full
git for-each-ref # all branches and tags present?If the repository uses Git LFS, confirm the working tree holds real content and not pointers:
git lfs ls-files -n | while read -r f; do
head -c 41 "$f" | grep -q '^version https://git-lfs' && echo "STILL A POINTER: $f"
doneSilence means every tracked file was materialised. A pointer file is about 130 bytes of text where your data should be.
gitdr restore does this itself from v0.1.5 and fails the restore if anything is still a
pointer. Before v0.1.5 it did not. A clone from a bundle carries no LFS filter
configuration, and git lfs checkout exits 0 without doing anything when that configuration
is missing — so a restore onto a fresh machine, which is the usual machine in a disaster,
could report success over pointer files. If you restored with an earlier version, re-run the
check above against that copy.
The objects themselves were always backed up correctly; they are in the run's .lfs.tar and
nothing was lost from the bucket.
Point the restored repo at a fresh remote and push everything:
git remote add new https://new-host/acme/api.git
git push --mirror new--mirror pushes all branches and tags. Re-create the org or project on the new host
first. gitdr restores git data, not org settings.
For a full DR event, drive the per-repo restore from the run-manifest. It lists every repo
and the date. Pull the manifest, iterate its repos[], and run step 3 per repo, then step
5. Restore is independent per repo, so it parallelizes safely.
- Faithful: all git history, branches, tags, and LFS blobs. This is a true mirror.
- Audit and reference only: the
*.meta.json(issues, PRs/MRs, comments, labels, milestones, releases). No tool can replay these into a VCS with the original numbers, authors, timestamps, or cross-references. Treat recovered metadata as a read-only record, not a re-import.
| Symptom | Cause / fix |
|---|---|
verify reports a checksum failure |
Object was altered or truncated at rest. Restore from an earlier good run, then investigate the bucket. |
restore fails decrypting |
Wrong or missing GITDR_ENCRYPTION_KEY. It must be the KEK used at backup time. |
git bundle verify fails |
Bundle is corrupt. Restore the same repo from a different run or date. |
| Access denied listing or getting | Restore credential lacks read on the bucket or prefix. |
| Can't find the date | Backups are dated by run. List the repo prefix to see available dates. |