1. 迁移前先搞清楚:你要搬的到底是什么
接到"把旧仓库代码搬到新地址"这种活儿的时候,很多人的第一反应是 clone 一下、改个 remote、push 上去,半小时完事。我以前也这么干过,直到有次迁移完,测试同事拿着旧版本的 tag 去拉代码发现一片空白,release 流程直接卡住,我才意识到:代码迁移这件事,核心根本不在"代码"两个字上,而在"分支"和"tag"上。
1.1 代码迁移的本质:不是复制文件,是搬"引用"和"历史"
对于不了解 Git 内部机制的人来说,代码迁移听起来就是"把文件拷过去"。但实际上,Git 仓库的核心不是工作区里那一堆文件,而是 .git 目录下的对象数据库——每一次提交(commit)、每一个分支指针(refs/heads/)、每一个 tag 引用(refs/tags/),都保存在这里。你看到的文件只是某个 commit 的"快照投影"。
换句话说,真正的迁移对象是:
- 所有分支的完整提交历史,包括那些已经合并但还没删的分支;
- 所有 tag 引用,包括轻量 tag 和附注 tag;
- HEAD、远程跟踪分支(remote-tracking branches)等元数据。
如果只 clone 默认分支再 push,那么其他分支的提交对象虽然可能因为共享历史而存在于本地仓库,但新仓库里不会有对应的分支引用,等于这些分支"消失"了。这也是为什么我不建议用"复制文件"的思路去理解迁移。你可以把 Git 仓库想象成一个图书馆:分支和 tag 就是书架上的分类标签,commit 是书。你光把书搬过去,不把分类标签搬过去,图书馆就变成了一堆乱书。标题里的"将旧分支的 git 代码及 tag 全部迁移",关键词其实就是"全部"两个字——缺任何一个分支、任何一个 tag,都会让某个人在某一天突然拉不到代码。
1.2 迁移方案选型:镜像克隆、裸克隆、普通克隆怎么选
Git 提供了三种常见的克隆方式,很多人分不清它们的区别,这里直接给表格:
| 克隆方式 | 命令 | 包含内容 | 适用场景 |
|---|---|---|---|
| 普通克隆 | git clone <url> |
默认分支 + 远程跟踪分支 | 日常开发 |
| 裸克隆 | git clone --bare <url> |
所有分支、tag,无工作区 | 服务器端备份 |
| 镜像克隆 | git clone --mirror <url> |
所有分支、tag、远程跟踪配置 | 仓库整体迁移 |
这里重点推荐镜像克隆。它本质上是一个裸克隆,但会把源仓库的 remote 配置也一并复制过来,推送的时候用 git push --mirror 可以把你本地所有的 refs(包括远端分支、tag)原样推到新地址,是最接近"复制粘贴"的方案。
不过镜像克隆也有个前提:你得能通过 SSH 或者 HTTPS 完整拉取整个仓库。如果你的旧仓库非常大(几个 GB 甚至更大),或者网络条件不好,可以考虑 git clone --bare 配合 git fetch 增量同步,后面踩坑章节会细说。另外多说一句,如果旧仓库用了 Git LFS 管理大文件,迁移时记得确认 LFS 的对象也一并迁移了,否则新仓库里的大文件指针会变成一堆指向空对象的废链接。
1.3 动手前必须完成的盘点清单
在敲任何命令之前,我建议先花五分钟做一次盘点,确认三件事:
- 旧仓库有哪些分支:
git branch -a看本地和远程分支;git ls-remote --heads origin直接看服务器上的分支列表。 - 旧仓库有哪些 tag:
git tag -l看本地 tag;git ls-remote --tags origin看远端 tag。 - 旧仓库的提交量级:
git rev-list --all --count统计总提交数,用于迁移后的核对。
这一步看似多余,但非常关键。盘点清单就是迁移后的验收标准。我习惯把这些数据记下来,迁移完逐项比对,少一个分支、少一个 tag 都能立刻发现。别嫌麻烦,没有这份清单,出了问题你只能靠猜。尤其当旧仓库是团队里好几个人共同维护的,分支和 tag 的数量可能远超你的预期,光凭记忆根本靠不住。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心操作全流程:从旧仓库到新仓库的完整链路
盘点完之后就可以正式动手了。这里我给出一个经过多次验证的完整流程,照着做基本不会出问题。整个过程分三步:镜像克隆、换 remote 地址、推送全部引用。
2.1 镜像克隆:一条命令拿到全部家当
第一步,在本地任意目录执行:
bash复制git clone --mirror git@old-server:group/old-repo.git
执行完会生成一个 old-repo.git 的目录(注意,没有工作区,全是 Git 内部对象)。这一步会把源仓库的所有分支、tag、远程跟踪分支全部下载到本地。
如果你用的是 HTTP 方式,命令变成:
bash复制git clone --mirror https://old-server/group/old-repo.git
这里有个细节:镜像克隆成功后,目录内的 config 文件里会记录 remote.origin.mirror = true,这个配置决定了后续 git push 的行为是"镜像推送"而不是普通推送,先记住这一点,后面会用到。如果克隆过程中网络中断,别急着从头再来,先看看 .git 目录是否已经生成了部分对象,Git 的 clone 本身不支持断点续传,但你可以用后续的 git fetch 补拉,不过最省事的做法还是选个网络稳定的时段一次搞定。
2.2 更换 remote 地址:三种方式与适用场景
拿到镜像仓库后,需要把 remote 地址指向新仓库。有三种方式:
方式一:直接改 URL(最常用)
bash复制cd old-repo.git
git remote set-url origin git@new-server:group/new-repo.git
方式二:删除后重新添加
bash复制git remote remove origin
git remote add origin git@new-server:group/new-repo.git
方式三:改 config 文件
直接编辑 .git/config,把 url = git@old-server:group/old-repo.git 改成新地址。这种方式适合在脚本里批量操作。
三种方式本质一样,但方式一会保留原有的 fetch/push 配置,方式二会更干净一些。镜像克隆的场景下我更推荐方式一,因为 push 配置里带有 mirror 标志,方式二有可能把 mirror 标志弄丢。如果你已经用了方式二,记得手动补一下:
bash复制git config remote.origin.mirror true
为什么这个 mirror 标志这么重要?因为它决定了 git push origin 的默认行为。普通仓库执行 git push origin 只推当前分支;而 mirror 仓库执行 git push origin 会把本地所有 refs 全部推上去。这个差异在迁移场景里直接决定了你是一条命令完事,还是要分好几次小心翼翼地推。
2.3 推送全部分支和 tag:--all 与 --tags 的正确用法
remote 地址改好之后,推送这步很关键。很多人在这里会犯一个经典错误,只执行 git push origin --all,然后发现 tag 没过去,再补一个 git push origin --tags。这样做虽然最终结果可能对,但我建议你把顺序反过来理解一下——先分支后 tag 是有道理的,因为 tag 本质上是指向 commit 的引用,如果 commit 还没推上去,push tag 的时候 Git 会尝试把 tag 指向的 commit 也推上去。如果那个 commit 对应的文件很多,这个"附带推送"的过程会慢得让你怀疑人生;如果先推了 --all,大部分 commit 已经在远端了,--tags 就只是建立引用,速度会快很多。
更稳妥的做法是,既然我们已经用了镜像克隆,直接执行:
bash复制git push --mirror origin
这一条命令会把本地所有的 refs 推送到新仓库,包括 heads 下的所有分支、tags 下的所有 tag,以及远程跟踪分支。push 完成后,新仓库和旧仓库在引用层面就完全一致了。
如果你的场景不允许用 mirror(比如有些代码托管平台不允许推送非分支/非 tag 的 refs),那就老老实实用:
bash复制git push origin --all
git push origin --tags
这两条命令要按顺序执行,先分支后 tag。推送过程中如果出现 rejected 的报错,大概率是新仓库里已经有同名分支但 commit 历史不一致,这个后面会讲怎么处理。
3. tag 迁移是重灾区:为什么推完分支发现 tag 少了
我遇到过不少同行,分支推得很顺利,tag 却漏了一大半,排查半天也不知道问题出在哪。这一节把 tag 这块彻底讲透,也是标题里"及 tag 全部迁移"这个诉求最容易踩坑的地方。
3.1 --tags、--follow-tags、--mirror 三者的差异
先看三个容易混的命令:
git push origin --tags:推送所有本地 tag 到远端,不管 tag 指向的 commit 是否已在远端。git push --follow-tags:只推送"指向已推送 commit 的附注 tag",轻量 tag 不推。git push --mirror:推送所有 refs,包括所有 tag。
很多人以为 --tags 是"推送最新的 tag",其实它是"推送所有 tag"。这个差异在迁移场景尤其明显:假设旧仓库有 50 个 tag,你执行 git push origin --tags,50 个都会推上去,不会有"增量推送"这种智能行为。
--follow-tags 主要用于日常开发中"提交代码时顺带推 tag"的场景,它的行为是:只推送那些指向本次推送涉及的 commit 的附注 tag。如果某个 tag 指向的 commit 在远端已经存在,但不在你本次推送的提交链上,它不会推。所以 --follow-tags 不适合迁移场景,别用它来做"省事"的迁移,它省不了事,只会漏 tag。
3.2 轻量 tag 与附注 tag 的底层区别
Git 的 tag 分两种:
- 轻量 tag:只是一个指向 commit 的固定引用,类似"不让移动的分支",存储在
refs/tags/下,不包含作者、日期、消息信息。 - 附注 tag:是一个独立的 tag 对象,包含打 tag 的人、时间、消息,甚至可以签名。
用 git tag -a v1.0 -m "release v1.0" 创建的是附注 tag,用 git tag v1.0 创建的是轻量 tag。
这两种 tag 在迁移时都会通过 --tags 或 --mirror 推上去。但有一个细节:如果你用 git clone --branch v1.0 这种方式只拉了某个 tag 对应的代码,那么本地只会有这一个 tag 的引用,其他 tag 可能不会被克隆下来。这在用普通克隆迁移时很容易发生,也是"tag 少了"的常见原因之一。所以迁移时一定不要用 --branch 参数拉取,除非你明确知道自己只需要那一个分支的代码。
3.3 tag 丢失后的排查与补救
如果推完之后发现 tag 少了,先别急着重新推,按这个顺序排查:
- 确认旧仓库到底有哪些 tag:
git ls-remote --tags old-server-url。 - 确认新仓库有哪些 tag:
git ls-remote --tags new-server-url。 - 对比两份列表,找出缺失的 tag。
- 找到缺失 tag 对应的 commit:
git rev-list -n 1 <tag名>。 - 确认该 commit 是否在新仓库存在:
git cat-file -t <commit-sha>。
大多数情况下,缺失的原因是该 tag 指向的 commit 对象没有出现在新仓库里。这种情况要把那个 commit 单独推过去:
bash复制git push origin <commit-sha>:refs/tags/<tag名>
如果你能确定所有 commit 都已经在新仓库,只是引用没建立,那可以直接推送本地 tag:
bash复制git tag -l | xargs -I {} git push origin {}
不过这种批量命令要小心,最好先把 tag 列表导出对比一下再执行,否则万一某个 tag 有冲突,会中途报错,留下一半成功一半失败的状态。我个人的习惯是导出成文件再 diff:
bash复制git ls-remote --tags old-server-url | awk '{print $2}' | sort > old-tags.txt
git ls-remote --tags new-server-url | awk '{print $2}' | sort > new-tags.txt
diff old-tags.txt new-tags.txt
这样一目了然,比在终端里滚动看列表高效得多。
4. 迁移后的验证工作:别急着通知团队"已迁完"
push 完成不代表迁移成功。我见过太多人 push 完就在群里喊"迁移完了,大家用新地址吧",结果第二天就有人发现某个分支拉不下来。验证这一步绝对不能省,而且要做到可量化、可复核。
4.1 分支、tag、提交历史的四方校验
我习惯做一个"四方校验":
- 分支数量校验:
git ls-remote --heads new-server-url | wc -l和旧仓库对比。 - tag 数量校验:
git ls-remote --tags new-server-url | wc -l和旧仓库对比。 - 总提交数校验:
git rev-list --all --count分别在旧仓库和本地镜像仓库执行,对比结果。 - 关键分支 HEAD 校验:挑几个重要分支(比如 master/main、develop、正在迭代的功能分支),比较它们在新旧仓库的 HEAD commit SHA 是否一致。
第 4 步最容易被人忽略。分支存在不代表内容正确,万一 push 的时候漏了几个 commit,分支 HEAD 就和新仓库对不上。SHA 比对是最可靠的校验方式,没有之一。举个例子:
bash复制# 在旧仓库执行
git ls-remote origin refs/heads/master
# 在新仓库执行
git ls-remote origin refs/heads/master
两个命令输出结果里那个 40 位的字符串必须完全一致。只要有一处不一致,就要回到本地镜像仓库确认对应分支的 HEAD 是否正确,然后重新推送。
4.2 本地旧仓库的清理与重新指向
迁移完成后,团队里每个人的本地仓库还指向旧地址。这一步需要在团队内同步执行,我建议在群里发一份"三步走"的操作指引:
bash复制# 查看当前 remote
git remote -v
# 更换为新的 remote 地址
git remote set-url origin git@new-server:group/new-repo.git
# 拉取最新的远程分支和 tag
git fetch origin
如果旧服务器之后要下线,还需注意本地可能存在的"幽灵远程分支"——之前 fetch 留下的 origin/xxx 引用。可以用 git remote prune origin 清理。另外,如果某位同事本地有未推送的 commit,先别急着让他删本地仓库,先确认这些 commit 是已经合并到远端分支了,还是只剩他一个人有的独有提交。后者需要先推送到新仓库,再清理。
4.3 团队协作和 CI/CD 的衔接问题
仓库地址换了,受影响的不只是开发者的本地仓库,还有一堆自动化流程。我按容易踩坑的程度排个序:
- CI/CD 流水线:Jenkins、GitLab CI、GitHub Actions 等流水线里的仓库地址、webhook、部署密钥都要更新,否则下一次自动构建会直接失败。
- 分支保护规则:新仓库默认通常没有保护规则,master/main 上的直接 push 可能没人拦得住,迁移后第一件事就是重新配好保护规则,把"禁止直接推到主干分支""必须走 MR/PR"之类的规则恢复原样。
- 依赖管理:如果项目通过
go get/npm install从 Git 仓库直接拉依赖,相关配置里的地址也要换,否则其他项目构建时还是会去旧仓库拉。 - 文档与脚本:README、部署文档、自动化脚本里写死的旧地址要全局搜索替换。这个容易被忽略,但影响很大,尤其是脚本,一旦部署的时候跑不起来,排查半天才发现是 git 地址的问题,非常冤枉。
还有一个容易被忽略的点:如果新仓库的默认分支名和旧仓库不一样(比如旧仓库是 master,新平台默认建 main),迁移后记得调整默认分支的设置,并且让团队成员把本地分支重新关联到正确的远程分支上。
5. 实战踩坑记录:这几个问题我基本每次都遇到
最后分享几个我在实际迁移中反复踩过的坑,希望能帮你省掉几个小时的排查时间。这些都是真实发生过的事情,不是文档里会告诉你的。
5.1 大仓库迁移超时与断点续传
如果旧仓库体积很大(超过 1GB),一次 clone 很可能因为网络波动失败。我的经验是分两步走。
第一步,做一个不带完整历史的裸克隆先占位:
bash复制git clone --bare --filter=blob:none git@old-server:group/old-repo.git
这里 --filter=blob:none 的意思是先只拉 commit 和 tree 对象,不拉具体的文件内容(blob)。这样 clone 的速度会快很多,尤其适合那种历史里塞过大文件、导致仓库体积膨胀的项目。
第二步,后续需要完整历史时再补齐:
bash复制git fetch --unshallow
或者按需拉取某个分支的 blobs。当然,如果你确定新仓库不需要完整历史(比如彻底重构),那直接浅克隆就行。但迁移场景下我强烈建议要完整历史,因为你不知道哪天有人需要翻旧账,到时候历史没了,神仙也救不回来。顺带提一句,如果仓库历史里有不小心提交的大文件(比如几百 MB 的压缩包),迁移过去之后新仓库的体积也会跟着膨胀,这种情况建议先处理历史再迁移,用 git filter-branch 或者 git filter-repo 清理,但那就是另一个大工程了。
5.2 默认分支和本地分支的"幽灵引用"
Git 仓库迁移后,本地经常出现 origin/xxx 指向旧地址的情况。这是因为远程跟踪分支的引用还保留着,但旧服务器已经连不上了。这时候如果不做清理,git branch -a 会看到一堆"死引用",误导后续操作。
处理方式是对每个开发者的本地仓库执行:
bash复制git remote prune origin
git fetch --prune
注意 remote prune 和 fetch --prune 的差别:前者只清理远程跟踪分支,后者会顺便更新所有远程跟踪分支。迁移场景建议两个都执行。另外有个小技巧,如果某个本地分支对应的远程分支在新仓库里不存在了,你想要保留这个本地分支,记得把它重新推送到新仓库:
bash复制git push origin <local-branch-name>
否则等旧仓库下线,这个分支就只剩你本地一份了,一旦本地丢了就真的找不回来了。
5.3 权限与免密:换了服务器之后 push 失败
换了新 git 服务器之后,最常见的报错是 Permission denied (publickey) 或者 HTTP 方式下的 403。原因通常是新服务器的 SSH 公钥没有配置,或者新平台账号的权限没有分配到位。
排查分两步:
- 确认本机 SSH 公钥已经添加到新平台账号:
cat ~/.ssh/id_rsa.pub,然后去新平台的后台添加。如果之前生成过新的 key,可能还要在~/.ssh/config里指定IdentityFile。 - 确认你在新仓库有写权限:代码托管平台一般有 Guest/Reporter/Developer/Maintainer/Owner 等角色,至少要有 Developer 权限才能 push 分支。
HTTP 方式的话,还要注意凭证缓存的问题。如果之前用的是旧服务器的账号密码,新服务器地址不同,Git 会用缓存的凭证去认证,导致 403。可以执行:
bash复制git config --global credential.helper ""
清掉全局凭证缓存,再 push 时重新输入账号密码。
最后再分享一个我自己常用的收尾动作:迁移完成后,我会把旧服务器仓库的设置改成只读(如果有权限的话),或者至少通知所有成员停止向旧仓库推送。这样能避免"两边都在推,最后不知道以哪个为准"的混乱。等到确认新仓库稳定运行一两周之后,再真正下线旧仓库,这个节奏是最稳的。迁移这种事情,追求的不是快,而是稳——毕竟代码是团队的资产,宁可多花半天验证,也不要出了岔子之后花一天去补。
