一招搞定!用 git-filter-repo 拆分 Monorepo 并完美保留提交记录

105 阅读3分钟

想把一个大大的前端 Monorepo 里 project-1project-2project-3project-4 拆分成 4 个小仓库,却又担心历史提交和分支都丢了?别慌,今天带你轻松上手官方推荐的 git-filter-repo,一步一步拆出来,保留所有提交、分支、标签,堪称搬家般顺滑!


🚀 整体思路

  1. 干净克隆
    先给仓库来个“Fresh Clone”,确保本地没半点脏数据。
  2. 循环拆分
    按照 packages/project-x/ 逐个「抽取」目录,只保留对应子项目的提交,并把内容搬到根目录。
  3. 一次搞定所有分支 & 标签
    --refs refs/heads/* --refs refs/tags/*(或简单的 --all)一次性处理,分支、标签、PR 都不掉链子。
  4. 推送新仓库
    配好新 remote,就能把完整历史推上去,开开心心写新功能。

📦 前置准备

  • 安装 git-filter-repo(替代 filter-branch,靠谱又快)
    # macOS(Homebrew)
    brew install git-filter-repo
    
    # 或者 Python Pip
    pip install git-filter-repo
    
  • 务必在「Fresh Clone」上操作,让 git-filter-repo 知道这是一次全新搬家。
  • 强烈建议 先在离线或临时目录试跑一遍,别在生产库里玩命令。
  • 如果你的 Monorepo 里还有子模块,得额外给子模块也跑一遍同样流程。

🔨 拆分单个项目示例

下面以 project-1 为例,其他项目同理:

1. Fresh Clone

git clone --no-local git@your.host:your-org/monorepo.git mono-split
cd mono-split

提示:--no-local 可以防止 Git 在同机做本地快照拷贝,确保“真正”干净克隆。

2. 移除旧的 remote

git remote remove origin

我们要防止后续命令误推回原 Monorepo,先把旧的 origin 干掉。

3. 运行 git-filter-repo

git filter-repo \
  --path packages/project-1/ \
  --path-rename packages/project-1/:/ \
  --refs refs/heads/* --refs refs/tags/* \
  --force
  • --path:只保留 packages/project-1/ 下的提交。
  • --path-rename:把这部分内容搬到新仓库的根目录。
  • --refs refs/heads/* --refs refs/tags/*:保证分支、标签都处理到。
  • --force:跳过 Fresh Clone 限制(确认自己在安全环境下再加)。

4. 配置新 remote & 推送

git remote add origin git@your.host:your-org/project-1.git
git push -u origin --all
git push     origin --tags
  • --all:把所有分支都推上去。
  • --tags:标签也别忘了。

5. 验收

git log --oneline       # 看看提交历史  
git branch -a && git tag  # 确认分支和标签都齐活儿了  

🛠️ 一脚本批量拆分(Node.js 版)

省事秘诀来了!把下面代码保存为 split-monorepo.js,直接 node split-monorepo.js 跑起来:

#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const MONO_REPO = 'git@your.host:your-org/monorepo.git';
const PROJECTS = ['project-1','project-2','project-3','project-4'];

function run(cmd, cwd) {
  console.log(`\n> ${cmd}`);
  execSync(cmd, { stdio:'inherit', cwd });
}

PROJECTS.forEach(proj => {
  const dir = `split-${proj}`;
  console.log(`\n✨ 拆分 ${proj}${dir}`);

  // 干净开始
  if (fs.existsSync(dir)) fs.rmSync(dir,{recursive:true,force:true});
  run(`git clone --no-local ${MONO_REPO} ${dir}`, process.cwd());

  const cwd = path.join(process.cwd(), dir);
  run(`git remote remove origin`, cwd);

  run([
    'git filter-repo',
    `--path packages/${proj}/`,
    `--path-rename packages/${proj}/:/`,
    '--refs refs/heads/* --refs refs/tags/*',
    '--force'
  ].join(' '), cwd);

  const newRepo = `git@your.host:your-org/${proj}.git`;
  run(`git remote add origin ${newRepo}`, cwd);
  run(`git push -u origin --all`, cwd);
  run(`git push origin --tags`, cwd);

  console.log(`✅ ${proj} 完成`);
});
console.log('\n🎉 全部拆完,OK!');

Tip: 给脚本加上执行权限:

chmod +x split-monorepo.js

💡 小贴士 & 常见坑

  • 子模块:要拆的子模块里也得各自跑一遍这套流程。
  • 演练验证:在测试环境先跑一遍,确认目录、提交、分支都没问题再上生产。
  • 仓库备份:真的,备份!打个 tag、拷个 bundle,出了问题好回滚。
  • CI/CD 别忘改:拆完之后记得把各子项目的流水线、文档链接、依赖配置都跟着更新。