Move Git History Offline with git bundle
A git bundle packs references and objects into one file you can carry across a gap Git itself cannot cross. This guide creates one on a source machine, checks exactly what it needs, moves it, and fetches its commits into another repository, an air-gapped network, a courier drive, a one-way transfer approval, whatever the boundary is. It uses Git 2.43.0, the version installed while it was written. Allow about 15 minutes for a small repository, plus however long the file takes to travel.
The route
Jump straight to the step you need, or tick off Done means at the end.
What a bundle actually does
A bundle is a file holding Git references and a pack of objects. It earns its keep when two repositories cannot talk to each other directly, but a file can travel by removable media, a controlled transfer service or another approved route. It is not a stand-in for a remote: Git can fetch from a bundle, but it cannot push into one.
The format records which reference tips the recipient can fetch. It can also record prerequisite objects that are deliberately left out of the file, which keeps incremental bundles small, but means the receiving repository must already hold that required history.
Checkpoint
Use a full bundle when the destination is new or its history is uncertain. Use an incremental bundle only when you can name, and verify, the destination's last known commit.
1. Prepare the source repository
Run these as your normal repository user, changing the two placeholders first. The example exports the main branch from a source checkout.
SOURCE=/path/to/source-repository
BUNDLE=/path/to/transfer/source-main.bundle
cd "$SOURCE"
git status --short
git log -1 --oneline main
A clean status is a nice sign, but it does not change which committed objects the bundle contains: a dirty working tree has no bearing on this. It is worth stopping, though, if a commit you expected is missing from the log. The bundle carries commits and related objects, never uncommitted file changes.
2. Create a complete bundle
Build a self-contained starting point, naming the branch as the revision argument:
cd "$SOURCE"
git bundle create "$BUNDLE" main
Git should report no error and leave a file at the path in BUNDLE. A destination can clone this kind of bundle without holding any of its objects first.
Safety warning
The output path is overwritten if it already names a file. Do not point it at your only copy of a previous bundle. Pick a new filename, or keep the old file until the new one has passed verification and transfer checks.
Inspect the references before you transfer anything:
git bundle list-heads "$BUNDLE"
git bundle verify "$BUNDLE"
For a complete bundle, verification should name the advertised branch and raise no complaint about missing prerequisite commits. The exact object ID depends on your source repository. If verification fails, do not send the file anyway; check the path, repository and revision argument first.
3. Transfer and verify the file
Copy the bundle through your approved offline route. Unmount removable media cleanly once the copy finishes. On the destination machine, point the path at the received file and verify it before touching the repository:
BUNDLE=/path/to/received/source-main.bundle
git bundle list-heads "$BUNDLE"
git bundle verify "$BUNDLE"
sha256sum "$BUNDLE"
Compare the checksum against the value calculated on the source machine, over a trusted channel. A mismatch means the file changed in transit, or the two values got mixed up; copy it again rather than trying to patch the bundle.
A matching checksum is not proof the contents are the right ones. list-heads tells you which references are on offer, while verify tells you whether the receiving repository has the prerequisites. Read both before importing anything.
4. Import into an existing repository
Make a safety copy, or confirm the destination repository already has a working backup, before you fetch. A fetch updates references and object storage, so it changes repository state even though it will not rewrite your current branch.
DEST=/path/to/destination-repository
cd "$DEST"
git bundle verify "$BUNDLE"
git fetch "$BUNDLE" main:refs/heads/offline-main
git show-ref refs/heads/offline-main
The refspec maps the bundle's main reference onto a new local branch named offline-main. The last command should print that branch's object ID and full ref name. Inspect it before merging or switching to it:
git log --oneline --decorate -n 10 offline-main
If the destination already holds what an incremental bundle needs, the fetch goes through. If git bundle verify reports missing prerequisites, stop: fetch the earlier full bundle, or build a fresh incremental one from a commit the destination genuinely has.
5. Build an incremental bundle
For repeated transfers, keep a tag on the last commit the destination actually received. That tag must already sit at the older commit when you build the next bundle. Once the destination has accepted a transfer, the next file looks like this:
cd "$SOURCE"
git bundle create "$BUNDLE" last-offline-transfer..main
That asks Git for everything reachable from main but not from the old tag, which keeps the bundle smaller. Once the destination has successfully imported it, move the tag on the source up to the new tip:
git tag -f last-offline-transfer main
Warning
Do not move the tag before the destination accepts the bundle. Moving it early can open a gap the next bundle will not cover. On a first transfer, build a complete bundle instead, or set the tag to the destination's confirmed starting commit before making an incremental one.
Run the same checks before sending the incremental file:
git bundle list-heads "$BUNDLE"
git bundle verify "$BUNDLE"
An incremental bundle normally carries a prerequisite line. That prerequisite is not a shallow-clone boundary: it is an object the reader must already possess, which the bundle may use for reachability or delta storage. A shallow repository is not automatically a fit recipient.
Common errors and recovery
"Repository lacks these prerequisite commits"
The destination does not hold the history the bundle names. Fetch the missing earlier bundle, use a complete bundle instead, or regenerate the incremental file from a commit the destination can prove it has. Do not delete the destination's refs just to make verification look happier.
The expected branch is missing
Check the source revision and the bundle's advertised refs with git bundle list-heads. A bundle records the refs selected at creation time; a later branch update never rewrites the file. Build a replacement bundle if the source has moved on.
The fetch landed on the wrong local ref
Review the refspec before running git fetch. If you made a throwaway branch and want to remove only that ref, confirm its name first, then run:
git branch -D offline-main
Warning
That removes the local branch name, not necessarily the underlying objects straight away. It is destructive to that branch reference, so check nothing depends on it first. Re-fetch the bundle with a corrected refspec if you need to.
Done means
- Built the source bundle from the intended repository and revision.
- Confirmed the reference:
git bundle list-headsshows what you expect. - Verified on the receiving end:
git bundle verifypasses there. - Matched the checksum on the transferred file against the source value.
- Inspected the fetched reference before any merge or branch switch.
- Advanced any incremental tag only after the destination accepted the previous bundle.