强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 服务栈。