DEV Community

Cover image for Git bundle verify said "complete history". git clone exited 0. The working tree was empty
Juan Camilo Auriti
Juan Camilo Auriti

Posted on AI-assisted

Git bundle verify said "complete history". git clone exited 0. The working tree was empty

I needed to move commits between two machines with no network route between them, so: git bundle. Write the bundle, copy it across, clone from it.

I checked my work the way you're supposed to:

$ git bundle verify transfer.bundle
9c824386e56b3c496c45823acc195283c024bccc refs/heads/main
The bundle records a complete history.
Enter fullscreen mode Exit fullscreen mode

Complete history. Then:

$ git clone transfer.bundle restored
Cloning into 'restored'...
warning: remote HEAD refers to nonexistent ref, unable to checkout.
$ echo $?
0
$ ls restored
$
Enter fullscreen mode Exit fullscreen mode

Exit status 0. A .git directory. No branches. No files.

What the bundle was missing

A HEAD. That's it.

$ git bundle list-heads transfer.bundle
9c824386e... refs/heads/main
Enter fullscreen mode Exit fullscreen mode

One ref. git clone needs to know which ref to check out, and it resolves that through the bundle's HEAD. There isn't one, so it falls back to a default that the bundle doesn't contain — you can see the wreckage afterwards:

$ git -C restored symbolic-ref HEAD
refs/heads/master
$ git -C restored branch
$
Enter fullscreen mode Exit fullscreen mode

HEAD points at refs/heads/master, which does not exist, because the bundle carried main. The objects are all there. Nothing is corrupt. git log main inside that clone works fine. There is simply no branch and no checkout.

What I assumed, and why it was wrong

I had this filed in my head as "don't build bundles from refs/remotes" — I'd made the bundle from refs/remotes/origin/main the first time, and blamed that.

Wrong. I tested both:

bundle created from verify clone exit files branch
refs/remotes/origin/main complete history 0 0 none
main (i.e. refs/heads/main) complete history 0 0 none
--all HEAD complete history 0 1 main

The local ref behaves exactly as badly. The refs/remotes detail was a coincidence I'd turned into a rule. The actual requirement is the HEAD ref, and nothing in verify's output is sensitive to it.

The incantation

git bundle create transfer.bundle --all HEAD
Enter fullscreen mode Exit fullscreen mode

--all takes every ref; HEAD adds the one that makes it clonable. That bundle lists four heads including HEAD, and the clone checks out a working tree with a branch on it.

If you want one branch rather than everything:

git bundle create transfer.bundle main HEAD
Enter fullscreen mode Exit fullscreen mode

Why this is worse than a failure

Every signal available to a script says success.

git bundle verify exits 0 and prints a sentence containing the word complete. git clone exits 0. Under set -e, nothing stops. The only evidence is a warning: on stderr, and a transfer script that logs stdout — or pipes through tail, or runs under a CI step that only surfaces failures — will never show it to anyone.

The failure lands later, somewhere unrelated, as "the repo on the other machine is empty."

The check that costs nothing

verify answers "are the objects intact". It does not answer "is this clonable". So assert the thing you actually want, after cloning:

set -euo pipefail

git bundle create "$B" --all HEAD
git bundle verify "$B"

git bundle list-heads "$B" | grep -qw HEAD \
  || { echo "bundle has no HEAD ref, clone would produce an empty tree" >&2; exit 1; }

git clone -q "$B" "$DEST"
git -C "$DEST" rev-parse --verify HEAD >/dev/null \
  || { echo "cloned repo has no resolvable HEAD" >&2; exit 1; }
Enter fullscreen mode Exit fullscreen mode

Two lines of assertion, and they're the two questions verify doesn't answer: is there a HEAD in the bundle, and did the clone end up on a commit.

The general shape, which is the part worth keeping: a verify step that checks integrity is not a verify step that checks usability. Anything you move between machines wants an assertion on the property you actually need at the destination — the objects being intact is necessary and nowhere near sufficient.

Tested on git 2.34.1. If your version prints something friendlier than a stderr warning, good — mine didn't, and the exit code is 0 either way.

Top comments (3)

Collapse
 
systemcraftdev profile image
SystemCraftDev •

Useful write-up, and the "verify checks integrity, not usability" point is the part worth keeping.

One addition for anyone who already has a bundle without a HEAD and can't re-create it: you don't need to rebuild it. Naming the branch explicitly works around the missing HEAD:

git clone -b main transfer.bundle restored

And if you've already cloned and ended up with the empty tree, the objects are all there, so this recovers it in place:

git switch main

Git creates the local branch from origin/main and checks out the files. It also makes the empty-clone symptom easier to read: HEAD is pointing at a branch name (master here) that doesn't exist yet, so Git has nothing to check out, even though main is sitting right there in the remote refs.

Collapse
 
juanauriti profile image
Juan Camilo Auriti •

Thanks for the recovery commands. The post covers how to build the bundle with HEAD, your two commands cover the case where you already have one without HEAD and can't re-create it. Good to have both in the same place.

On the empty-clone symptom, same read: HEAD points at a branch that doesn't exist, main is sitting in the remote refs but git has nothing to check out.

Collapse
 
mrsaynothing profile image
Mr Say Nothing •

The split between the two checks is the whole story: bundle verify validates object connectivity inside the bundle file, while clone re-validates and re-hydrates into a working tree — the second one checks things the first never looks at. 'verify said OK' and 'checkout produced files' being different code paths is exactly why backup-restore drills exist. Did the empty tree trace back to a broken ref in the bundle, or to something on the receiving side — and did git fsck ever disagree with either tool?