Chapter 24Lesson 02~190 minutes

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.

Hands-on transferOffline bundleMirror pushArchive validation

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

Mirror push can force-update and delete remote refs. In production, verify the destination URL, authorization, source/destination ref snapshots, and rollback artifact before proceeding.
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

  1. You need a release tarball with no Git history. Which command family?
  2. You need a fully offline history transfer that can be cloned from a file. Which artifact?
  3. You need to preserve notes and custom visible refs in a local replication. Bare or mirror?
  4. You need to inspect what a mirror push would delete without changing the destination. Which option?
  5. You manually unbundle objects and see no local branches. Is that corruption?

16. Cleanup

All paths below are disposable lab data. Confirm you are inside the expected parent directory before recursive deletion.
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.

Next

Turn transfer mechanics into migration policy

Lesson 3 covers mirror/prune configuration, incremental bundle prerequisites, Git LFS and submodule dependencies, hosted metadata, offline-media controls, and environment-specific responsibility.

Authoritative references

 git-bundle
 git-clone
 git-push
 git-remote
 git-archive

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.