Move Git History Offline with Full and Incremental Bundles
You will create a Git bundle on one machine, check that it is usable, move it by a removable drive or another approved channel, and fetch it into a second repository. You will also have a smaller incremental bundle for later updates. This is useful when the two machines cannot reach each other directly. Allow about fifteen minutes for a small repository, plus the time needed to transfer the file.
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 examples use Git 2.43.0 from the installed git and git-man packages. 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.
1. Check the repository and Git version
Run these ordinary, read-only checks in the source repository. No elevated privileges are needed:
$ git --version
git version 2.43.0
$ git status --short
$ git branch --show-current
main
The working tree may contain uncommitted files, but a bundle contains committed objects and refs. Decide which branch or refs are meant to leave the machine. 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 without an exclusion creates a self-contained bundle. It includes the reachable history and can be cloned into a new repository without any existing Git objects:
$ git bundle create /path/to/transfer/project-full.bundle main
Replace both placeholders with real paths and refs. If Git reports progress, it normally writes that progress 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 moving it:
$ 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
The object ID is different in every repository, so compare the ref name rather than copying the example output literally. verify checks the bundle format and its prerequisites against the repository in which you run it. 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.
The destination is a new directory, so check first that it 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)
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. If you need 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 can be moved 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. 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 that the bundle contains objects reachable from main but not from last-project-copy. This 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
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. This lets you 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 them into main. Review the commits, then choose the normal integration operation for your project. For example, 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
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:
$ 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. Keep bundle files until the recipient has a verified copy, then apply your normal retention and encryption policy. A bundle contains repository history, which may include secrets that were committed in the past.
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
- The source ref and Git version were checked.
- A bundle was created and its heads and validity were inspected.
- A full bundle cloned successfully, or an incremental bundle passed verification in the destination repository.
- The update was fetched into a review ref before integration.
- The transfer tag was advanced only after the destination accepted the update.
- Bundle files are protected and retained or removed according to a deliberate backup policy.