Move Git History Offline with git bundle
Use git bundle to carry a whole repository, or just its latest commits, across an air gap on a single file. You will create a full bundle, verify it, clone it on the far side, then send small incremental updates after that. Allow about fifteen 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.
- 1. Check the repository and Git version
- 2. Create a self-contained bundle
- 3. Clone the full bundle on the destination
- 4. Mark the destination point for an incremental update
- 5. Create and verify an incremental bundle
- 6. Fetch the update without overwriting the working branch
- 7. Advance the transfer marker
- Common traps
The situation: two machines that cannot see each other, a USB stick, and a deadline. A bundle is an archive of Git objects and references, not a second writable remote. You can clone from one or fetch from one, but git push into a bundle is not supported.
The examples use Git 2.43.0 from the installed git and git-man packages.
1. Check the repository and Git version
These checks are read-only and need no elevated privileges. Run them in the source repository:
$ git --version
git version 2.43.0
$ git status --short
$ git branch --show-current
main
Uncommitted files do not matter, because a bundle holds committed objects and refs only. What does matter is which refs leave the machine.
Warning
Do not use --all casually. It can include remote-tracking refs and other local refs that the recipient does not need.
Checkpoint
Write down the exact source ref, such as main, and the destination path for the bundle. Keep the bundle outside the repository if it is intended for transfer or backup.
2. Create a self-contained bundle
A named ref with no exclusion gives you a self-contained bundle. It includes the reachable history and can be cloned into a new repository that has no existing Git objects:
$ git bundle create /path/to/transfer/project-full.bundle main
Replace both placeholders with real paths and refs. Progress, if Git reports any, normally goes to standard error. A successful command returns to the prompt; it does not print a success message in every terminal configuration.
Check the file before it leaves your hands:
$ test -s /path/to/transfer/project-full.bundle && echo 'bundle is non-empty'
bundle is non-empty
$ git bundle list-heads /path/to/transfer/project-full.bundle
<commit-id> refs/heads/main
$ git bundle verify /path/to/transfer/project-full.bundle
/path/to/transfer/project-full.bundle is okay
- Compare ref names, not IDs. The object ID is different in every repository, so do not copy the example output literally.
- Know what verify checks.
verifychecks the bundle format and its prerequisites against the repository in which you run it. - Expect complete history. A self-contained bundle reports a complete history.
3. Clone the full bundle on the destination
After transferring the file, clone it into a new destination directory:
$ git clone -b main /path/to/transfer/project-full.bundle /path/to/project-copy
Cloning into '/path/to/project-copy'...
done.
Check first that the destination does not contain work you need. This command creates files and Git metadata. It does not need sudo unless your chosen destination is not writable by your user.
Checkpoint
Confirm that the expected ref and history arrived.
$ git -C /path/to/project-copy log --oneline --decorate -2
<newest-commit> (HEAD -> main, origin/main) latest commit
<older-commit> earlier commit
$ git -C /path/to/project-copy remote -v
origin /path/to/transfer/project-full.bundle (fetch)
origin /path/to/transfer/project-full.bundle (push)
Tip
The displayed push line is normal remote configuration inherited from cloning, but it does not make pushing to the bundle possible. Treat the bundle remote as fetch-only. To send changes back, create a new bundle from that repository or use another supported transport.
4. Mark the destination point for an incremental update
For repeated transfers, record the last commit known to be present at the destination. A tag is easy to inspect and easy to move after each successful transfer. Run this in the source repository before later work:
$ git tag -f last-project-copy main
Updated tag 'last-project-copy' (was <old-commit>)
If the tag is new, Git may print a creation message instead. This changes only the local tag. It does not rewrite commits or alter a remote service.
Warning
Do not move the tag forward until the recipient has received and checked the corresponding bundle.
5. Create and verify an incremental bundle
After new commits land on main, use the tag as the left side of a revision range:
$ git bundle create /path/to/transfer/project-update.bundle last-project-copy..main
$ git bundle list-heads /path/to/transfer/project-update.bundle
<newest-commit> refs/heads/main
The two dots mean the bundle contains objects reachable from main but not from last-project-copy. That is usually smaller than another full bundle. It is not self-contained: the destination must already have the prerequisite history.
Verify it in the destination repository before fetching:
$ git -C /path/to/project-copy bundle verify /path/to/transfer/project-update.bundle
/path/to/transfer/project-update.bundle is okay
The bundle contains this ref:
<newest-commit> refs/heads/main
Recovery
If the destination lacks the prerequisite, verification fails with an error such as Repository lacks these prerequisite commits. Transfer the correct full bundle first, or create a new self-contained bundle. Do not ignore this failure: an incremental bundle cannot reconstruct history that is absent at the destination.
6. Fetch the update without overwriting the working branch
Fetch the bundle into a temporary local ref first, so you can inspect the result before deciding how to integrate it:
$ git -C /path/to/project-copy fetch /path/to/transfer/project-update.bundle main:refs/heads/bundle-update
From /path/to/transfer/project-update.bundle
* [new branch] main -> bundle-update
$ git -C /path/to/project-copy log --oneline main..bundle-update
<new-commit> latest transferred commit
Fetching stores the objects and creates or updates the named local ref. It does not merge anything into main. Review the commits, then choose the normal integration operation for your project. A fast-forward-only update is deliberately strict:
$ git -C /path/to/project-copy merge --ff-only bundle-update
Updating <old-commit>..<new-commit>
Fast-forward
Recovery
If the merge reports that a fast-forward is not possible, stop and inspect the branch histories. Do not force a reset or delete the temporary ref as a reflex. If you decide not to keep it, the reversible cleanup is below.
$ git -C /path/to/project-copy branch -D bundle-update
That removes only the local name. Objects may remain until Git housekeeping prunes them.
7. Advance the transfer marker
Only after the destination has verified and integrated the update should you move the source tag:
$ git tag -f last-project-copy main
If the transfer fails, leave the tag where it is and create the next update from the same marker. Moving it early can omit commits from the next bundle.
Warning
A bundle contains repository history, which may include secrets committed in the past. Keep bundle files until the recipient has a verified copy, then apply your normal retention and encryption policy.
Common traps
- Empty bundle: a range whose right-hand ref resolves to no new objects is rejected. Check the marker and
git log last-project-copy..mainbefore creating another update. - Wrong refs:
maintransfers one ref. Use more named refs when needed, or use--branches --tagsfor the refs normally obtained from a direct clone. Use--allonly when mirror-like coverage is intentional. - Unsafe overwrite: do not replace a known-good bundle while a recipient still depends on it. Write a new filename, verify it, then rotate old files according to your backup policy.
- Unexpected exposure: a bundle is portable data, not encryption. Protect it as you would the repository and use an approved encrypted transfer channel.
Done means
- Source checked. The source ref and Git version were confirmed.
- Bundle inspected. It was created, and its heads and validity were checked.
- Destination accepted it. A full bundle cloned successfully, or an incremental bundle passed verification in the destination repository.
- Review before merge. The update was fetched into a review ref before integration.
- Marker moved late. The transfer tag was advanced only after the destination accepted the update.
- Files handled. Bundle files are protected and retained or removed according to a deliberate backup policy.