Git Bundle Sync — Air-Gap Repository Synchronization
Bidirectional git sync between a home and an enterprise network using a shared file system. No direct network connection between the two networks is required.
Problem & Solution
Two git repositories on separate, non-routable networks need to stay in sync.
The only common ground is a shared file system (mounted at ~/fifiletrans on
both sides by default).
Solution: git bundle — a built-in git feature that packs any range of commits
into a single portable binary file. The bundle is written to the shared folder
by one side and picked up by the other.
Home repo ──── sync-push.sh ────► ~/fifiletrans/ ◄──── sync-pull.sh ──── Office repo
Home repo ◄─── sync-pull.sh ──── ~/fifiletrans/ ◄──── sync-push.sh ──── Office repo
Network context:
- Home network has a Gitea instance — used as a local mirror/backup
- Office network has a GitLab instance — no direct sync between the two is possible
- Bundle files via shared filesystem are the only transport between the two networks
Scripts Overview
| Script | Purpose |
|---|---|
sync-push.sh |
Export new commits as a bundle into the shared folder |
sync-pull.sh |
Import a bundle from the shared folder and integrate it |
sync-init-export.sh |
One-time: create full bootstrap bundle + copy scripts for new machine |
sync-init-import.sh |
One-time: clone from bootstrap bundle into a brand-new directory |
gitea-setup.sh |
Initialize a local folder (git or not) and push it to Gitea |
Initial Setup of a New Machine
Use this when setting up the repo on a machine for the first time (no existing directory, no git history).
On the source machine
cd /path/to/your/repo
./sync-init-export.sh
This places in ~/fifiletrans/:
<repo>-INIT-from-<host>-<timestamp>.bundle— complete git historysync-push.sh,sync-pull.sh,sync-init-import.sh— scripts for the other sideINIT-HOWTO-<repo>.txt— printed instructions
On the receiving machine
bash ~/fifiletrans/sync-init-import.sh ~/fifiletrans /path/to/new/repo
This will:
- Clone the full bundle into
/path/to/new/repo(directory must not exist) - Check out all branches
- Install
sync-push.shandsync-pull.shinto the new repo - Set export checkpoints so the first
sync-pushis incremental
After this, both machines are in sync and ready for the ongoing workflow.
Ongoing Sync Workflow
Send changes (on either machine)
cd /path/to/your/repo
./sync-push.sh
Creates an incremental bundle (only commits since last push). First run after init creates a full bundle. Output example:
Repository : myrepo
Host : homepc
Branches : main
Mode : incremental
New commits: 3
main: 3 new commit(s)
a1b2c3d Add feature X
e4f5g6h Fix bug in Y
i7j8k9l Refactor Z
Bundle created : /home/user/fifiletrans/myrepo-from-homepc-20260221-143022.bundle (12K)
Receive changes (on the other machine)
cd /path/to/your/repo
./sync-pull.sh
The script:
- Lists all available bundles for this repo in the shared folder
- Defaults to the newest one (press Enter to accept)
- Verifies bundle integrity
- Shows exactly which commits are incoming and whether branches have diverged
- Offers fast-forward, merge, rebase, or skip per branch
Typical round-trip
# ── At home, after finishing work ──────────────────────────────────────────
cd ~/projects/myrepo
git commit -m "..."
./sync-push.sh
# → writes ~/fifiletrans/myrepo-from-homepc-TIMESTAMP.bundle
# ── At office, pick up home changes ────────────────────────────────────────
cd ~/projects/myrepo
./sync-pull.sh
# → lists bundles, shows new commits, fast-forwards main
# ── At office, continue working ────────────────────────────────────────────
git commit -m "..."
./sync-push.sh
# → writes ~/fifiletrans/myrepo-from-officepc-TIMESTAMP.bundle
# ── At home, pick up office changes ────────────────────────────────────────
./sync-pull.sh
Gitea Integration (Home Network)
gitea-setup.sh registers a local repository with the home Gitea instance.
Works for both cases:
- Folder already a git repo — creates Gitea repo and pushes
- Folder not yet versioned — runs
git init, stages all files, commits, then pushes
Prerequisites
# Install tea (Gitea CLI) if not already present
# See https://gitea.com/gitea/tea/releases
# Configure your Gitea login (once per machine)
tea login add
Usage
./gitea-setup.sh [options] [<repo-path>]
Options:
-n, --name NAME Gitea repo name (default: directory name)
-d, --desc DESC Repository description
-o, --org ORG Create under organization
-p, --private Create as private repo
-l, --login LOGIN tea login profile (default: active login)
--auto-push Enable Gitea auto-push after sync-push.sh
--no-auto-push Disable Gitea auto-push (default)
Example
# New, unversioned project
./gitea-setup.sh --name my-project --private ~/projects/my-project
# Existing git repo, enable auto-push to Gitea
cd ~/projects/existing-repo
./gitea-setup.sh --auto-push
Gitea Auto-Push
When enabled, every sync-push.sh run also pushes all branches and tags to the
Gitea remote automatically.
# Enable
git config sync.gitea.autopush true
# Disable
git config sync.gitea.autopush false
# Check status
git config sync.gitea.autopush
The Gitea remote name defaults to gitea. Set a different one with:
git config sync.gitea.remote <remote-name>
Configuration Reference
All settings are stored in the repo's local git config (.git/config).
| Key | Set by | Purpose |
|---|---|---|
sync.exported.<branch> |
sync-push.sh |
Last exported commit hash per branch |
sync.gitea.autopush |
gitea-setup.sh |
true/false — auto-push to Gitea after bundle export |
sync.gitea.remote |
gitea-setup.sh |
Name of the Gitea git remote (default: gitea) |
Bundle Filename Convention
<repo-name>-from-<hostname>-<YYYYMMDD-HHMMSS>.bundle ← ongoing sync
<repo-name>-INIT-from-<hostname>-<YYYYMMDD-HHMMSS>.bundle ← initial bootstrap
The pull scripts filter by <repo-name> automatically, so multiple repos can
safely share the same folder.
Multiple Branches
All scripts handle multiple branches automatically:
sync-push.sh/sync-init-export.sh— export all local branches; incremental checkpoint is tracked independently per branchsync-pull.sh/sync-init-import.sh— import all branches from the bundle and prompt for integration of each one individually
Important Notes
Apply incremental bundles in order. An incremental bundle depends on commits already present in the receiving repo. If you skip a bundle, the next pull will fail verification. Apply bundles in chronological order (oldest timestamp first) when catching up after a gap.
Checkpoints are local and per-machine.
git config sync.exported.<branch> tracks what this machine has sent. It is
not shared and not affected by imports.
Incoming refs persist.
Fetched commits are stored under refs/sync/incoming/<branch> and remain
available between pull runs. Inspect them anytime:
git log refs/sync/incoming/main
git diff main refs/sync/incoming/main
Conflict resolution. When branches have diverged and you choose merge or rebase, the script hands off to standard git. Resolve conflicts normally, then:
git rebase --continue # or
git merge --continue
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "Bundle verification failed" | Incremental bundle missing prerequisites | Apply missing earlier bundles first, or force full re-export (see below) |
| "Nothing new to export" | No commits since last push on any branch | Nothing to do |
| No bundles listed by sync-pull | Bundle filename mismatch (repo dir name differs) | Check repo directory name matches on both machines |
| Gitea push fails | Remote URL wrong or auth issue | git remote -v and tea login list to verify |
tea: command not found |
Gitea CLI not installed | Install from https://gitea.com/gitea/tea/releases |
| Want to reset one branch checkpoint | — | git config --unset sync.exported.<branch> |
| Want to force a full re-export | — | git config --remove-section sync.exported |
| Skipped integration, want to redo | — | Run ./sync-pull.sh again and pick the same bundle |