上个月我接到一个有点“零碎”的需求:要把某个后端仓库里的支付模块迁移到另一个新建的仓库去。这个模块大概有四十几个文件,牵扯到几十个 commit 的改动历史,旧仓库的 CI、Dockerfile、依赖清单里到处都有它的影子。
听到“迁移部分代码”,很多人的第一反应是复制粘贴,或者直接把整个目录拷过去提交了事。但如果团队里几十个人都在用这个仓库,代码历史又关联着需求单、bug 修复记录和上线审计,简单复制就会把所有上下文丢掉。反过来,直接把整个仓库搬过去也不现实,旁边还躺着订单、库存、用户中心一堆无关代码。
这种场景就是典型的“跨仓库迁移部分代码”:把指定目录或一组文件,从源仓库搬到目标仓库,尽量保留有意义的提交记录,同时不让两边之后的开发互相踩脚。这篇文章不想讲那种“整体仓库搬家”的教程,而是专门针对“只搬一部分”的情况,把我完整跑通的流程、踩过的坑、以及为什么在四个方案里最终选了 filter-repo,一次说清楚。
1. 先别急着敲命令:跨仓库迁移部分代码前要确定的三件事
1.1 这次迁移到底想保留什么,历史能不能丢
迁移之前,我习惯先问需求方一个问题:搬过去的代码,历史记录重要吗?
如果你的场景是“把临时写的脚本丢到新仓库”“把某个开源项目的单文件拿过来改改”,那直接复制粘贴完全没问题。但多数真实业务模块不是这样。支付模块里的每一次提交,都可能是某次事故的修复、某个客户需求的落点,或者审计时要用到的凭证。这种时候,保留 commit 历史就不是“锦上添花”,而是“基本要求”。
不同保留程度对应完全不同的实现手段:
- 完全不要历史:直接在目标仓库
git checkout oldrepo/master -- src/payments,然后 commit 一次。 - 保留部分提交:用
git format-patch把某几个 commit 导成补丁文件,再到目标仓库git am打进去。 - 保留指定目录下所有历史:用
git subtree split或者git filter-repo。 - 保留所有 commit 但只挑出相关文件:
filter-repo是当前最可控的方式。
我见过不少团队第一次做这种事,拍脑袋选了最快的方式,等到半年后查线上问题、翻 blame 时发现历史是空的,只能对着代码猜当时的上下文,那才叫痛苦。所以先谈“历史要不要”,后面所有技术选型才有意义。
1.2 迁移边界:目录、文件还是“某次提交开始”的全部代码
第二个要理清的是边界。这里的边界不只是“src/payments 这个目录要不要包括 tests/payments”,还包括:
- 代码是否还引用同一仓库里其他模块的类、函数、配置?
- 某个接口是只被你迁移的模块用,还是旁边好几个模块都在用?
- 数据库迁移 SQL、资源文件、配置文件、Dockerfile 是否也要一起搬?
- 那部分代码在旧仓库里可能不在同一个目录下,
src/main/java/com/example/payments只是核心代码,config/payment.yml和scripts/migrate_payment_db.sh分别散在别的位置。
我的做法是先把所有相关路径列在一张纸上,宁可多列出来,再逐个跟团队确认“哪些必须搬、哪些可以留着”。这一步不要省,因为后面filter-repo的--path参数就是拿这张清单当输入。漏了一个路径,搬过去之后编译不过,重新跑一遍 filter-repo 会把所有 commit hash 再改一次,影响一堆人。
还有一个容易被忽略的边界:有些 commit 是在目录重命名之前提交的。比如支付模块最早叫src/checkout/payment,后来才改名为src/payments。如果直接用--path src/payments提取,很可能会丢掉重命名之前的历史。这时候要么把旧路径也加进--path,要么用--path-rename把新旧路径统一起来。这个细节后面实操章节会展开。
1.3 依赖边界:代码移走后,旧仓库和新仓库各自缺了什么
迁移代码本质上不是“把文件从一个格子挪到另一个格子”,而是“把一个节点从旧网络里拆出来,再连到新网络里”。代码本身只是节点,依赖关系才是真正的主干。
迁移前建议做一次依赖扫描,把下面几类东西全部找出来:
- import / require / include 语句中指向旧仓库其他模块的引用;
- 通过 Maven / Gradle / npm / pip 声明但实际由本地模块提供的包;
- Docker 构建上下文里引用的其他目录;
- CI 配置中预设的环境变量、SSH key、私有仓库地址;
- 硬编码的路径,比如
/app/payments/这种部署路径; - 同一个 git 仓库里共享的公共库模块。
这一步的心情起伏很大。你以为搬的是一个模块,最后发现搬的是一个“隐形的微服务”——它依赖了公共库里 5 个类,而公共库又不能整个搬过去。遇到这种情况,最好的选择不是强行把公共库也搬过去,而是在新仓库里抽出需要的公共代码,或者把公共库单独拆成独立包,然后两边都依赖这个包。
2. 四种迁移路径的取舍:从最小改动到彻底重写历史
在做具体命令之前,我把四种主流做法摆开对比过一遍。很多文章只推其中一种,但实际工程里没有银弹,关键看你的边界和历史要求。
2.1 直接复制文件:零历史,最快,适合一次性搬运
这是最原始也是最常见的方法。到旧仓库把目标目录拷下来,粘到新仓库,提交完事。简单到不需要解释。
但不建议在“代码还在活跃开发”的业务模块上这么干,原因有三:
- 历史被清零,
git blame、git log全变成一条空提交,之后排查问题全靠记忆。 - 复制过程中容易把
.gitignore忽略掉的本地文件或者构建产物一起带过去。 - 你没法区分“哪些文件是应该搬的”,等于把所有文件混成一团交过去。
它真正适合的场景是:从一个不打算再维护的参考项目里提取样例代码,或者搬一个基本冻结的旧模块过去做归档。如果你正在迁移一个活模块,请直接跳到下面几个方案。
2.2 format-patch + git am:按提交粒度搬运,但边界容易漏
git format-patch能把一个或多个 commit 导出成补丁文件,然后在目标仓库用git am重新应用。这样做的好处是保留了每个提交的改动内容和原始 message,作者信息也能带上。
实际操作时可以先在旧仓库里看目标目录的提交记录:
cd old-repo git log --oneline -- src/payments然后确定一个起始提交,把从它到当前分支的所有相关提交导出:
git format-patch start-commit..HEAD -- src/payments --stdout > payments.patch再到新仓库:
cd new-repo git am payments.patch这个方案的第一个坑是:它只跟踪“对路径有直接改动”的提交。如果某个提交把公共类从common/util.py移动到了payments/util.py,它对两个路径都有操作,但导出-- src/payments时会把这个文件的新版本算进去,却不会带上它在旧路径下的历史。你可能最终能看到文件,但它的“前世”丢了。
第二个坑是如果目标文件路径在新仓库已经存在,git am会产生冲突,而 conflict 解决起来比普通 merge 还麻烦。
所以我的结论是:format-patch适合迁移少量、边界清晰、路径稳定的提交,比如把某个模块过去三天的 bug 修复同步到新仓库,不适合做大面积目录级迁移。
2.3 subtree split:保留了目录历史,但可能夹带“路过”的提交
git subtree是 git 官方支持的一种子项目管理方式,核心思路是把一个仓库的子目录,当作另一个仓库的完整历史来做。
要把旧仓库的src/payments目录拆成一个独立分支:
cd old-repo git subtree split -P src/payments -b payments-branch这样payments-branch里就只有src/payments下的文件,以及这些文件的历史。然后可以把它推到新仓库:
git push ../new-repo payments-branch:master它的优势是历史完整、操作命令少、不依赖额外工具。但我也遇到过不太顺的地方:subtree split是按树对象一层层拆的,只要某次提交里改动过src/payments下面任何一个文件,这个提交就会被算进去。如果同一个提交里既改了支付模块又改了订单模块,那么payments-branch的这次提交虽然不包含订单文件,但提交 message 里可能同时提到订单,历史看起来是“夹带”的。大多数情况下能接受,可如果你要求每条历史都干净到只跟支付相关,那就得靠 filter-repo。
另外,subtree split产生的新分支和原仓库不是同一个 commit hash,它内部通过映射关系维持关联,这个关联关系在把分支推给新仓库后并不会自动带过去。所以别指望在新仓库里还能用git subtree pull跟旧仓库无缝同步。
2.4 filter-repo:外科手术式提取,最接近“完整迁移”的解法
git filter-repo是官方推荐的历史重写工具,近些年被很多人用来清理仓库大文件、删除敏感信息、按路径拆分目录。它比老一代的filter-branch快得多,规则也更清晰。
在“跨仓库迁移部分代码”这个场景里,它做的是:扫描整个仓库的提交图,把所有跟目标路径无关的文件从每个提交里删掉,再把因此产生的空提交清理掉,最后得到一段干干净净、只包含目标目录历史的提交链。
它对历史的重写比 subtree 更彻底。subtree 只是“提取”,filter-repo 是“重写”。如果旧仓库的目标目录下原来有一个README.md提到了整体系统架构,filter-repo 不会替你把内容改掉,它只是保留文件本身。如果你的目标是“支付模块被独立成一个项目”,你可能需要自己再对文档做一次清理。
这也是我最终选 filter-repo 的原因:它能精确控制哪些路径进入新仓库、哪些全部丢弃,还能顺便解决路径重命名、作者改写、空提交清理等问题。代价是它改变了所有 commit hash,所以操作前必须把影响半径想清楚。
2.5 方案对比总表
| 方案 | 是否保留历史 | 历史完整度 | 操作复杂度 | 适用场景 |
|---|---|---|---|---|
| 直接复制文件 | 否 | 无 | 极低 | 一次性参考代码、冻结模块归档 |
| format-patch + am | 是 | 有限,易漏路径边界 | 中 | 少量提交、短期同步 |
| git subtree split | 是 | 较完整,可能夹带无关 message | 中 | 不想额外装工具、接受少量杂质 |
| git filter-repo | 是 | 最可控,可精确切割 | 较高 | 真正把模块“独立成仓”、需要干净历史 |
3. filter-repo实操:把支付模块从旧仓库完整搬进新仓库
下面这段是我实际操作中跑通的完整链路。假设旧仓库叫monolith-server,目标新仓库叫payment-server,要搬的目录是src/payments和tests/payments。
3.1 前置准备:clone一份全新的源仓库
git filter-repo刻意设计成“只有在 fresh clone 的仓库上才允许运行”,目的就是防止有人直接在原仓库上执行并破坏远端历史。所以第一步永远是克隆一份全新的副本。
git clone --no-single-branch /path/to/monolith-server monolith-migrate cd monolith-migrate git branch -a--no-single-branch很关键。默认 clone 只拉取远端默认分支,如果支付模块的历史散落在release/2.0、feature/payment-v3等分支里,单分支克隆会把这些分支的历史全部漏掉。加了这个参数,Git 会拉取远端所有分支和 tag 的 commit 对象,后面重写时才能覆盖到完整历史。
克隆好之后,先把 origin 这个远程配置记下来,因为 filter-repo 运行后会自动删掉它。
3.2 用 --path 提取目标目录并删除无关历史
执行下面这条命令,把仓库重写成只包含src/payments和tests/payments两个路径:
git filter-repo --path src/payments --path tests/payments --force--force是因为这份克隆已经被我加过参数或者看过分支,filter-repo 会判断它不是“刚 clone 完、从未改过”的仓库,需要用 force 覆盖这个保护机制。如果你严格在刚 clone 完的仓库里直接执行,可以不加 force,但实际流程里很少有人能忍住不先看一眼分支,所以干脆加上。
执行完之后,仓库里的所有文件应该只剩下这两个目录,所有提交也变成只涉及这两个目录的提交。你可以检查一下:
git log --oneline --all | head -20 git ls-files | head -50如果发现某些提交变成了空提交(原提交只改了支付模块之外的文件,被过滤后没有任何文件变化),可以再补一次清理:
git filter-repo --path src/payments --path tests/payments --prune-empty=always--prune-empty=always会删掉所有空提交。默认的 auto 模式通常也会清理,但如果遇到保留空提交用于维持 merge 结构的情况,可以手动加强。
3.3 处理路径前缀:保留原路径还是放到新仓库根目录
迁移到新仓库后,代码所在位置不一定要沿用原来的目录结构。
如果要同时把src/payments平移到新仓库根目录,可以在 filter-repo 命令里加--path-rename:
git filter-repo \ --path src/payments \ --path tests/payments \ --path-rename src/payments:payment \ --path-rename tests/payments:tests/payment \ --force这里--path-rename只对匹配到的路径生效,而且可以和--path组合使用。它会重写历史里所有相关路径,比迁移完再git mv更好,因为后者会留下一整段“移动文件”的历史噪声。
不过我不建议一次性做太多改名。如果新仓库本身已经有一些结构,先保持原有路径,导入成功后再用git mv移动会更容易排查问题。只有在新仓库是全新空仓、路径又明显不合理的时候,才值得在 filter-repo 阶段顺手改名。
3.4 导入目标仓库并保留提交信息
重写完源仓库的临时副本后,回到目标仓库,把这份历史拉进来:
cd /path/to/payment-server git remote add migration /path/to/monolith-migrate git fetch migration master:refs/heads/import/payments我习惯先拉到独立分支,而不是直接合并到主干。这样可以在合并前做一次完整的 diff 和 review:
git log --oneline import/payments git diff import/payments..master --stat确认没问题后,再合并进来。由于两个仓库之前没有共同祖先,必须使用--allow-unrelated-histories:
git merge import/payments -m "feat(payments): import payment module from monolith"如果目标仓库已经有同名文件,这个 merge 会产生冲突。解决冲突后提交,然后删除临时 remote 和分支:
git remote remove migration git branch -D import/payments注意,git merge会用三分量合并,因为两份历史的根 commit 不同,很多文件会被当作新增而不是冲突。这通常是合理的,但如果有少量文件恰好同名且内容相似,一定要在合并前先看 diff。
3.5 迁移后的验证:不只对比文件,还要对比commit id
代码merge进去后,至少要验证三件事。
第一,文件完整性。在旧仓库克隆副本里生成一个文件清单,再在新仓库里生成同一份清单,diff 一下:
cd monolith-migrate git ls-files | sort > /tmp/old-files.txt cd /path/to/payment-server git ls-files | sort > /tmp/new-files.txt diff /tmp/old-files.txt /tmp/new-files.txt第二,提交内容是否对得上。由于 filter-repo 改写了提交 hash,不能用 commit ID 直接找对应关系,但可以通过提交 message、作者、时间戳这些维度抽样核对。比如拿旧副本里的某个中间提交的 message 在新仓库的 git log 里搜索,找到它,看它下面改动的文件是不是也在迁移范围内。
第三,构建验证。这一步比前面任何检查都重要,因为代码迁移光是“文件对得上”没用,跑不通就是废的。拉到最新分支之后立刻构建,编译错误会非常诚实地告诉你还有哪些依赖没搬过来。
4. 迁移之后最容易翻车的四件事:依赖、CI、模块路径和团队同步
4.1 依赖关系梳理:用一把好用的 grep 把所有隐性引用找出来
filter-repo 能把文件和历史搬过来,但它不会自动改写代码里的 import。迁移完第一件事就是全项目搜索旧仓库的包名、模块名、路径前缀。
假设旧仓库 Redis 连接工具类叫com.example.monolith.common.cache,支付模块里大量使用它。迁移后,要么把这个工具类也搬过来,要么把它替换为新仓库里已有的实现。
我通常会用一个临时清单:
grep -R "com.example.monolith" --include="*.java" . grep -R "from common" --include="*.py" . grep -R "require\('.*monolith" --include="*.js" . grep -R "monolith-server" --include="*.gradle" --include="pom.xml" .凡是搜出来的结果,逐个确认是“应该替换为新仓库的等价引用”,还是“这个依赖本来就不该被支付模块使用,迁移前已经被宿主的旧仓库默默继承了”。后者往往是被隐藏的最深的问题。
4.2 构建与CI脚本里的绝对路径和分支依赖
这个坑我印象特别深。之前迁移一个 Python 服务,代码搬完编译都过了,一执行部署就报“找不到配置文件”。最后发现是部署脚本里写死了/opt/monolith/payments/config.yml,而新仓库的部署路径是/opt/payment/config.yml。
CI 同样会有问题。旧仓库的 GitLab CI 或 GitHub Actions 里,可能把构建依赖的某个私有仓库地址写在环境变量里,也可能把上传制品包的路径写成了带旧项目名的地址。迁移后需要把所有 CI 配置文件翻一遍:
.gitlab-ci.yml.github/workflows/*.ymlJenkinsfile.circleci/config.yml- Dockerfile 中的
WORKDIR和COPY路径 - 集群部署用的 helm values 或 k8s manifest
我的建议是不要等 CI 报错再改,而是在迁移后的第一次 CI 运行前,就手动把所有写死的旧项目名、旧路径列出来,做一个全局替换。替换之后还要确认分支策略:旧仓库的 master 不等于新仓库的 master,触发流水线的分支名、tag 规则都要同步调整。
4.3 模块名和导入路径的重构
如果旧仓库是单体服务,模块名往往跟仓库名强绑定。进入新仓库后,最怕的就是代码里到处都是旧仓库的包名,却还叫“新服务”。
这不仅仅是风格问题。Java 的package、Go 的module、Python 的import、Node 的package.jsonname 都会影响依赖解析。比如 Go 服务迁移后,如果不修改go.mod里的 module 路径,本地能跑,但其他同事拉下来后一旦有跨包引用,就会因为路径对不上而无法编译。
拿 Go 举例,迁移前是:
package monolith/payments/service迁到payment-server后,至少要改成:
package payment/service并且go.mod第一行也要同步改。对于大型代码库,这种改名别全手动,最好用 IDE 的 rename 功能,或者配合脚本做批量替换。改完之后跑一次完整测试,不要只编译。
模块名改动的真正难点在于它会破坏“历史连续性”。代码原来叫monolith/payments/something.go,现在叫payment/something.go,将来git blame时看到的是迁移合并那条消息,而不是具体每一次改动的原始记录。这个心理预期要提前跟团队讲清楚。
4.4 团队协作:hash变化和PR重新提交
迁移完,旧仓库里可能还浮现着二三十个未合并的 MR/PR,里面都改到了支付模块的代码。filter-repo 改写了所有 commit hash,旧 PR 里的 commit 跟新仓库的历史已经对不上了。如果你把新仓库远程地址告诉了团队,让他们直接往新仓库发 MR,这些分支过去之后大概率会因为“历史不连续”出现诡异冲突。
我的做法是给团队留两到三天的“缓冲期”,期间旧仓库允许继续小幅度改动,但属于支付模块的改动都要同时登记到迁移清单里;迁移合并完成后,所有人基于新仓库 master 重新拉分支,旧 PR 能关就关,不能关的重新在新仓库里提一版。
另外,因为 filter-repo 会删除 origin,重写后仓库里的所有 commit hash 都变了,任何基于旧 hash 的自动化脚本都要失效重配。像是部署系统里记录“上次构建版本号 = 某 commit hash”、监控面板里链接到某个 commit 的功能,都需要同步更新。
5. 没有filter-repo也能干活:read-tree和checkout路径导入的轻量方案
虽然 filter-repo 是当前最推荐的方案,但不是所有环境都能安装它。Python 环境受限、公司安全策略禁止装第三方工具、或者只是想快速同步一下代码的时候,Git 自带命令也能承担一部分工作。
5.1 git checkout -- path:最常见的无历史导入
这是最轻量的文件级导入方案:
cd /path/to/new-repo git fetch /path/to/old-repo master git checkout FETCH_HEAD -- src/payments git commit -m "chore: import current payments code"这条命令会把旧仓库src/payments目录下的最新文件直接写到新仓库的工作区和索引里。它不保留任何历史,但它有一个额外优点:如果新仓库里已经有一份同名文件,checkout会提示冲突,你可以先备份再决定怎么覆盖。
缺点也很明显:如果把模块是分阶段开发出来的,比如 v1 有几十个文件,v2 把其中 5 个文件改名了,git checkout FETCH_HEAD -- src/payments只会把当前分支对应的整个目录拿过来,目录下被删掉的文件并不会自动从新仓库里清除。所以这条命令更适合“新仓库里本来没有这个目录”的场景。
5.2 read-tree --prefix:把旧仓库的某个tree塞进新仓库的子目录
如果想把旧仓库的某个目录完整地导入到新仓库的指定子目录下,并且新仓库里已经有了别的内容,用read-tree会更精准。
cd /path/to/new-repo git fetch /path/to/old-repo master git read-tree --prefix=modules/payments/ FETCH_HEAD:src/payments git checkout-index -a git commit -m "chore: import payments tree into modules/payments"这里FETCH_HEAD:src/payments是取旧仓库最新提交里src/payments这个 tree 对象,--prefix=modules/payments/决定把这个 tree 放到新仓库的什么位置。read-tree默认只更新 index,所以后面还需要checkout-index -a把文件展开到工作区。
这条命令不会保留提交历史,但它能做到“只把指定 tree 塞进指定位置”,比粗暴复制整个目录要干净。适合在小型团队、工具受限、也不在乎历史的情况下快速完成目录级迁移。
5.3 轻量方案的适用场景和局限
这类方案最大的局限是:它们做的都是“状态迁移”,不是“过程迁移”。代码最终长什么样能保证,但代码是怎么一步步变成这样的,过程信息全部丢失。
如果迁移目的只是为了归档,或者新仓库只是运行代码的容器,不需要做审计和复盘,那没问题。但如果是核心业务模块,以后还要继续在上面积累需求、排查线上事故,我强烈建议至少用subtree split,预算允许就上filter-repo。毕竟git filter-repo唯一真正的门槛是历史重写需要理解 commit hash 变化的含义,而不是它有多难安装。
6. 长期维护视角:迁完之后是“一次性结算”还是“持续同步”
代码搬过去只是开始。真正影响后续开发体验的,是迁完之后选择什么样的仓库关系。
6.1 subtree push/pull:想双向同步时的配置方式
如果你迁移出去的模块以后还要从旧仓库拉回新代码,或者反过来旧仓库要用新仓库的改动,那么在迁移阶段就应该优先用git subtree,而不是filter-repo。原因很简单:filter-repo把两个仓库共同的“根”彻底打掉了,之后再想让两者产生交叉合并,Git 找不到共同祖先,会非常痛苦。
使用git subtree的持续同步流程大致是:
在旧仓库中把src/payments拆分推给新仓库:
git subtree push -P src/payments /path/to/payment-server master之后新仓库的代码变更想要合并回旧仓库:
cd /path/to/payment-server git subtree push -P src/payments /path/to/monolith-server some-branch在旧仓库侧拉取:
git subtree pull -P src/payments /path/to/payment-server master这套机制能工作,但每次 push/pull 都会把整棵子树的历史做一次三路合并,commit 会越来越多,冲突也容易出现在“两边同时改了同一处”的情况下。所以它适合“代码仍然高度耦合、暂时拆不了”的场景,如果你已经定了长期分家,这种双向同步反而会拖累两边独立演进的速度。
6.2 发布成独立包/组件:比源码搬运更干净的模块化
跨仓库迁移部分代码的尽头,往往不是“把代码放进新仓库就完事”,而是“让这段代码变成可以被多个仓库引用的独立单元”。这时候更好的做法是把它打包成独立制品。
Java 可以打 jar 传到私有 Maven 仓库,Node 可以发 npm package,Python 可以发到私有 PyPI,Go 可以直接用 git tag + go mod 引用另一个仓库的模块。把代码变成包之后,消费方不需要关心代码仓库在哪里,只需要在构建配置里声明依赖版本。
这不是否定代码迁移,而是说迁移应该和“模块化”同时进行。我见过太多团队把代码从一个仓库搬到另一个仓库,内部还是一团纠缠的依赖,结果只是换了个地方继续耦合。迁移时如果不顺便把依赖边界切干净,下一次拆分必然还会来一次。
6.3 从代码迁移到仓库治理:一份迁移前检查清单
最后分享一份我每次做跨仓库迁移都会认真过的检查清单,照着打勾能省掉大量返工:
- [ ] 明确历史保留要求:完全保留 / 部分保留 / 可不保留
- [ ] 列出全部待迁移路径,包括配置、脚本、测试、资源和CI文件
- [ ] 扫描代码中所有对源仓库其他模块的依赖,并给出处理方案
- [ ] 确认旧仓库分支和tag中哪些也需要迁移绑定
- [ ] 准备新仓库的分支结构、命名规范和初始README
- [ ] 确定是否需要在迁移过程中改包名、模块名和路径前缀
- [ ] 检查新仓库是否已有同名文件,以及是否有下一步冲突预案
- [ ] 执行迁移后做文件清单diff、提交历史抽样diff和构建验证
- [ ] 更新CI、部署脚本、监控面板、文档中对旧仓库路径的引用
- [ ] 通知团队旧仓库停止对迁移模块的新改动,给出新仓库的PR入口
我在实际迁移里体会最深的一件事:命令本身十分钟就能跑完,真正花时间的永远是“边界到底在哪里”和“搬完后面还有什么跟着一起断掉”。所以如果你现在准备做类似的事,别急着找工具,先把上面这份清单填完,填的过程中你就已经知道该怎么做了。