Bundles, Mirrors, Repository Migration, Archival, and Offline Transfer: Guided Hands-On Workflow and Core Operations
Build a disposable source with multiple ref namespaces, create and verify a full bundle, clone it offline, contrast bare/mirror clones, perform dry-run-first local mirror updates, and prove git archive is source-only.
Learning objectives
- Create and inspect a self-contained full bundle using visible refs and immutable commit IDs.
- Clone from a bundle offline and explain why ordinary clone/ref mapping differs from a mirror.
- Demonstrate unbundle as object plumbing without automatic local-ref creation.
- Perform a guarded mirror migration only after destination/ref inspection and dry run.
- Validate source/destination refs and distinguish repository transfer from source archive export.
1. Create a fully disposable migration source
mkdir git-transfer-lab
cd git-transfer-lab
git init -b trunk source
cd source
git config user.name "Transfer Lab"
git config user.email "transfer-lab@example.invalid"
printf "service=payments\nversion=1\n" > service.conf
git add service.conf
git commit -m "seed service"
printf "runbook=v1\n" > RUNBOOK.md
git add RUNBOOK.md
git commit -m "add runbook"
git switch -c feature
printf "feature=offline-transfer\n" > feature.conf
git add feature.conf
git commit -m "add offline transfer feature"
git switch trunk
git tag -a v1.0 -m "migration lab release" HEAD~1
git notes add -m "validated by transfer lab" HEAD
git update-ref refs/meta/release-candidate HEAD
git update-ref refs/remotes/legacy/integration HEAD~1
The last two commands create harmless extra namespaces so you can see exactly what ordinary bare cloning and mirror cloning preserve. All state exists only inside the disposable lab.
2. Record the source ref/object baseline
git status --short --branch
git for-each-ref --sort=refname --format='%(refname) %(objectname)'
git rev-list --count --all
git fsck --full
TRUNK_OID=$(git rev-parse refs/heads/trunk)
FEATURE_OID=$(git rev-parse refs/heads/feature)
printf "trunk=%s\nfeature=%s\n" "$TRUNK_OID" "$FEATURE_OID"
Save the OIDs. Representation may change during packing/transfer; these source history identities should not.
3. Produce a self-contained full-ref bundle
git bundle create ../full.bundle --all
git bundle list-heads ../full.bundle
git bundle verify ../full.bundle
Expected: the bundle lists branch, tag, note, custom, and legacy
remote-tracking refs selected by --all. Verification
should say the bundle records complete history and report the
repository hash algorithm without requiring you to assume a fixed
OID length.
4. Clone the bundle completely offline
cd ..
git clone --mirror full.bundle offline-mirror.git
git -C offline-mirror.git for-each-ref --sort=refname --format='%(refname) %(objectname)'
git -C offline-mirror.git fsck --full
test "$(git -C offline-mirror.git rev-parse refs/heads/trunk)" = "$TRUNK_OID"
test "$(git -C offline-mirror.git rev-parse refs/heads/feature)" = "$FEATURE_OID"
No network transport is involved: the bundle file acts as the
readable source. Using --mirror here is deliberate
because the lesson wants to preserve the complete visible ref
namespace, not only a normal developer clone mapping.
5. Compare with an ordinary clone from the same bundle
git clone -b trunk full.bundle offline-working
git -C offline-working for-each-ref --sort=refname --format='%(refname) %(objectname)'
Expected: the ordinary clone creates a checked-out
trunk, remote-tracking branches for ordinary heads, and
reachable tags. It does not automatically reproduce every custom
namespace as a same-named local ref. The bundle can contain more
refs than a normal clone chooses to map.
6. Inspect unbundle as plumbing
git init --bare -b trunk unpacked.git
git -C unpacked.git bundle verify ../full.bundle
git -C unpacked.git bundle unbundle ../full.bundle
git -C unpacked.git for-each-ref --sort=refname --format='%(refname) %(objectname)'
git -C unpacked.git fsck --full
The unbundle command stores objects and prints the refs
carried by the bundle. Direct use does not update local refs, so the
final for-each-ref can be empty. That behavior is why
normal workflows use clone or fetch to
combine object transfer with an explicit ref mapping.
7. Contrast a bare clone with a mirror clone
git clone --bare source bare-copy.git
git clone --mirror source mirror-copy.git
printf '%s\n' '--- bare refs ---'
git -C bare-copy.git for-each-ref --sort=refname --format='%(refname)'
printf '%s\n' '--- mirror refs ---'
git -C mirror-copy.git for-each-ref --sort=refname --format='%(refname)'
git -C mirror-copy.git config --get-all remote.origin.fetch
git -C mirror-copy.git config --get remote.origin.mirror
Expected: the bare clone contains ordinary branch heads and tags;
the mirror also carries visible notes/custom/ref namespaces from
this local source. The mirror fetch refspec should cover
refs/*.
8. Create an empty disposable destination for a controlled mirror push
git init --bare -b trunk destination.git
git -C mirror-copy.git remote add --mirror=push destination ../destination.git
git -C mirror-copy.git remote -v
git -C mirror-copy.git for-each-ref --sort=refname --format='%(refname) %(objectname)'
git -C destination.git for-each-ref --sort=refname --format='%(refname) %(objectname)'
Preflight proves the destination is the intended empty lab repository before a dangerous mirror operation is even previewed.
9. Dry-run the mirror push before sending anything
git -C mirror-copy.git push --mirror --dry-run --porcelain destination
Read every proposed ref update. On this empty destination, all entries should be creations rather than deletions.
10. Execute mirror push only because both repositories are disposable
git -C mirror-copy.git push --mirror destination
git -C destination.git for-each-ref --sort=refname --format='%(refname) %(objectname)'
git -C destination.git fsck --full
test "$(git -C destination.git rev-parse refs/heads/trunk)" = "$TRUNK_OID"
test "$(git -C destination.git rev-parse refs/heads/feature)" = "$FEATURE_OID"
The destination should now have the mirror's refs. This is a local demonstration of a potentially destructive production operation.
11. Simulate a final migration delta
cd source
printf "version=2\n" >> service.conf
git add service.conf
git commit -m "final migration delta"
git tag final-cutover
git notes add -m "final migration validation" HEAD
FINAL_OID=$(git rev-parse HEAD)
cd ..
This models changes made after the initial mirror snapshot but before a production write freeze.
12. Refresh the mirror, inspect, then update the destination
git -C mirror-copy.git remote update --prune origin
git -C mirror-copy.git for-each-ref --sort=refname --format='%(refname) %(objectname)'
git -C mirror-copy.git push --mirror --dry-run --porcelain destination
git -C mirror-copy.git push --mirror destination
test "$(git -C destination.git rev-parse refs/heads/trunk)" = "$FINAL_OID"
git -C destination.git show-ref --verify refs/tags/final-cutover
git -C destination.git fsck --full
--prune matters for a true mirror refresh because refs
deleted at the source should not linger locally. It also increases
deletion risk: inspect the ref snapshot after refresh and before
pushing.
13. Export a source archive and prove its purpose is different
git -C source archive --format=tar --prefix=service-release/ \
-o ../service-release.tar refs/heads/trunk
mkdir archive-check
tar -xf service-release.tar -C archive-check
find archive-check -maxdepth 2 -type f -print
test ! -d archive-check/service-release/.git
Now try a Git query:
git -C archive-check/service-release rev-parse --git-dir
echo "exit=$?"
Expected: the Git query fails because the exported directory is source material, not a repository.
14. Validate source and destination refs, commits, and representative objects
git -C source for-each-ref --sort=refname \
--format='%(refname) %(objectname)' > source-refs.txt
git -C destination.git for-each-ref --sort=refname \
--format='%(refname) %(objectname)' > destination-refs.txt
git diff --no-index -- source-refs.txt destination-refs.txt
echo "ref comparison exit=$?"
git -C source rev-list --count --all
git -C destination.git rev-list --count --all
git -C source cat-file -t "$FINAL_OID"
git -C destination.git cat-file -t "$FINAL_OID"
A zero diff --no-index exit means the visible ref
snapshots match exactly. Commit counts and representative object
checks add independent evidence.
15. Challenge — choose the transfer primitive
- You need a release tarball with no Git history. Which command family?
- You need a fully offline history transfer that can be cloned from a file. Which artifact?
- You need to preserve notes and custom visible refs in a local replication. Bare or mirror?
- You need to inspect what a mirror push would delete without changing the destination. Which option?
- You manually unbundle objects and see no local branches. Is that corruption?
16. Cleanup
cd ..
pwd
rm -rf git-transfer-lab
PowerShell equivalent:
Set-Location ..; Remove-Item -Recurse -Force
git-transfer-lab.
17. Knowledge check
Question 1. Why was
push --mirror --dry-run mandatory before the real
push?
Question 2. What did direct
git bundle unbundle change?
Question 3. Why did the ordinary clone and mirror clone have different ref sets?
Question 4. What invariant mattered more than packfile names?
Question 5. What did the archive test prove?
18. Summary
You created a full bundle, cloned it offline, inspected plumbing unbundle behavior, compared bare and mirror refs, performed a dry-run-first local mirror migration, applied a final delta, exported a source archive, and validated refs and immutable commit identities.
Authoritative references
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.