尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

git push --mirror 原理与实操:一键迁移完整仓库历史、分支与标签

git push --mirror 原理与实操:一键迁移完整仓库历史、分支与标签 事情源于上周帮同事做的一次仓库搬迁公司内部新搭了一套 GitLab要把 GitHub 上某个项目的完整历史同步过去要求不多就一条——原来有什么新仓库就得有什么分支、标签、每一条提交记录一个都不能少。很多人遇到这种需求会下意识操作“把代码下载下来再传上去”但真这么做就会把提交历史、分支结构、原作者信息全弄丢。正确做法很简单用git push --mirror把整个仓库镜像到新地址。接下来我就从这个命令的底层原理、完整实操到迁移中容易踩的坑一次讲透。1. 什么情况下会用到 --mirror从一次仓库搬迁说起1.1 需求其实很常见完整历史搬进新仓库平时说起 Git大家最熟悉的是git add、git commit、git push这一套日常提交流程。但有一种需求跟“提交当前代码”完全是两码事把整个仓库从旧地址搬到一个新地址。触发场景非常多比如公司从 GitHub 公共仓库迁到内网 GitLab代码和数据不能留在外部。旧的 Git 服务器要退役需要把全部项目搬到新服务器。想给某个项目做一份“一模一样”的备份仓库连远端分支的引用关系都保留。接手别的团队项目对方直接把仓库地址发过来要求原封不动复制一份。这类需求有一个共同点重视的不是当前工作区的文件而是仓库里沉淀下来的全部记录。develop 分支的几百次提交、release 目录下的几十个 tag、某次合并背后的父子关系这些都属于需要迁移的“数据”。而git push --mirror这个命令就是专门为这类场景设计的。1.2 为什么不能直接下载代码再重新上传我见过有人用最朴素的办法迁移仓库把项目代码下载成 zip然后去新平台创建一个空仓库git init、git add .、git commit、git push一条龙。这么搞确实能把代码文件传到新仓库但损失几乎是灾难性的提交历史全部丢失git log里只有一条“初始提交”。每个文件不再显示原作者blame 信息变成迁移人。所有分支、标签的引用关系全部消失。代码评审、里程碑相关的 commit 关联信息全部作废。一句话你搬过去的只是代码快照不是仓库。仓库之所以叫仓库是因为它有完整历史、引用和对象数据库。--mirror要解决的核心问题就是把整个“仓库实体”复制过去而不只是把当前检出的文件复制过去。2. --mirror 的底层原理它复制的不是文件是引用2.1 先搞懂 refs分支、标签、远端分支都是一种引用想理解--mirror先得知道 Git 里的分支和标签本质是什么。我们平时说“main 分支”“v1.0 标签”在 Git 底层其实就是一个个refs也就是引用refs/heads/main指向一个提交对象哈希。refs/tags/v1.0指向一个标签对象或提交对象。refs/remotes/origin/main是本地记录的远端分支状态同样是一个引用。refs/notes/...这类附加引用也存在。日常操作里git branch是查看本地分支引用git tag是查看标签引用git branch -r是查看远端跟踪引用。但很多人不会刻意去想这些“指针”本身也是仓库的一部分当然后续也就低估了迁移时要复制的范围。普通git push推送时默认只推送“当前分支”对应的引用最多再带一些关联对象。你可以用--all推送全部本地分支用--tags推送全部标签但这仍然没有覆盖所有引用类型。而--mirror直接把整个 refs 空间端平有多少引用就推多少引用。2.2 mirror push 的 refspec 如何工作git push命令本质上是一个“按 refspec 规则执行引用传输”的操作。refspec 的格式一般是src:dst意思是把本地的src引用推送到远端的dst引用。普通推送的 refspec 是refs/heads/main:refs/heads/main你指定哪个分支就传哪个。而git push --mirror会把 refspec 改写成refs/*:refs/*这个通配符表达式的含义是把本地所有refs/*下的引用一一映射到远端同名路径下。前面的表示允许强制更新也就是远端引用和新内容不一致时可以直接覆盖。更关键的是--mirror不只是“写入”它还会删除远端有、而本地没有的引用。这就像你把一面镜子对准物体镜子里必须只有物体的样子任何不存在的“杂物”都会被抹掉。所以它的行为是双向的本地有、远端没有 → 推送创建。本地有、远端也有但指向不同 → 强制覆盖。本地没有、远端有 → 删除远端引用。这才是“镜像”的真正含义。明白这一点你就能理解一个重要的使用前提目标仓库最好是全新的空仓库或者你明确知道它可以被完整覆盖。2.3 和 git push --all --tags 的核心区别很多文章会把git push --mirror和git push --all --tags混淆认为两者差不多。我实际对比下来差别非常大直接看表格对比项git push --mirrorgit push --all --tagsgit push origin main本地分支全部引用全部本地分支仅当前分支标签全部引用全部标签不推送远端跟踪分支会同步不会不会目标端多余引用会被删除保留保留附加引用notes 等会同步不会不会适用场景整库镜像迁移/完整备份常规多分支多标签迁移日常提交看起来--all --tags好像也能覆盖大部分需求但它有一个关键缺陷不会清除目标端的多余引用也不会同步你在本地缓存的远端跟踪分支。如果旧仓库曾经 fetch 过很多合作分支镜像迁移后希望这些分支也出现在新仓库--all --tags就漏掉了。--mirror才是真正意义上的 1:1 复制。3. 实操全流程把本地仓库镜像推送到新地址3.1 准备新仓库记住一个关键条件先说准备工作。无论你用的是 GitHub、GitLab、Gitee 还是自建服务第一步都是登录平台创建一个新仓库。这里有一个所有教程都会强调、但总有人不看的前提创建时不要勾选“初始化仓库”“添加 README”“添加 .gitignore”这类选项。如果平台默认帮你初始化了 main 分支并提交了 README那新仓库一开始就带着一个独立的提交记录。等会儿用--mirror推送时本地 refs/* 会覆盖远端所有引用这个初始化提交会被强制清理掉。虽然结果没问题但这个动作本身风险很高尤其当你对--mirror的覆盖逻辑不够熟悉时容易慌神。最省心的方式就是创建一个完全空的仓库什么文件都不要预置。创建好之后复制仓库地址。这里建议用 SSH 地址而不是 HTTPS# HTTPS 地址 https://gitee.com/yourname/new-repo.git # SSH 地址推荐 gitgitee.com:yourname/new-repo.git使用 SSH 的好处是可以通过密钥免密推送避免 HTTPS 在命令行里反复输入账号密码也更方便自动化脚本调用。3.2 添加远端并执行 git push --mirror本地操作就从你已经存在的项目目录开始。先确认当前仓库状态和远端情况# 进入项目目录 cd my-project # 查看当前远端配置 git remote -v假设当前仓库的远端还指向旧服务器执行下面的命令把新仓库地址添加为一个临时远端。我习惯把它命名为new-origin避免和原来的origin混淆git remote add new-origin gitgitee.com:yourname/new-repo.git添加完成后可以再确认一次git remote -v接下来就是核心命令git push --mirror new-origin执行后 Git 会按照refs/*:refs/*的规则开始传输。输出内容大致是推送了哪些分支、标签和引用类似Enumerating objects: 15234, done. Counting objects: 100% (15234/15234), done. Delta compression using up to 16 threads ... To gitgitee.com:yourname/new-repo.git * [new branch] main - main * [new branch] develop - develop * [new tag] v1.0.0 - v1.0.0 * [new tag] v1.2.1 - v1.2.1如果你看到* [new branch]、* [new tag]这些输出说明引用创建成功。这里不需要额外执行git push --tags因为--mirror已经把标签一并推上去了。还有一种情况如果新仓库的地址和当前远端一致或者你想直接迁移到当前origin指向的位置也可以简写成git push --mirror origin但我个人强烈建议迁移时用一个独立的new-origin名称等确认无误后再调整远端这样中途发现问题可以随时回退。3.3 推送结束后的 remote 调整与团队同步镜像推送完成后本地仓库的new-origin只是临时用来推送的远端。接下来要做的是把仓库日常使用的远端地址更新为新地址。假设以后默认远端就叫origin可以这样调整# 方法一删除旧的 origin把 new-origin 改名成 origin git remote remove origin git remote rename new-origin origin # 方法二直接修改 origin 的 URL git remote set-url origin gitgitee.com:yourname/new-repo.git git remote remove new-origin两种方式选一种就行。我倾向用方法一因为git remote rename不仅能改 URL还能把本地之前记录的refs/remotes/origin/*跟踪引用一起改过来一步到位。团队里其他成员的处理方式也类似让每个人的本地仓库把远端地址指向新仓库然后执行一次git fetch重新同步即可。如果有人在迁移期间本来就有未推送的本地提交那就先让它推送到新仓库的对应分支再让其他人 fetch。在这里补充一个和编辑器的衔接VS Code、IntelliJ IDEA 这些 IDE 里的“推送”按钮本质上还是调用git push。只要本地远端地址改对IDE 里的 Source Control 面板刷新后就会指向新仓库。如果发现 IDE 还显示旧地址重启一下或者执行git fetch --prune刷新远端引用即可。4. 我在实际操作中踩过的坑4.1 新仓库默认初始化分支导致镜像覆盖第一次做 mirror 迁移时我在 Gitee 上创建仓库手一滑勾选了“使用 Readme 初始化仓库”。当时没意识到后果直接执行了git push --mirror new-origin结果看到输出里有一行delete [refs/heads/main]那一瞬间我愣了一下然后才反应过来这是--mirror发现目标端多了一个 main 引用而这个引用不在本地引用列表里于是主动删除了它。因为我的本地仓库本来就有 main后续真正的 main 被推了上来所以最终结果没问题。但这个“主动删除”的行为隐藏着一个大坑如果目标仓库里存在你本地没有的分支那些分支会被全部删掉。比如新仓库里其他人已经推了一个hotfix分支你执行 mirror它就会被删掉。所以我再次强调除非目标仓库完全由你掌控且确定是空的否则不要随便对已有内容的仓库执行--mirror。4.2 受保护分支拒绝推送仓库平台普遍默认保护主分支。在 GitLab 里main/master 默认是受保护分支普通成员甚至 Maintainer 都不能直接强制推送。如果你在受保护分支上执行--mirror很可能遇到这样的报错remote: GitLab: You are not allowed to push code to protected branches on this project.我当时第一次看到这个报错时还以为是权限不够后来才明白是分支保护规则在起作用。--mirror本身带有强制覆盖性质受保护分支的“拒绝强制推送”策略会把它拦下来。解决方案有两个在目标仓库的分支保护设置里临时把主分支的强制推送权限打开或者直接去掉保护等 mirror 推送完成后再恢复。如果仓库还没有任何代码干脆删掉仓库重新创建建一个完全空的新仓库再推。内部系统迁移时我比较推荐第二种方式因为一个全新仓库没必要带着平台自动生成的 main 提交去处理。如果仓库里已经有其他人的工作那就必须提前沟通好再临时调整保护规则。4.3 LFS 大文件不会自动迁移如果你项目里用了 Git LFSLarge File Storage比如存了设计稿、二进制包、数据集那--mirror只能帮你把仓库的引用和指针提交迁移过去LFS 对象本身不会跟着走。LFS 的原理是仓库里只存一个文本指针真正的文件存在远端的 LFS 存储服务里。mirror 推送只复制了 Git 对象数据LFS 对象还在旧服务器的对象存储中。新仓库拿到 LFS 指针后如果拉取不到对应对象文件内容就会变成损坏状态。正确的配合做法是在新仓库启用 LFS然后在本地执行# 从当前环境抓取全部 LFS 对象 git lfs fetch --all # 推送到新远端 git lfs push --all new-origin这两个命令会把项目引用到的所有 LFS 对象完整搬运到新仓库。我的经验是先推送 LFS 对象再 push mirror或者反过来都行但一定要在迁移清单里把 LFS 这个步骤写进去漏掉它后面测试克隆时会非常头痛。4.4 子模块的 url 仍旧地址如果项目使用了 submodule情况会更隐蔽。Git 仓库里记录的 submodule 只是一个指向某个提交的 gitlink真正的子模块仓库地址写在.gitmodules文件里。mirror 迁移只负责把主仓库和子模块的 gitlink 引用推过去子模块仓库本身的 URL 仍然是旧地址。子模块的迁移必须单独处理。通常的做法是把子模块的仓库也用--mirror迁移到新服务器。修改主仓库的.gitmodules文件把子模块 URL 改为新地址。重新执行git submodule sync和git submodule update --init验证。如果子模块也放在同一个旧服务器上这一步很容易被忽略结果就是新仓库可以看代码但git submodule update时依然去访问旧服务器仓库迁移就变得不彻底。4.5 CI/CD 配置、Webhook 和权限体系都不会跟着走很多公司仓库迁移后出的第一个大事故不是代码没了而是 CI 不跑了。--mirror只是 Git 数据层面的镜像GitLab CI/CD 的.gitlab-ci.yml虽然会在代码里被一起推过来但流水线相关的配置项、变量、Runner 绑定、Webhook 回调、成员权限、分支保护规则这些都不会自动迁移。这就像你搬了新办公室电脑文件拷过来了但门禁权限、上网账号、会议室预约系统都要重新申请。迁移正式仓库前最好先列一个清单有哪些 CI 脚本依赖旧地址有哪些人需要被加入新仓库成员Webhook 要指向新服务器还是不变这些事项越早规划越好不然代码迁移完成后的第一个工作日会被各种“CI 报错”“Webhook 推不进来”的消息淹没。5. 迁移后的验证怎么确定搬家搬完整了5.1 远端引用核对git ls-remote 一眼看全镜像推完之后不要急着告诉团队“迁移完成”先做验证。最简单的校验方式是直接查看远端引用git ls-remote new-origin这个命令会列出新仓库当前所有的引用哈希和名称。如果分支数量、标签数量和旧仓库一致基本可以确定迁移成功。对比时可以利用排序和数量统计# 查看远端分支列表 git ls-remote --heads new-origin # 查看远端标签列表 git ls-remote --tags new-origin # 统计引用数量 git ls-remote new-origin | wc -l实际操作中我一般会对比“旧仓库本地所有引用”和“新仓库远端所有引用”的数量。如果两端完全一致说明 refs 层面迁移完整。5.2 在新机器上 clone 对比远端引用一致还不够最好真正 clone 一份出来做二次确认。我惯用的验证流程是# 在临时目录克隆新仓库 git clone gitgitee.com:yourname/new-repo.git /tmp/verify-repo cd /tmp/verify-repo # 检查提交记录是否完整 git log --oneline --graph --all # 查看分支 git branch -a # 查看标签 git tag # 统计所有提交数量 git rev-list --count --all然后回到旧仓库执行同样的git rev-list --count --all对比数字是否一致。这一步能有效发现那些“引用在但对象缺失”的隐蔽问题。如果是在服务器上迁移clone 一份到本地再跑一遍就非常放心了。5.3 旧仓库别着急删迁移完成后旧仓库的去留也有讲究。我见过不少人确认新仓库能用当天就把旧仓库删了结果过几天发现某个 CI 配置还没迁移完或者某个成员本地还链着旧地址只能懊悔地找备份。比较稳妥的时间安排是新仓库上线后旧仓库至少再保留 2 到 4 周。迁移当周主动检查团队成员的本地 remote 是否都已切换。第二周确认没有新提交发到旧仓库后再把旧仓库标记为只读或归档。归档后再观察一周确认没有服务依赖才彻底物理删除。如果旧仓库托管在 GitHub 这类公共平台还可以把仓库改成 Private然后降级成 Archive 状态。这样旧地址即使还在也不会再被误提交数据也不会被外部访问。6. 什么时候不该用 --mirror替代方案对照6.1 只想迁移部分分支用 git push --all --tags--mirror是“全量强制同步”的工具但如果你的需求只是把几个常用分支和一个标签列表搬到新仓库并不要求绝对精确那git push --all --tags反而是更安全的选择。因为它不会删除目标端多余引用只做增量写入。举个例子你想把一个项目的 main 和 develop 分支迁到新仓库同时把 release 标签全部带上直接执行git push --all --tags new-origin后续每次有什么新的分支要同步再手动推送对应分支即可。这种模式下两个仓库可以暂时共存不会出现覆盖事故。6.2 离线迁移场景git bundle如果两个服务器之间网络不通或者你只能通过文件拷来拷去迁移可以用git bundle把仓库打包成一个文件再在目标端解包。这有点像给 Git 仓库做了一份“存档快照”非常适合离线环境。打包命令git bundle create repo.bundle --all这个命令会把所有引用和对象打成一个repo.bundle文件。把这个文件拷贝到目标机器后可以有两种恢复方式# 方式一直接从 bundle 克隆仓库 git clone repo.bundle new-project cd new-project git remote remove origin git remote add origin gitgitee.com:yourname/new-repo.git git push --mirror origin # 方式二将 bundle 添加为远端后拉取 git remote add bundle /path/to/repo.bundle git fetch bundle --allbundle 方案最大的优势是不需要网络连接适合内网服务器之间做数据搬运。缺点也很明显如果仓库很大打包文件会非常大存在 U 盘或移动硬盘时要留足空间。6.3 只要最终代码不要历史直接拷贝文件有些场景反而要用“重新提交”的方式。比如一个老项目里有大量历史提交可能包含敏感信息你迁移到公开平台前想把这些痕迹全部抹掉或者你只需要把当前版本作为新起点不关心过去发生了什么。这时候可以不用任何 Git 高级技巧直接把代码拷出来新建仓库后提交一次即可。通过合并 squash 的方式也可以达到类似效果# 在本地创建一个不保留历史的新分支 git checkout --orphan clean-history main git commit -m Initial import用--orphan创建的分支不会继承任何现有历史提交记录从这一条开始。这比直接删除.git目录更可控至少文件列表和当前分支内容是完整的。6.4 选型对照表方案历史记录分支/标签目标端覆盖风险适用场景git push --mirror保留全保留高会删除多余引用整库迁移、完整备份git push --all --tags保留本地分支和标签全保留低不会删除部分分支迁移、日常多分支同步git bundle clone保留全保留低手动控制离线环境、跨机房迁移拷贝文件 重新提交丢失丢失无公开化、去敏感历史、新起点我个人在实际操作中的体会是能确定目标仓库是全新空仓库时优先用--mirror一步到位目标仓库已经有内容或者你和团队还没法确认所有引用列表时宁可选择更保守的--all --tags再手动补。在做这种处理时尽量先备份一份远程仓库的元数据或者至少确认一下本地 remote URL再执行命令能避免很多本可避免的“搬家后遗症”。最后再分享一个小建议执行--mirror前可以先在本地跑一次git fsck git count-objects -v确认仓库对象完整也可以提前写一个包含验证步骤的检查清单真正迁移时照着执行会发现整个过程远比凭感觉操作要稳得多。
返回列表