Git 服务器搭建完全指南 / 第 12 章 - 仓库迁移
第 12 章 - 仓库迁移
从一个 Git 平台迁移到另一个平台是常见需求。本章涵盖迁移策略、工具使用、历史清理和 LFS 数据处理。
12.1 迁移策略概述
12.1.1 迁移类型
| 类型 | 内容 | 复杂度 | 停机时间 |
|---|---|---|---|
| 纯 Git 数据 | 分支、标签、提交历史 | 低 | 分钟级 |
| Git + 元数据 | Issues、PR、Wiki、标签 | 中 | 小时级 |
| 完整迁移 | Git + 元数据 + CI/CD + 用户 | 高 | 天级 |
12.1.2 迁移决策树
需要迁移元数据(Issues/PR)?
├── 否 → 直接 Git 克隆/推送
└── 是 → 平台内置迁移功能?
├── 是 → 使用平台迁移 API(Gitea/GitLab)
└── 否 → 需要保持历史关联?
├── 是 → 使用第三方工具(如 git-subtree-filter)
└── 否 → 导出 CSV + API 重建
12.2 Git 数据迁移
12.2.1 简单迁移(纯代码)
# 方法一:直接克隆并推送
git clone --mirror https://github.com/old-org/project.git
cd project.git
git remote set-url origin git@new-server:/opt/git/project.git
git push --mirror
# 方法二:使用 git bundle(适合离线迁移)
# 在源服务器上创建 bundle
cd /opt/git/project.git
git bundle create /tmp/project.bundle --all
# 在目标服务器上恢复
git clone /tmp/project.bundle project.git
# 或在裸仓库中
git init --bare /opt/git/project.git
cd /opt/git/project.git
git bundle unbundle /tmp/project.bundle
12.2.2 批量迁移脚本
#!/bin/bash
# batch-migrate.sh
set -euo pipefail
SOURCE_URL="https://github.com"
SOURCE_ORG="old-org"
SOURCE_TOKEN="ghp_xxxxxxxxxxxx"
TARGET_URL="git@new-server"
TARGET_ORG="new-org"
LOG_FILE="/var/log/git-migration.log"
mkdir -p /tmp/migration-workdir
cd /tmp/migration-workdir
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG_FILE"
}
# 获取仓库列表
log "Fetching repository list from ${SOURCE_ORG}..."
repos=$(curl -s -H "Authorization: token $SOURCE_TOKEN" \
"https://api.github.com/orgs/${SOURCE_ORG}/repos?per_page=100&type=all" | \
jq -r '.[].name')
total=$(echo "$repos" | wc -l)
log "Found $total repositories"
current=0
for repo in $repos; do
current=$((current + 1))
log "[$current/$total] Migrating: $repo"
# 克隆(mirror 模式包含所有分支和标签)
if git clone --mirror "${SOURCE_URL}/${SOURCE_ORG}/${repo}.git" "${repo}.git" 2>> "$LOG_FILE"; then
cd "${repo}.git"
# 修改远程地址
git remote set-url origin "${TARGET_URL}:${TARGET_ORG}/${repo}.git"
# 推送所有引用
if git push --mirror 2>> "$LOG_FILE"; then
log " ✅ SUCCESS: $repo"
else
log " ❌ PUSH FAILED: $repo"
fi
cd ..
else
log " ❌ CLONE FAILED: $repo"
fi
# 清理
rm -rf "${repo}.git"
done
log "Migration completed: $current/$total"
12.2.3 使用 GitHub API 获取仓库列表
# 获取个人仓库
curl -s -H "Authorization: token $TOKEN" \
"https://api.github.com/user/repos?per_page=100&type=all&sort=updated" | \
jq -r '.[] | "\(.full_name) \(.private) \(.default_branch)"'
# 获取组织仓库(分页)
page=1
while true; do
repos=$(curl -s -H "Authorization: token $TOKEN" \
"https://api.github.com/orgs/${ORG}/repos?per_page=100&page=$page" | \
jq -r '.[].name')
if [ -z "$repos" ]; then
break
fi
echo "$repos"
page=$((page + 1))
done
12.3 元数据迁移
12.3.1 Gitea 平台内置迁移
Gitea 支持从 GitHub、GitLab、Gogs、Gitea 等平台迁移完整数据。
# API 迁移
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"$GITEA_URL/api/v1/repos/migrate" \
-d '{
"clone_addr": "https://github.com/owner/repo.git",
"repo_name": "repo",
"repo_owner": "new-org",
"service": "github",
"auth_token": "ghp_xxxxxxxxxxxx",
"issues": true,
"issue_labels": true,
"milestones": true,
"pull_requests": true,
"releases": true,
"wiki": true,
"labels": true,
"milestones": true,
"mirror": false,
"private": true
}'
12.3.2 批量 API 迁移
#!/bin/bash
# batch-api-migrate.sh
GITEA_URL="https://git.example.com"
GITEA_TOKEN="your_gitea_token"
GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
GITHUB_ORG="old-org"
GITEA_ORG="new-org"
# 获取 GitHub 仓库
repos=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/orgs/${GITHUB_ORG}/repos?per_page=100" | \
jq -r '.[] | select(.archived==false) | .name')
for repo in $repos; do
echo "Migrating $repo..."
result=$(curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"$GITEA_URL/api/v1/repos/migrate" \
-d "{
\"clone_addr\": \"https://github.com/${GITHUB_ORG}/${repo}.git\",
\"repo_name\": \"${repo}\",
\"repo_owner\": \"${GITEA_ORG}\",
\"service\": \"github\",
\"auth_token\": \"${GITHUB_TOKEN}\",
\"issues\": true,
\"pull_requests\": true,
\"labels\": true,
\"milestones\": true,
\"releases\": true,
\"wiki\": true,
\"private\": true
}")
status=$(echo "$result" | jq -r '.full_name // .message // "unknown"')
echo " Result: $status"
# 避免速率限制
sleep 5
done
12.3.3 GitLab 迁移
GitLab 通过 UI 和 API 迁移:
# GitLab Import API
curl -s -X POST -H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
-H "Content-Type: application/json" \
"$GITLAB_URL/api/v4/import/github" \
-d '{
"personal_access_token": "ghp_xxxxxxxxxxxx",
"repo_id": 123456,
"target_namespace": "engineering",
"new_name": "project-name"
}'
12.4 仓库历史清理
12.4.1 使用 git filter-repo(推荐)
# 安装 git-filter-repo
pip install git-filter-repo
# 克隆仓库(必须是新鲜克隆)
git clone --mirror https://github.com/org/repo.git
cd repo.git
# 删除大文件
git filter-repo --strip-blobs-bigger-than 10M
# 删除特定文件
git filter-repo --path secrets.yaml --invert-paths
git filter-repo --path-glob '*.env' --invert-paths
# 删除特定路径
git filter-repo --path node_modules/ --invert-paths
git filter-repo --path vendor/ --invert-paths
# 替换敏感信息
git filter-repo --replace-text expressions.txt
# expressions.txt 格式:
# old_password==>REDACTED
# api_key_abc123==>REDACTED
# 推送清理后的仓库
git push --mirror origin
12.4.2 使用 BFG Repo-Cleaner
# 下载 BFG
wget https://rtyley.github.io/bfg-repo-cleaner/releases/bfg-1.14.0.jar
# 克隆仓库
git clone --mirror https://github.com/org/repo.git
# 删除大文件
java -jar bfg-1.14.0.jar --strip-blobs-bigger-than 50M repo.git
# 删除特定文件
java -jar bfg-1.14.0.jar --delete-files "*.env" repo.git
java -jar bfg-1.14.0.jar --delete-files "secrets.yaml" repo.git
# 替换敏感信息
echo "password123==>REDACTED" > passwords.txt
java -jar bfg-1.14.0.jar --replace-text passwords.txt repo.git
# 清理和推送
cd repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --mirror
12.4.3 清理前后对比
# 检查仓库大小
du -sh repo.git
# 检查大文件
git rev-list --objects --all | \
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
sed -n 's/^blob //p' | \
sort -rnk2 | \
head -20
# 使用 git-sizer
git-sizer --verbose
12.5 Git LFS 迁移
12.5.1 迁移 LFS 数据
# 安装 Git LFS
git lfs install
# 克隆仓库(包含 LFS 数据)
GIT_LFS_SKIP_SMUDGE=1 git clone https://github.com/org/repo.git
cd repo
# 拉取所有 LFS 对象
git lfs fetch --all
# 验证 LFS 文件
git lfs ls-files
# 迁移 LFS 跟踪规则(如果有变更)
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "*.bin"
# 推送到新服务器
git remote add new-server git@new-server:/opt/git/repo.git
git push new-server --all
git push new-server --tags
git lfs push --all new-server
12.5.2 将普通文件转换为 LFS
# 将历史中的大文件迁移到 LFS
git lfs migrate import --include="*.psd,*.zip,*.bin,*.jar" --everything
# 查看迁移统计
git lfs migrate info --everything
# 提交变更
git push --force-with-lease
12.5.3 LFS 验证
# 验证 LFS 对象完整性
git lfs fsck
# 检查 LFS 对象存储使用
git lfs dedup # 去重
# 查看 LFS 配置
git lfs env
12.6 子模块迁移
# 克隆包含子模块的仓库
git clone --recursive https://github.com/org/repo.git
cd repo
# 或者之后初始化子模块
git submodule update --init --recursive
# 更新子模块 URL 到新服务器
git submodule set-url -- submodule/path git@new-server:org/submodule.git
# 提交变更
git add .gitmodules
git commit -m "Update submodule URLs to new server"
git push
12.7 迁移验证
12.7.1 迁移完整性检查
#!/bin/bash
# verify-migration.sh
SOURCE="https://github.com/org/repo.git"
TARGET="git@new-server:/opt/git/repo.git"
echo "=== 迁移验证 ==="
# 克隆源和目标
git clone --mirror "$SOURCE" /tmp/source.git 2>/dev/null
git clone --mirror "$TARGET" /tmp/target.git 2>/dev/null
cd /tmp/source.git
source_branches=$(git branch -a | wc -l)
source_tags=$(git tag | wc -l)
source_commits=$(git rev-list --all | wc -l)
cd /tmp/target.git
target_branches=$(git branch -a | wc -l)
target_tags=$(git tag | wc -l)
target_commits=$(git rev-list --all | wc -l)
echo "源仓库: 分支=$source_branches, 标签=$source_tags, 提交=$source_commits"
echo "目标: 分支=$target_branches, 标签=$target_tags, 提交=$target_commits"
if [ "$source_branches" = "$target_branches" ] && \
[ "$source_tags" = "$target_tags" ] && \
[ "$source_commits" = "$target_commits" ]; then
echo "✅ 迁移验证通过"
else
echo "❌ 迁移数据不一致,请检查"
fi
# 清理
rm -rf /tmp/source.git /tmp/target.git
12.7.2 SSH 连接测试
# 测试新服务器 SSH
ssh -T git@new-server
# 测试克隆
git clone git@new-server:/opt/git/repo.git /tmp/test-clone
cd /tmp/test-clone
git log --oneline -5
git branch -a
git tag
12.8 扩展阅读
本章小结
| 学到了什么 | 关键要点 |
|---|---|
| Git 迁移 | git clone --mirror + git push --mirror 最简单 |
| 元数据迁移 | Gitea/GitLab 内置迁移 API 支持 Issues/PR/Wiki |
| 历史清理 | git-filter-repo(推荐)/ BFG 清理大文件和敏感信息 |
| LFS 迁移 | 先 fetch –all,再 push –all 到新服务器 |
| 验证 | 对比分支数、标签数、提交数确保完整性 |
下一章:第 13 章 - Docker 部署 — 使用 Docker Compose 部署完整 Git 服务栈。