Test Git object connectivity before mirroring a repository

John Burns

A repository can have a branch name, a current-looking HEAD, and a clean worktree while still being unable to supply every object reachable from its history. That matters when a backup, mirror, deployment export, or migration job treats a successful git fetch as enough evidence that the repository can be copied safely.

git fsck --connectivity-only gives that review a small, local failure boundary. It walks the object graph without performing the broader object-validity checks that a full git fsck performs. In a disposable repository tested with Git 2.47.3, the connectivity check exited zero while the committed blob existed. After that one reachable blob was removed from the disposable object database, the same command reported a missing blob and exited 2. Nothing contacted a remote and no application repository was modified.

Use this as a pre-mirror or pre-export gate when the important question is whether the refs you intend to preserve still lead to available objects. It is not a replacement for a backup restore test, a full integrity review, or a policy that protects the repository’s storage in the first place.

Decide what the gate proves

A Git commit refers to a tree, and the tree refers to blob objects holding file content. If one of those reachable objects is absent, a clone, bundle, archive, or later checkout can fail even though a short status command does not expose the problem.

git fsck verifies connectivity and validity of objects in a repository. The --connectivity-only option narrows that work to connectivity. That makes it useful for a fast, explicit check in a process that already has separate controls for object format and storage health. Do not describe a zero result as proof that a repository is free of corruption: it says the connectivity check found no missing reachable object, not that every object passed every validity rule.

Run the gate against the exact local repository that a mirror job will read. A check in a developer clone does not prove that a separate bare repository, build cache, or backup mount has the same objects. If the process mirrors a bare repository, run the command in that bare repository or name its --git-dir explicitly.

Start with a read-only check:

git fsck --connectivity-only

A successful result is normally silent and exits with status zero. Capture both the status and any diagnostic output in the job log. A mirror process should stop on a nonzero result rather than copying a repository whose reachable graph is incomplete.

set +e
git fsck --connectivity-only >git-fsck.stdout 2>git-fsck.stderr
status=$?
set -e
printf 'git-fsck-exit=%s\n' "$status"

if [ "$status" -ne 0 ]; then
    printf '%s\n' 'repository connectivity check failed; mirror not started' >&2
    exit "$status"
fi

The temporary set +e lets the script retain the command’s real exit code. Return that same status so a scheduler or CI runner marks the gate as failed. Do not add || true to this step; that would turn the evidence of a missing object into a successful job.

Reproduce the failure in a disposable repository

Do not remove object files from a repository that matters. The following fixture creates an isolated repository under a temporary directory, makes one commit, identifies the blob for its tracked file, and removes only that disposable loose object.

workdir="$(mktemp -d)"
repo="$workdir/repository"
git init "$repo"
git -C "$repo" config user.name 'Validation User'
git -C "$repo" config user.email '<synthetic-email>'
printf '%s\n' 'synthetic release manifest' > "$repo/manifest.txt"
git -C "$repo" add manifest.txt
git -C "$repo" commit -m 'add synthetic manifest'

The local identity exists only so the fixture can make a commit; it is not a suggested global Git configuration. The test data and email address are synthetic. No remote is configured, and the fixture does not need credentials or network access after Git is installed.

First confirm that the committed object graph is complete:

git -C "$repo" fsck --connectivity-only
printf 'healthy-exit=%s\n' "$?"

The Git 2.47.3 validation run printed no diagnostic and returned healthy-exit=0. That is the positive control. It confirms the command is usable in the test environment before the negative case changes the fixture.

Next obtain the object ID for the committed file and map it to its loose-object path. A Git object ID’s first two hexadecimal characters select the directory below .git/objects; the remaining characters form the filename.

blob=$(git -C "$repo" ls-files -s manifest.txt | cut -d' ' -f2)
object="$repo/.git/objects/${blob:0:2}/${blob:2}"
test -f "$object"
rm "$object"

This deletion intentionally damages only the disposable fixture. It is safe for the article test because the repository has no remote, contains no user data, and will be discarded. It is not a repair method and must never be pointed at a production checkout, a bare mirror, or a backup repository.

Run the same gate after the deletion:

set +e
git -C "$repo" fsck --connectivity-only
status=$?
set -e
printf 'missing-object-exit=%s\n' "$status"

In the validation run, Git printed a missing-blob diagnostic and returned missing-object-exit=2. The command followed the commit and tree to a blob that the repository no longer contained, then made the failure visible to the calling process. That difference between the two runs is the useful operational result: a gate can distinguish a complete graph from a repository that cannot supply a reachable file.

Put the check before the irreversible step

For a repository copy or bundle job, place the check after fetch or maintenance has completed and before the process writes a backup artifact, updates a mirror, or replaces an older copy.

git -C /srv/git/project.git fsck --connectivity-only
git clone --mirror /srv/git/project.git /srv/backups/project.git

Replace the paths with locations owned by the backup process. The example does not imply that a filesystem copy is a complete backup strategy. A useful backup plan also records the expected refs, protects the resulting artifact, and performs a restore test in an isolated location.

When the gate fails, preserve the diagnostic output and stop writing new artifacts. Investigate the repository that produced the failure: confirm the storage volume is healthy, compare the affected ref with a known-good peer or remote, and use the team’s approved recovery source. Do not run destructive cleanup, garbage collection, or ref deletion just to make fsck quiet. Those actions can remove evidence and make a recoverable object harder to locate.

A full git fsck is appropriate when an integrity investigation needs the wider validation that connectivity-only deliberately skips. Run it during maintenance or against a copy if the repository is large or if its output needs careful review. The narrow gate is best treated as one clear contract in an automation path: every object reachable from the refs being mirrored must be present before the mirror begins.

Verify the mirror separately

A clean connectivity result is an input check, not proof that a copy job worked. After a mirror is created, inspect its refs and run the same gate on the destination:

git -C /srv/backups/project.git show-ref --head
git -C /srv/backups/project.git fsck --connectivity-only

For a bare mirror, show-ref --head gives a compact inventory of the refs retained by the destination. Compare it with the source according to the retention policy; a mirror that contains complete objects but omits a required branch or tag is still not the backup you intended.

The final success condition has two parts. The source passes the connectivity gate before copying, and the destination passes it after copying with the expected refs present. The disposable test shows why the first part matters: Git reports a missing reachable blob before the automation can turn an incomplete source into a trusted artifact.

Sources