Git refspecs, multi-remote synchronization, and repository mirroring¶
Git refspecs define the mapping between local references and remote references. They control how git fetch and git push translate branches, tags, and internal object pointers across repositories. In DevOps infrastructure, refspecs enable multi-forge repository mirroring, automated pull request validation in CI pipelines, and disaster recovery replication.
1. Refspec architecture¶
A refspec is a colon-separated string that instructs Git how to map a source reference on the left-hand side to a destination reference on the right-hand side.
The components operate as follows:
+(optional): Forces Git to update the destination reference even if the change is not a fast-forward merge.<source-ref>: The reference pattern on the source repository.<destination-ref>: The reference pattern on the destination repository.
During git fetch, <source-ref> is on the remote server and <destination-ref> is in the local repository. During git push, <source-ref> is in the local repository and <destination-ref> is on the remote server.
Default fetch refspec¶
When you clone a repository, Git writes a standard fetch refspec in .git/config:
[remote "origin"]
url = https://git.local.sneakysquid.xyz/hermes/git-master.git
fetch = +refs/heads/*:refs/remotes/origin/*
This instructs git fetch to download every branch under refs/heads/ on the remote server and store it locally under refs/remotes/origin/.
2. Advanced refspec patterns¶
2.1 Fetching pull requests and merge requests¶
Continuous integration runners and local workstations can inspect pull requests directly without adding separate developer remotes.
GitHub, Forgejo, and Gitea pull requests¶
Forgejo, Gitea, and GitHub store open pull requests under the refs/pull/ namespace on the server:
# Append to .git/config under [remote "origin"]
[remote "origin"]
url = https://git.local.sneakysquid.xyz/hermes/git-master.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/pull/*/head:refs/remotes/origin/pr/*
Run git fetch origin. Git maps pull request 42 on the server (refs/pull/42/head) to local tracking ref refs/remotes/origin/pr/42.
To check out and test the pull request locally:
GitLab merge requests¶
GitLab stores merge requests under refs/merge-requests/:
One-off fetch command¶
To fetch a specific pull request on demand without modifying .git/config:
2.2 Negative refspecs (Git 2.29+)¶
Negative refspecs exclude specific patterns from wildcard fetches. They begin with a caret (^).
[remote "origin"]
url = git@github.com:org/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = ^refs/heads/temp/*
fetch = ^refs/heads/wip-*
Git fetches all branches except those starting with temp/ or wip-. This reduces transfer overhead in CI environments.
3. Multi-remote topologies¶
Enterprise setups often maintain a primary internal forge (such as Forgejo or GitLab) and a secondary external forge (such as GitHub) for disaster recovery or open-source distribution.
3.1 Dual-push configuration¶
You can configure Git to push to multiple remotes with a single git push command by defining multiple pushurl entries under one remote name:
[remote "origin"]
url = git@git.local.sneakysquid.xyz:hermes/app.git
fetch = +refs/heads/*:refs/remotes/origin/*
pushurl = git@git.local.sneakysquid.xyz:hermes/app.git
pushurl = git@github.com:sneakysquid/app.git
When you execute git push origin master, Git transmits the commits to both the internal Forgejo instance and GitHub sequentially.
# Add push URLs via CLI
git config --add remote.origin.pushurl git@git.local.sneakysquid.xyz:hermes/app.git
git config --add remote.origin.pushurl git@github.com:sneakysquid/app.git
3.2 Explicit upstream and mirror remotes¶
For automated CI pipelines, separate the remotes explicitly to avoid accidental push loops:
git remote add origin git@git.local.sneakysquid.xyz:hermes/app.git
git remote add backup git@github.com:sneakysquid/app.git
4. Repository mirroring and disaster recovery¶
Mirroring requires exact state synchronization across repositories. This includes branches, tags, notes, and deleted references.
4.1 Bare mirror clones¶
To create an exact mirror replica on a CI runner or migration server:
A --mirror clone configures Git to map all references verbatim:
[remote "origin"]
url = git@git.local.sneakysquid.xyz:hermes/app.git
fetch = +refs/*:refs/*
mirror = true
4.2 Safe mirror push synchronization¶
To synchronize the mirror repository to a backup destination:
git push --mirror forces updates on all references and deletes remote branches that do not exist locally.
4.3 Atomic synchronization for active repositories¶
For live CI synchronization without a bare mirror clone, use explicit refspecs combined with --prune and --atomic:
# Fetch latest state from primary
git fetch --prune --tags origin '+refs/heads/*:refs/remotes/origin/*'
# Push branches and tags atomically to secondary
git push --prune --atomic backup \
'+refs/remotes/origin/*:refs/heads/*' \
'+refs/tags/*:refs/tags/*'
The --atomic flag ensures all reference updates succeed together on the remote server. If a single reference fails due to a permission check or branch protection rule, the entire push transaction rolls back.
5. Git LFS synchronization across mirrors¶
Standard Git fetch and push commands transfer only Git LFS pointer files, not the underlying binary payloads. If you mirror a repository without syncing LFS objects, checkouts on the secondary mirror will fail when downloading binary assets.
To synchronize all LFS payloads across remotes:
# 1. Download all LFS objects from origin across all branches
git lfs fetch --all origin
# 2. Push all LFS objects to the backup mirror
git lfs push --all backup
6. Automated CI mirroring script¶
Create an automated synchronization job in your CI/CD runner. Use file locks to prevent overlapping sync executions.
#!/usr/bin/env bash
set -euo pipefail
LOCK_FILE="/tmp/git-mirror-sync.lock"
PRIMARY_URL="https://git.local.sneakysquid.xyz/hermes/git-master.git"
BACKUP_URL="https://github.com/sneakysquid/git-master-backup.git"
WORK_DIR="/var/cache/git-mirrors/git-master.git"
exec 200>"$LOCK_FILE"
flock -n 200 || { echo "Sync already running. Exiting."; exit 0; }
if [ ! -d "$WORK_DIR" ]; then
echo "Initializing bare mirror clone..."
git clone --mirror "$PRIMARY_URL" "$WORK_DIR"
cd "$WORK_DIR"
git remote add backup "$BACKUP_URL"
else
cd "$WORK_DIR"
fi
echo "Fetching all updates from primary..."
git fetch --prune origin '+refs/*:refs/*'
echo "Pushing exact mirror to backup..."
git push --prune --atomic backup '+refs/*:refs/*'
echo "Mirror synchronization complete."
7. Common failure modes¶
Missing references after branch deletion¶
If developers delete branches on the primary server, a standard git fetch retains local tracking branches. Always include the --prune flag when fetching:
Tag overwrite collisions¶
If a tag is recreated pointing to a new commit on the source, non-force pushes will reject the update. Use +refs/tags/*:refs/tags/* in your mirror refspec to enforce parity.
Protected branch rejections on mirrors¶
Secondary mirrors with branch protection rules will reject force-pushes and deletions. Configure repository settings on the backup forge to allow the CI service account admin push privileges without branch rules blocking synchronization.