Skip to content

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.

[+]<source-ref>:<destination-ref>

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:

git checkout origin/pr/42

GitLab merge requests

GitLab stores merge requests under refs/merge-requests/:

[remote "origin"]
    fetch = +refs/merge-requests/*/head:refs/remotes/origin/mr/*

One-off fetch command

To fetch a specific pull request on demand without modifying .git/config:

git fetch origin refs/pull/42/head:pr-42
git checkout pr-42

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:

git clone --mirror git@git.local.sneakysquid.xyz:hermes/app.git app-mirror.git
cd app-mirror.git

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 backup

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:

git fetch --prune origin

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.