对比维度代码大仓(Monorepo)普通仓库(Multirepo)
定义单个仓库中存储整个项目(或公司)的所有代码,包括多个应用、库、模块等。每个项目、应用或模块单独作为一个仓库,彼此独立存储。
代码边界代码共享通过内部引用(如相对路径),无物理隔离。代码共享通过包管理工具(如 npm、Maven),有物理隔离。
版本管理整个仓库使用统一版本号(或无版本号),一次提交影响全局。每个仓库独立版本号,变更仅影响单个仓库。
权限控制通常对仓库整体授权,细粒度控制较复杂。可针对单个仓库精确控制权限,安全性隔离性更强。
典型工具Git(配合 Git Subtree)、Google Repo、Perforce 等。Git(默认多仓库模式)、SVN 等。

代码大仓(Monorepo)

优点:

  • 代码共享便捷:不同模块 / 应用可直接引用内部代码,无需发布成外部包,减少 “重复造轮子”。
  • 原子化变更:跨模块的修改可在一次提交中完成,避免多仓库同步更新的繁琐(如 A 依赖 B,修改 B 后需先发布再更新 A)。
  • 统一规范与工具:容易维护统一的编码规范、构建工具和 CI/CD 流程,降低配置碎片化。
  • 简化依赖管理:避免依赖版本冲突,所有代码使用同一套依赖环境(如同一版本的 Node.js、Python)。
  • 便于代码重构:跨模块重构时,可一次性修改所有相关代码并验证,减少遗漏。

缺点:

  • 仓库体积庞大:随着项目增长,仓库体积可能达到 GB 级,拉取、克隆速度慢,占用本地存储空间大。
  • 权限控制复杂:难以精确限制团队只能修改特定模块,可能存在误操作风险(如新手修改核心代码)。
  • 构建效率问题:全量构建耗时极长,需依赖高级工具(如 Bazel、Nx)实现增量构建,学习成本高。
  • 历史记录混乱:所有修改混在一个仓库的提交记录中,定位特定模块的历史变更较困难。
  • 团队协作成本高:多人同时修改时,冲突概率增加,需要更严格的分支管理策略。

适用场景:

适合中小型团队、业务关联紧密的项目(如前端单页应用 + 组件库 + 工具库)、需要频繁跨模块协作的场景(如 Google、Facebook 内部项目)。

Git Subtree 

Git Subtree 是 Git 中用于管理代码仓库间依赖关系的一种技术,允许将一个仓库(子仓库)作为另一个仓库(主仓库)的子目录嵌入,同时保持子仓库的独立性。它是解决代码复用问题的方案之一,尤其适合在不采用 Monorepo 整体架构的情况下,实现部分代码的共享管理。

 引入子仓库(首次添加)

git subtree add --prefix=路径/子目录名 子仓库URL 分支名 --squash
  • --prefix:指定子仓库在主仓库中的存储路径
  • --squash:将子仓库的历史压缩为一个提交,避免主仓库历史过于臃肿
git subtree add --prefix=libs/utils https://github.com/example/utils-repo main --squash

从子仓库拉取更新

git subtree pull --prefix=路径/子目录名 子仓库URL 分支名 --squash

实际开发中,部分团队会采用 “混合策略”(如核心库用大仓管理,业务应用用多仓库),平衡两种模式的优势。

迁移命令

git filter-repo --force --to-subdirectory-filter docs

重写 Git 仓库历史的命令,主要功能是将仓库中所有文件和提交历史 “移动” 到指定的子目录下。

执行后,原来的 readme.md 会变成 docs/readme.md,且所有历史提交中涉及该文件的记录都会自动更新路径。

适用场景

  1. 仓库拆分:从一个大仓库中提取部分内容作为独立仓库,同时保持提交历史
  2. 目录重组:重构仓库结构,将分散的文件整理到统一子目录,且不丢失历史记录
  3. 多项目合并准备:为将多个仓库合并到一个主仓库做准备(每个子项目放在不同子目录)

简单说,这个命令就是在不丢失提交历史的前提下,给仓库里的所有内容 “换个新家”。

git clone –mirror 仓库地址

–mirror 镜像克隆用于完整备份或迁移

  • 用于创建仓库的完整备份(包括所有分支和历史)。
  • 迁移仓库到新平台(如从 GitHub 迁移到 GitLab)。
  • 搭建仓库的镜像服务器(保持与原仓库完全同步)。
NAME="main"
default="main"
git merge --no-ff --allow-unrelated-histories -m "merge $NAME" "$default"

用于合并分支的 Git 命令,包含多个参数以处理特定场景

  • git merge:基础命令,用于将一个分支的修改合并到当前所在分支。
  • --no-ff:禁用 “快进合并”(fast-forward merge)。
    默认情况下,如果目标分支是当前分支的直接延续(没有分叉),Git 会直接移动指针(快进),不产生新的合并提交。
    加上 --no-ff 后,强制生成一个新的合并提交,即使可以快进,这样能清晰保留合并历史(在提交记录中能看到 “合并” 这个动作)。
  • --allow-unrelated-histories:允许合并 “无关联历史” 的分支。
    通常用于两个完全独立的仓库(或分支)首次合并时(比如两个分支没有共同的祖先提交),默认情况下 Git 会拒绝这种合并,加上这个参数可以绕过限制。
  • -m "merge $NAME":指定合并提交的备注信息。
    $NAME 是变量,会替换为具体名称(比如分支名),最终提交信息可能是 merge feature/login
  • "$default":要合并的目标分支(变量,比如 main 或 develop)。

使用场景

  1. 合并两个独立仓库的分支(比如把一个老项目的代码合并到新项目)。
  2. 希望严格保留合并历史,不希望快进合并(方便后续查看分支合并轨迹)。

filter-repo

合并多个仓库历史的实现原理

  1. Git 仓库的本质
    每个 Git 仓库都是一个独立的数据库,包含所有文件的快照(blob)、目录结构(tree)、提交记录(commit)和分支标签(ref)。合并多个仓库的历史,本质是将这些独立数据库中的数据整合到一个新的数据库中,并重新组织提交历史的关联关系。
  2. 核心操作步骤与原理
    假设要将仓库 A、B 合并到目标仓库 C 中,典型流程如下:
    • 步骤 1:初始化目标仓库
      创建一个新的空仓库 C,作为最终的合并结果仓库。
    • 步骤 2:将源仓库作为子目录导入
      分别将仓库 A、B 的所有文件和历史提交,“移动” 到目标仓库 C 的特定子目录下(例如 repo-a/repo-b/),避免文件冲突。
      这一步通过 filter-repo 的 --to-subdirectory-filter 实现,原理是:
      • 重写源仓库的每一个提交,将所有文件路径前添加子目录前缀(如 file.txt → repo-a/file.txt)。
      • 保持源仓库的提交时间、作者、提交信息等元数据不变,仅修改文件的存储路径。
    • 步骤 3:合并提交历史
      将处理后的仓库 A、B 的提交历史依次 “拼接” 到目标仓库 C 中。
      这一步利用 Git 的 git remote add 和 git fetch 将源仓库的提交数据拉取到目标仓库,再通过 git reset 或 git merge 调整分支指针,使目标仓库的分支包含所有源仓库的提交历史。
      由于每个源仓库的文件已被隔离到不同子目录,提交历史可以安全合并而不会产生文件冲突。
  3. 关键技术点
    • 历史重写filter-repo 通过重写源仓库的每一个提交(修改文件路径),确保不同仓库的文件在目标仓库中处于隔离的目录,避免冲突。
    • 数据整合:Git 允许将多个仓库的提交数据(commit、tree、blob)导入到同一个仓库,只要它们的哈希值不冲突(由于文件路径已修改,哈希值自然不同)。
    • 引用调整:通过调整分支和标签的引用,让目标仓库的分支包含所有源仓库的提交历史,形成一个连续的提交链。

简化示例(合并两个仓库)

# 1. 创建目标仓库
mkdir merged-repo && cd merged-repo && git init

# 2. 导入仓库 A 并移动到 repo-a 目录
git remote add repo-a ../repo-a  # 添加源仓库 A 作为远程
git fetch repo-a                 # 拉取仓库 A 的所有数据
git filter-repo --to-subdirectory-filter repo-a --source repo-a/main  # 重写历史,文件放入 repo-a/

# 3. 导入仓库 B 并移动到 repo-b 目录
git remote add repo-b ../repo-b  # 添加源仓库 B 作为远程
git fetch repo-b                 # 拉取仓库 B 的所有数据
git filter-repo --to-subdirectory-filter repo-b --source repo-b/main  # 重写历史,文件放入 repo-b/

# 4. 最终合并后的仓库包含 A 和 B 的所有历史,文件分别在 repo-a/ 和 repo-b/ 目录下

总结

filter-repo 合并多仓库历史的核心逻辑是:通过重写每个源仓库的提交历史(将文件隔离到子目录),再将这些重写后的历史数据整合到同一个仓库中。这种方式既能保留各仓库完整的提交记录,又能避免文件冲突,最终形成一个包含所有源仓库历史的合并仓库。