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.
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
$
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
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
$
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
--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
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; }
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)
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 restoredAnd 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 mainGit creates the local branch from
origin/mainand checks out the files. It also makes the empty-clone symptom easier to read: HEAD is pointing at a branch name (masterhere) that doesn't exist yet, so Git has nothing to check out, even thoughmainis sitting right there in the remote refs.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.
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?