git subtree split 拆目录:保留提交历史的仓库拆分实践
发布时间:2026/10/26 20:48:52 锦皓数字建站

仓库膨胀这件事往往是等到 clone 要等五分钟、IDE 索引卡死、CI 每次拉代码都超时的时候才被重视。前段时间接手一个项目主仓里同时躺着移动端、后台服务、共享组件和一堆历史遗留的脚本目录分支图像毛线团一样拧在一起。团队决定把其中一块业务目录独立出去要求只有一个提交历史不能丢。这类需求在 Git 里有个很直接的解法就是git subtree split把大项目按目录拆成独立仓库。它不像简单复制文件那样把历史一刀切掉而是沿着目录的变更轨迹重新组织提交。下面我把这次拆分的完整过程、使用要求、执行细节和替代方案摊开讲适合正在维护 monorepo、准备做仓库拆分或者被大仓库拖慢节奏的同学参考。1. 拆分大项目的真实动机与方案选型1.1 什么样的仓库该拆了判断一个仓库该不该拆不能只看体积。我见过只有两百多兆但提交数超过八万的仓库也见过接近两个G却依然跑得动的仓库。真正需要拆分信号通常来自协作层面而不是磁盘占用。比如某个目录的发布节奏和其他目录完全不一致别人一周发一次版它一天要发三次比如权限边界要求某部分代码只能让特定小组修改但仓库级权限做不到这么细再比如 CI 每次都要全量安装依赖、全量跑测试改一行文档也要等二十分钟流水线。下面这张表是我常用的判断清单命中三条以上就值得认真考虑拆分信号具体表现拆分收益克隆与索引慢clone 超过三分钟IDE 索引频繁卡顿新仓库体积和提交数下降发布节奏割裂同一仓库内多个目录版本号各自独立独立打 tag、独立发布权限边界模糊外包或不同小组需要目录级权限仓库级权限更清晰CI 全量构建改任意文件都触发全部流水线按仓库触发节省算力目录耦合度低目录之间几乎没有互相 import拆分后依赖关系简单历史噪声大搜索代码时混入大量无关目录代码检索更聚焦反过来如果目录之间调用关系密集、共享大量工具函数、构建脚本深度耦合拆分的代价可能大于收益。我一般会先跑一遍目录间引用统计用git grep搜跨目录的相对路径引用用构建工具的依赖图看模块关系。如果两个目录的改动在提交历史里经常成对出现说明它们本质上是一个整体硬拆只会把耦合从代码里搬到发布流程里。还有一个容易被忽略的点拆分不是目的降低协作成本才是。有些团队一上来就把仓库切成十几个微仓结果联调时要同时开十几个窗口版本对齐比原来更痛苦。我的建议是先拆最独立、最有明确边界的那一块跑三个月再决定要不要继续。1.2 拆分不是复制粘贴历史为什么重要很多人第一反应是新建一个仓库把目标目录的文件拷过去提交一条“初始化”就完事。这样做快是快但代价在后面才会显现。git blame全部指向那条初始化提交谁在什么时候为什么改了这行代码彻底查不到线上出问题想用git bisect定位历史断了二分无从下手代码审计要求追溯某段逻辑的引入时间只能去旧仓库翻翻完还要手动对照路径。保留历史的价值不只是“好看”。第一责任追溯。老代码的坑往往藏在五年前的提交信息里提交信息里可能写着当时的业务背景和取舍原因。第二问题定位。git log -S可以按代码内容搜索引入和删除的提交历史完整时这是排查回归的利器。第三合规与审计。有些行业要求保留完整的变更记录复制粘贴拿不出证据链。第四迁移成本。历史完整的仓库可以直接接上原有的 issue 引用、PR 链接和 tag团队不用重新建立心智映射。git subtree split的核心价值就在这儿它读取指定目录在所有提交中的树对象把该目录的内容“提升”到仓库根目录重新生成一条只包含这些变更的提交链。原来的提交作者、提交时间、提交信息都会保留只是 commit SHA 会变因为树对象和父提交变了。这个细节很重要后面讲校验的时候会展开。1.3 四种拆分路线对照Git 生态里做仓库拆分不止一种手段选错工具会让后续维护很别扭。我把常见的四条路线列出来方便你按场景对号入座方案历史保留操作难度适用场景主要风险git subtree split保留目标目录相关提交低单目录拆出、历史基本线性重命名历史可能丢失大仓较慢git filter-repo保留并可重命名路径中多目录、复杂路径、需要精细过滤需要额外安装重写全部 refsgit submodule不拆分历史只做引用中新仓库已存在主仓只想引用子模块指针管理复杂物理复制不保留低临时交付、代码归档、无审计要求blame 和 bisect 失效如果目标目录的历史比较干净没有经历过多次大重命名git subtree split是最省事的。它不需要额外装工具Git 自带命令也直观。如果目录在历史中改过名字、搬过位置或者你想一次性拆多个目录git filter-repo更合适它可以用--path指定多个路径用--path-rename调整目录结构还能保留 tag 和分支。git submodule严格来说不是拆分工具它解决的是“主仓如何引用一个独立仓库”适合拆分完成之后的聚合阶段。物理复制只推荐给一次性交付或者根本不需要追溯的代码。我这次选的路线是git subtree split原因有三个目标目录从创建以来路径没变过历史线性没有复杂合并团队不想引入新工具减少学习成本拆分后只需要保留主分支和最近两个发布 tag对多分支重写没有强需求。如果你的情况更复杂直接跳到第 4 节看filter-repo的方案。2. 使用要求动手前必须满足的前置条件2.1 仓库干净与基线冻结拆分操作最怕一边拆一边有人提交。git subtree split读取的是某个时间点的提交图如果期间有人往目标目录推代码split 结果可能不包含最新变更也可能因为引用变化导致结果不一致。所以第一步不是敲命令而是冻结基线。具体做法是提前一天在团队里发通知要求所有针对目标目录的开发合并到主分支拆分窗口期内不要再开新分支。然后确认工作区干净git status不能有未提交改动所有本地分支该推的推、该删的删。接着打一个基线 tag名字要能看出来是拆分前状态比如pre-split-2024方便出问题时回滚。最关键的一步是做完整备份。不要只依赖远端仓库远端也可能被误推。我的习惯是做一份裸镜像git clone --mirror gitexample.com:team/main-repo.git main-repo-backup.git--mirror会把所有 refs、tag、远程分支原样复制包括一些不常见的引用。备份文件放到独立磁盘或者对象存储确认大小和 refs 数量无误再继续。有人觉得这一步多余直到某次 split 把主仓的--rejoin合并提交推上去导致所有人都要重新拉代码才想起来备份的价值。还有一个细节确认仓库没有正在进行的 rebase、merge、cherry-pick。.git目录下如果有rebase-merge、MERGE_HEAD这类文件说明状态不干净先处理完再操作。可以用git status和ls .git双重确认。2.2 目录边界与依赖关系梳理拆分之前必须把目标目录的边界画清楚。这里的边界不只是文件夹路径还包括它依赖了谁、谁依赖了它、构建时从哪里找配置文件。先看目标目录内部有没有嵌套的子目录也需要独立。比如packages/business-a下面还有个packages/business-a/shared如果这个 shared 只被 business-a 使用跟着一起走没问题如果多个目录都在用就要考虑它是不是应该单独拆或者留在主仓作为公共包。我一般用下面几条命令做初步统计# 查看目标目录最近一年的提交量 git log --since1 year ago --oneline -- packages/business-a | wc -l # 查看目标目录和其他目录同时被修改的提交 git log --since6 months ago --name-only --prettyformat: -- packages/business-a packages/shared | sort | uniq -c | sort -rn | head -20第二条命令能看出目录之间的“共变关系”。如果business-a和shared经常在同一个提交里出现说明它们耦合紧拆开后要么把 shared 一起带走要么在新仓库里通过包管理器引用主仓的 shared后者会带来跨仓联调成本。然后搜跨目录引用。前端项目重点看相对路径 import后端项目看模块引用和构建配置。比如git grep -n packages/shared -- packages/business-a git grep -n \.\./\.\./shared -- packages/business-a把结果整理成清单逐条判断是改成包依赖、还是复制一份、还是保留相对路径。相对路径在拆分后大概率失效因为新仓库的根目录变了。这一步偷懒拆分后构建必挂。还要检查配置文件里的路径。tsconfig.json的paths、webpack 的 alias、go.mod的 module 名、Maven 的 parent 相对路径、Dockerfile 的COPY指令、CI 配置里的working-directory这些都不会因为 Git 历史拆分而自动调整。我的做法是列一张“路径依赖表”每改一处打个勾后面 CI 报错时对着表排查比盲猜快得多。2.3 工具版本与环境准备git subtree在较新的 Git 版本里是内置的但有些发行版的 Git 编译时没带 contrib 脚本执行git subtree会提示不是 Git 命令。先确认git --version git subtree --help如果提示找不到命令Linux 上可以检查/usr/lib/git-core/git-subtree是否存在或者安装对应发行版的 contrib 包。macOS 用 Homebrew 安装的 Git 一般自带。Windows 的 Git for Windows 也自带 subtree。版本上建议至少 2.25 以上老版本在--annotate和合并点识别上有一些行为差异。如果准备用git filter-repo作为替代或补充需要 Python 3 环境安装方式通常是pip install git-filter-repo或者直接下载单文件脚本放到PATH里。它不属于 Git 官方内置命令但社区接受度很高。磁盘空间要留够。拆分过程会在.git里生成新的对象大仓库可能临时膨胀一倍。我的经验是至少留出原仓库体积的两倍空间如果原仓库有 LFS 对象还要把 LFS 缓存算进去。操作前用du -sh .git看一眼心里有数。权限方面确认你对源仓库有读取权限对目标新仓库有创建和推送权限。如果新仓库要走审批流程提前建好空仓库避免 split 完了卡在推送环节。还有 CI 的 token、部署密钥、webhook这些通常绑定在仓库级别新仓库需要重新配置提前列清单。3. git subtree split 的执行过程与实现细节3.1 命令参数逐项拆解git subtree split的常用形式是git subtree split --prefixpackages/business-a --branchbusiness-a-split --annotatesplit from main repo--prefix指定要拆的目录路径必须从仓库根目录算起不能写./packages/business-a也不能写绝对路径。末尾不要带多余斜杠虽然多数情况能容忍但不同版本行为不完全一致统一写成packages/business-a最稳。--branch指定拆分结果放到哪个本地分支。这个分支如果已存在命令会失败或者覆盖取决于版本所以每次执行前先确认分支名没被占用。我习惯用split/目录名-日期这种命名避免和日常分支混淆。--annotate给每个重写后的提交追加一行说明方便以后看出来这条历史是拆分产生的。这个参数不影响代码但排查问题时很有用。如果不加重写后的提交信息就是原提交信息看起来和普通提交一样。还有一个参数--rejoin需要特别小心。它的作用是在当前仓库里创建一个合并提交把 split 出来的历史接回主仓目的是让下次再 split 同一个目录时能复用之前的计算结果提升速度。但它会改写主仓的历史结构所有协作者都要重新同步风险很高。除非你明确知道自己在做什么并且团队能接受历史变更否则不要加--rejoin。我这次拆分没有用因为只拆一次不需要复用。--ignore-joins用于忽略之前的 subtree 合并点一般在历史里有 subtree 合并记录、但你想强制重新计算时使用。普通拆分场景用不到。还有个隐藏细节git subtree split默认从当前HEAD可达的提交里找目标目录的变更。也就是说它只看当前分支的历史。如果你的目标目录在其他分支上有独立提交那些提交不会进入拆分结果。需要多分支的话要么切到对应分支分别 split要么用filter-repo的--refs一次性处理。这一点很多教程没讲清楚导致拆完发现某个分支的修复丢了。3.2 一次完整的拆分实操记录下面是我这次的实际操作顺序命令都经过脱敏处理路径和仓库名替换成了示例。第一步在备份仓库的同级目录做一次普通克隆不要用浅克隆。浅克隆缺少历史split 会失败或者结果不全。git clone gitexample.com:team/main-repo.git main-repo-split cd main-repo-split git checkout main git pull --ff-only第二步确认目标目录存在且路径正确记录当前 HEAD 的 SHA方便后面核对git rev-parse HEAD git ls-tree --name-only HEAD packages/business-a第三步执行拆分。这个过程在大仓库上可能跑几分钟到几十分钟取决于提交数量和目录变更频率。终端没有进度条不要以为卡死了。我这次跑了大约六分钟输出最后一行是拆分结果的 commit SHA。git subtree split \ --prefixpackages/business-a \ --branchsplit/business-a-2024 \ --annotatesplit from main repo at $(date %Y-%m-%d)第四步检查拆分分支。先看提交数量再看文件结构git log --oneline split/business-a-2024 | head -20 git ls-tree --name-only split/business-a-2024预期结果是目标目录的内容出现在根目录比如原来的packages/business-a/src/index.ts变成src/index.ts提交信息保留原样末尾多了一行 annotate 内容提交数量应该和目标目录相关的提交数量接近但不会完全相等因为有些提交可能只改动了目录外的文件被过滤掉了。第五步创建新仓库并推送。先在远端建一个空仓库不要勾选初始化 README否则推送时会因为历史不相关被拒。然后git remote add business-a gitexample.com:team/business-a.git git push business-a split/business-a-2024:main如果新仓库默认分支不是main推送后再去仓库设置里改默认分支。推送完成后在另一台机器上重新克隆新仓库验证文件完整性和历史可读性。这一步不要省本地看着正常不代表远端对象完整。第六步处理 tag。原来的 tag 指向旧仓库的提交拆分后的提交 SHA 变了tag 不会自动跟过来。如果业务上需要保留发布记录可以在新仓库里对照旧 tag 的日期和提交信息重新打 taggit tag -a v1.4.0 split-commit-sha -m migrated from main repo tag v1.4.0 git push business-a --tags注意不要直接把旧仓库的所有 tag 推过去那些 tag 指向的提交在新仓库里不存在推上去会报错或者产生悬空引用。只挑和目标目录相关的 tag 重新打。3.3 拆分结果校验历史、文件、签名、LFS拆分完成后光看文件在不在是不够的。我通常做四层校验每层都有对应的命令和判断标准。第一层提交历史连续性。用git log --oneline --graph看提交链有没有断裂特别关注合并提交。subtree split会尽量保留合并结构但如果原历史里有复杂的交叉合并结果可能被简化。判断标准是目标目录相关的关键提交都能找到提交顺序和原仓库一致。git log --oneline --graph --decorate split/business-a-2024 | head -50 git rev-list --count split/business-a-2024第二层文件完整性。把拆分后的文件列表和原仓库目标目录的文件列表做对比。最直接的办法是在原仓库执行git ls-tree -r HEAD packages/business-a在新仓库执行git ls-tree -r HEAD去掉路径前缀后 diffgit -C main-repo-split ls-tree -r --name-only HEAD packages/business-a | sed s#^packages/business-a/## | sort /tmp/old-files.txt git -C business-a ls-tree -r --name-only HEAD | sort /tmp/new-files.txt diff /tmp/old-files.txt /tmp/new-files.txt没有输出说明文件集合一致。如果有差异重点看是不是空目录、被.gitignore忽略的文件、或者子模块目录。Git 不跟踪空目录原仓库里如果有空目录占位拆分后不会保留需要手动补.gitkeep。第三层提交签名。如果原仓库启用了 GPG 签名重写后的提交签名会失效因为签名覆盖了提交内容内容变了签名自然对不上。git log --show-signature会显示签名无效。这不是拆分出错而是重写历史的必然结果。处理方式有两种接受签名失效在提交说明里备注或者用脚本重新签名但工作量大。我一般选择接受并在团队里说明原因。第四层LFS 对象。如果目标目录里有大文件走 LFS拆分后的仓库需要确认 LFS 指针能正常解析。先看.gitattributes有没有跟着过来再执行git lfs ls-files git lfs fetch --all git lfs checkout如果git lfs ls-files列出文件但 checkout 失败说明 LFS 对象没有推送到新仓库的 LFS 服务端。需要在拆分仓库里执行git lfs push --all business-a把对象补上去。这一步经常被忽略导致新仓库克隆后大文件是空的。3.4 多目录与嵌套目录的处理git subtree split一次只能处理一个--prefix。如果要把多个目录拆到同一个新仓库需要按顺序 split然后把结果合并。比如packages/a和packages/b要合成一个仓库可以这样做git subtree split --prefixpackages/a --branchsplit/a git subtree split --prefixpackages/b --branchsplit/b git checkout -b combined split/a git merge --allow-unrelated-histories split/b合并时大概率会有冲突因为两个目录的内容都被提升到了根目录同名文件会撞车。这时候需要提前规划目录结构比如在拆分前就把packages/a和packages/b的内容放到不同子目录或者合并后用git mv整理。我的建议是尽量避免一次拆多个目录到同一个仓库除非它们本来就该在一起。嵌套目录的情况更麻烦。比如你要拆packages/business-a但它下面有个packages/business-a/vendor是独立子项目。subtree split会把 vendor 一起带走因为它在 prefix 范围内。如果 vendor 不该跟着走拆分前先把它移出去或者拆分后用filter-repo删掉。反过来如果你要拆的是packages/business-a/vendorprefix 直接写这个更深的路径就行但要注意它上面的目录历史不会跟过来。中文路径和大小写也有坑。Git 对路径大小写敏感但 macOS 和 Windows 的默认文件系统不敏感。如果历史里有Packages/Business-A和packages/business-a混用split 可能只匹配其中一种写法。操作前用git log --name-only搜一下目标目录的大小写变体确认没有混用。中文路径一般没问题但终端编码要统一 UTF-8否则--prefix匹配不上。4. 替代解决方案与反向聚合4.1 git filter-repo更快更彻底的路径重写git filter-repo是这几年做仓库重写的首选工具官方文档也建议用它替代git filter-branch。它的优势是速度快、参数清晰、支持多路径和多 refs。安装后基本用法如下git filter-repo --path packages/business-a --path packages/shared --path-rename packages/business-a/: --path-rename packages/shared/:shared/这条命令的意思是只保留packages/business-a和packages/shared两个路径的历史然后把packages/business-a/下的内容提升到根目录把packages/shared/下的内容放到shared/目录。--path-rename的冒号左边是原路径前缀右边是新路径前缀末尾斜杠不能少。如果只想删除某个目录而不是保留用--invert-pathsgit filter-repo --path packages/legacy --invert-paths这条命令会删除packages/legacy的所有历史其他路径保留。适合给仓库瘦身把历史遗留的大目录彻底清掉。--refs可以指定处理哪些引用默认只处理当前分支。如果要把所有分支和 tag 都重写用--refs HEAD --refs refs/heads/* --refs refs/tags/*或者干脆不加限制。注意filter-repo默认会删除origin远程防止误推执行完需要重新添加git remote add origin gitexample.com:team/business-a.git它还会在.git/filter-repo/下生成报告记录哪些提交被保留、哪些被丢弃排查问题时很有用。相比subtree splitfilter-repo对重命名历史的处理更灵活但也不是全自动如果文件从old/path移到packages/business-a你需要把old/path也加进--path否则重命名前的历史还是会丢。这一点要有预期。还有一个实用参数--partial允许在非全新克隆的仓库上运行。默认情况下filter-repo要求仓库是新鲜的防止误操作。加--partial可以跳过这个检查但风险自负。4.2 用 submodule 或 subtree 把拆出的仓库接回去拆分完成后主仓可能还需要引用新仓库的代码。这时候有两种常见方式submodule 和 subtree。submodule 的做法是在主仓里执行git submodule add gitexample.com:team/business-a.git packages/business-a git commit -m replace business-a with submodule主仓里会多一个.gitmodules文件和一个指向新仓库特定提交的指针。好处是主仓不会包含新仓库的代码历史体积小坏处是协作者克隆时要加--recurse-submodules否则目录是空的。更新子模块也要显式操作git submodule update --remote packages/business-a如果团队不熟悉 submodule很容易出现“本地能跑、CI 拉不到代码”的问题。subtree 的做法是把新仓库的内容合并进主仓的一个子目录git subtree add --prefixpackages/business-a gitexample.com:team/business-a.git main --squash--squash会把新仓库的历史压缩成一个提交避免主仓历史膨胀。以后更新用git subtree pull。subtree 的好处是协作者不需要额外操作克隆主仓就能拿到全部代码坏处是主仓体积会变大而且subtree pull偶尔会产生冲突需要手动解决。怎么选如果新仓库是独立发布、主仓只是偶尔引用用 submodule如果主仓需要频繁修改新仓库代码、并且希望一次克隆搞定用 subtree。我这次拆分的目录是独立业务线主仓不再直接改它的代码所以选了 submodule并在 README 里写清楚初始化和更新命令。4.3 物理复制与历史截断的适用边界有些场景确实不需要保留历史。比如给外部合作方交付一份代码快照对方只关心当前版本能不能跑比如某个目录是自动生成的历史没有参考价值再比如仓库历史里有敏感信息必须彻底切断。这些情况下物理复制反而更合适。物理复制的正确姿势不是直接cp -r而是用git archive导出指定目录这样不会把.git和忽略文件带过去git archive --formattar HEAD:packages/business-a | tar -x -C /tmp/business-a-export导出后在新目录初始化仓库、提交历史只有一条。如果以后需要追溯可以在提交信息里写上源仓库的地址和源提交 SHA至少留一条线索。物理复制的风险要提前说清楚git blame失效git bisect失效issue 里引用的旧提交链接全部断掉。如果团队接受这些代价那就是合理选择。我见过一些团队为了“看起来历史完整”用脚本批量伪造提交日期结果提交顺序和真实开发顺序对不上反而误导排查。与其伪造不如老老实实写一条初始化提交说明来源。5. 拆分后的工程收尾与问题排查5.1 构建、依赖与CI的路径修正代码推送到新仓库只是开始构建跑通才算拆完。前端项目重点看三处package.json的workspaces配置、tsconfig.json的paths别名、构建工具的 alias。拆分后目录层级变了原来的shared/utils可能指向一个不存在的路径需要改成包依赖或者相对路径。后端项目看模块声明。Go 项目要改go.mod的 module 名所有内部 import 跟着改Java 项目要检查pom.xml的 parent 相对路径和模块聚合配置Python 项目看setup.py或pyproject.toml里的包发现规则。这些改完后跑一次全量构建把报错逐个消掉。CI 配置是最容易漏的。原来的流水线可能写了paths: packages/business-a/**做触发过滤拆到新仓库后这个过滤条件永远不匹配流水线看起来正常但从来不跑。还有缓存 key 里带了目录名、构建命令里写了cd packages/business-a、制品上传路径带了前缀这些都要改。我一般会把 CI 配置从头到尾读一遍把所有出现旧路径的地方列出来改完再跑一次空提交验证触发。依赖安装也要注意。如果新仓库依赖主仓的某个公共包而那个包没有发布到私有 registry构建会失败。临时方案是在新仓库里用replace或file:协议指向本地路径长期方案是把公共包正式发布。不要用 Git URL 直接引用主仓那样每次构建都要拉整个主仓拆分就白拆了。5.2 权限、分支保护与协作流程重建新仓库建好后权限是空的。原来主仓的权限模型可能是一个大团队全读写拆出来后需要重新划分谁有合并权限、谁只能提 PR、谁可以强制推送。分支保护规则也要重建包括必需审查人数、必需通过的 CI 检查、是否允许直接推送。这些看起来是管理配置但漏了会导致代码质量失控。CODEOWNERS文件要跟着更新。原来主仓里的规则可能写的是packages/business-a/ team-a新仓库里路径变成根目录规则要改成* team-a或者按子目录细分。PR 模板、issue 模板、贡献指南这些文件如果原来放在主仓根目录拆分时不会跟过来需要手动复制并调整内容。tag 和 release 流程也要重新约定。新仓库的版本号从哪个号开始是延续旧目录的版本号还是从1.0.0重新开始我建议延续旧版本号避免调用方混淆。发布流水线要改成在新仓库触发制品仓库的路径、部署脚本里的仓库名都要同步更新。还有一个容易忽略的webhook 和外部集成。代码扫描、制品构建、消息通知这些服务通常按仓库配置新仓库需要重新接入。拆分前列一张集成清单拆完后逐项打勾比事后发现某個扫描没跑要省事。5.3 常见问题速查表下面这些是我和同事在多次拆分里踩过的坑整理成速查表遇到问题先对照排查现象可能原因排查与处理split 结果为空--prefix路径写错或大小写不匹配用git ls-tree HEAD确认路径注意末尾不要带斜杠历史提交数量明显偏少只看当前分支其他分支提交未纳入切到目标分支分别 split或改用filter-repo --refs重命名前的历史丢失工具按路径过滤不自动跟踪重命名把旧路径也加入过滤条件或用--path-rename推送被拒新仓库初始化了 README历史不相关新建空仓库或用--force推送确认无协作者后克隆后子目录为空submodule 未初始化执行git submodule update --init --recursiveLFS 文件显示为指针LFS 对象未推送在新仓库执行git lfs push --all remote提交签名无效历史重写导致签名内容变化属预期行为重新签名或备注说明CI 不触发路径过滤条件仍指向旧目录修改paths配置或去掉过滤构建找不到共享包包依赖未发布或路径别名失效发布私有包或临时用本地路径映射subtree add冲突目标目录已存在同名文件先清理目录或改用--squash减少冲突面这张表不是全集但覆盖了八成以上的现场问题。遇到表里没有的先看git subtree的输出版本和 Git 版本再看仓库状态是否干净最后才怀疑工具本身。大部分“拆分失败”其实是路径写错或者分支没切对。5.4 踩坑之后的心得说几个只有实际动手才会体会到的细节。第一拆分前一定要在克隆出来的副本上先跑一遍不要直接在主工作区操作。我习惯用--mirror备份加普通克隆做演练确认 split 结果符合预期再动真的。第二git subtree split的耗时和提交数量成正比和目标目录大小关系不大。一个几十兆但提交数很多的目录split 可能比一个几百兆但提交少的目录更慢。如果仓库有十几万提交提前留出半小时。第三拆分不是一锤子买卖拆完后主仓里那个目录要明确标记为只读或者直接删掉否则过两个月又有人在旧目录里改代码两边分叉合并起来非常痛苦。还有一点关于沟通的体会。技术上的拆分半天能搞定团队习惯的迁移往往要一个月。拆分前把“以后去哪里提 PR、去哪里看流水线、版本号怎么走”写成一段简短说明发在团队常用的沟通渠道里比事后逐个解释有效得多。我在这次拆分里做了一张对照表左边是旧路径右边是新仓库地址和分支谁有疑问直接查表省了很多重复回答。最后分享一个校验小技巧拆分完成后在新仓库里对目标目录的一个核心文件执行git log --follow --oneline -- src/index.ts。如果能看到从旧仓库延续下来的提交记录说明历史迁移基本成功如果只有一条初始化提交说明 split 的路径或者分支选错了回去检查--prefix和当前HEAD。这个检查只要几秒钟但能提前发现大部分历史丢失问题。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。