Git 仓库主分支镜像同步(保留 Commit 历史)
最近需要将一个 Git 仓库 A 的 master 分支定期同步到另一个 GitLab 仓库 B,并且要求:
- 保留 A 的所有 commit 历史
- B 最终以 A 为准
- 后续支持定期同步
最终整理了一套稳定可复用的方案。
场景
源仓库 A:
ssh://git@example.com:10022/group/source-repo.git
目标仓库 B:
http://gitlab.example.com/group/target-repo.git
目标:
A/master -> B/master
并且:
- 保留所有 commit
- 后续支持自动同步
为什么不使用 git push –mirror
一开始尝试:
git push --mirror target
会遇到:
deny updating a hidden ref
因为 GitLab 内部存在:
refs/merge-requests/*
这些隐藏 refs 不允许推送。
另外:
master -> master (forced update)
说明:
A/master 与 B/master 历史不一致
因此:
- 普通 push 会失败
- 需要 force push
- 但只应该同步 master
- 不应该 mirror 全部 refs
推荐方案
核心思路:
使用 mirror clone 保存 A
仅 force push master 到 B
第一次初始化
1. 创建同步目录
mkdir -p ~/git-sync-test
cd ~/git-sync-test
2. mirror clone 源仓库
git clone --mirror \
ssh://git@example.com:10022/group/source-repo.git
进入目录:
cd source-repo.git
3. 添加目标仓库
git remote add target \
http://gitlab.example.com/group/target-repo.git
4. 获取远端信息
git fetch origin
git fetch target master
5. Dry Run 验证
git push \
--force-with-lease \
--dry-run \
target \
refs/heads/master:refs/heads/master
如果看到:
master -> master (forced update)
说明:
B/master 将被 A/master 覆盖
6. 正式同步
git push \
--force-with-lease \
target \
refs/heads/master:refs/heads/master
注意事项
1. B 的 master 可能需要允许 force push
如果出现:
You are not allowed to force push code to a protected branch
需要在 GitLab:
Settings
-> Repository
-> Protected branches
临时:
- Unprotect master
- 或允许 force push
2. B 最好不要再直接开发
因为:
B 会被 A 周期性覆盖
如果 B 上继续开发:
- 会再次产生历史分叉
- 后续同步还会需要 force push
因此:
B 更适合作为镜像仓库
后续定期同步
后续只需要:
cd ~/git-sync-test/source-repo.git
git fetch origin
git fetch target master
git push \
--force-with-lease \
target \
refs/heads/master:refs/heads/master
自动同步脚本
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="/path/to/git-mirror"
REPO_DIR="${BASE_DIR}/source-repo.git"
SOURCE_REMOTE="origin"
TARGET_REMOTE="target"
BRANCH="master"
cd "$REPO_DIR"
echo "[INFO] Fetch latest from ${SOURCE_REMOTE}..."
git fetch "$SOURCE_REMOTE"
echo "[INFO] Fetch latest from ${TARGET_REMOTE}/${BRANCH}..."
git fetch "$TARGET_REMOTE" "$BRANCH"
echo "[INFO] Push ${SOURCE_REMOTE}/${BRANCH} to ${TARGET_REMOTE}/${BRANCH}..."
git push --force-with-lease \
"$TARGET_REMOTE" \
"refs/heads/${BRANCH}:refs/heads/${BRANCH}"
echo "[INFO] Verifying synchronization..."
LOCAL_HASH=$(git rev-parse "refs/heads/${BRANCH}")
REMOTE_HASH=$(git ls-remote "$TARGET_REMOTE" "refs/heads/${BRANCH}" | awk '{print $1}')
echo "[INFO] Local ${BRANCH}: ${LOCAL_HASH}"
echo "[INFO] Remote ${BRANCH}: ${REMOTE_HASH}"
if [[ "$LOCAL_HASH" == "$REMOTE_HASH" ]]; then
echo "[INFO] Sync verification succeeded."
else
echo "[ERROR] Sync verification failed!"
exit 1
fi
cd "$BASE_DIR"
echo "[INFO] Done."
验证同步是否成功
本地:
git rev-parse refs/heads/master
远端:
git ls-remote target refs/heads/master
两个 commit hash 一致即可。
总结
最终方案:
mirror clone A
↓
fetch A
↓
fetch B/master
↓
force-with-lease push A/master -> B/master
↓
compare commit hash
这个方案适合:
- GitLab 仓库镜像
- 内网代码同步
- 多环境仓库同步
- 主仓库 -> 只读仓库
同时:
- 保留完整 commit 历史
- 避免 merge request refs 问题
- 支持自动化定期同步
