仓库合并
约 2079 字大约 7 分钟
2026-02-13
场景:把后端仓库合并进前端 Monorepo 的
apps/server,保留全部 commit 历史(Windows / PowerShell)
背景目标
我有一个前端 Monorepo(根目录已经是一个 Git 仓库),同时我有一个后端项目(也是独立 Git 仓库)。
目标是把后端代码合并到 monorepo 的 apps/server 目录中,并且后端历史提交记录一条都不丢(每个 commit 都保留)。
我一开始是直接把后端所有文件搬到 apps\server,甚至把后端的 .git 也一起搬进去了。这样会形成“嵌套仓库(nested repo)”,外层 monorepo 并不会真的拥有后端历史,后续非常容易混乱。最终用 git subtree 正式导入历史,并清理嵌套仓库问题。
核心结论
- 不要长期让
apps\server里存在一个独立.git(嵌套仓库),这会让 monorepo 的 Git 视角变得诡异。 - 要在 monorepo 里保留后端完整历史,推荐用:
git subtree add。 - subtree 导入成功后,会多一个“导入 commit”(merge 节点),后端历史会作为它的父系之一存在;你用特定命令能看到完整后端提交链。
术语说明
- Monorepo:根目录有一个
.git,统一管理多个子项目。 - 嵌套仓库(nested repo):在 monorepo 的子目录里又有一个
.git。外层 Git 不会自然地“吃进”内层历史。 - git subtree:把另一个仓库的历史导入到当前仓库的某个子目录下(比如
apps/server),并保留完整 commit 历史。
操作环境
- Windows
- PowerShell
- Git 已安装
- monorepo 路径示例:
F:\react-demo\app - 后端仓库路径示例:
F:\doc-back\app(或任何你实际的后端仓库目录)
注意:下面命令用到的路径你按自己实际情况替换。
过程复盘
把后端仓库当成 remote 添加进 monorepo
在 monorepo 根目录执行:
cd F:\react-demo\app
git remote add backend_local .\apps\server
git fetch backend_local当时能 fetch 到 backend_local/main,说明 .\apps\server 里确实存在后端 .git,它还是个“独立仓库”。
✅ 这一步证明:后端仓库还在,历史没丢。
⚠️ 但这只是“能读取”,并不代表历史已经进入 monorepo。
第一次 subtree add 报错:prefix already exists
git subtree add --prefix=apps/server backend_local main报错:fatal: prefix 'apps/server' already exists.
原因:apps\server 目录已经存在(你之前手动搬过文件),subtree 默认要求目标目录不存在(为了避免冲突/覆盖)。
解决:把当前 apps\server 临时挪走(备份),让 apps\server 不存在,然后再导入。
New-Item -ItemType Directory -Force C:\temp | Out-Null
Move-Item -Force .\apps\server C:\temp\server_worktree_tmp第二次 subtree add 报错:working tree has modifications
报错:fatal: working tree has modifications. Cannot add.
原因:monorepo 工作区有未提交改动(包括未跟踪文件),subtree 为了安全拒绝执行。
解决:用 stash 暂存所有改动(包含未跟踪文件 -u),让工作区干净。
git stash push -u -m "before subtree add backend"第三次 subtree add 报错:remote 路径失效(找不到仓库)
报错类似:fatal: '.\apps\server' does not appear to be a git repository
原因:你把 apps\server 挪走后,backend_local remote 还指向旧路径 .\apps\server,当然 fetch 不到了。
解决:把 remote 指回真正还带 .git 的那个目录(比如你挪去的 C:\temp\server_worktree_tmp,或者后端原始目录 F:\doc-back\app)。
我们这次最终指向了 后端仓库真实位置(例:F:\doc-back\app)并 fetch 成功:
git remote set-url backend_local F:\doc-back\app
git fetch backend_local mainsubtree add 成功,生成导入提交
最终成功执行后会看到类似输出:Added dir 'apps/server'
导入后,git log -- apps\server 看到一个导入节点 commit,类似:
Add 'apps/server/' from commit '...'
git-subtree-dir: apps/server
git-subtree-mainline: ...
git-subtree-split: a4b89c8...这说明:
- 后端历史已经被导入进 monorepo 的对象库里
git-subtree-split这个 hash 是“后端历史链”在 monorepo 中的锚点,非常关键
最终标准操作流程
进入 monorepo 根目录并确认位置
git rev-parse --show-toplevel添加后端仓库为 remote 并 fetch
推荐直接指向“后端真实仓库目录”(不要指向一个你可能会移动的临时目录):
git remote add backend_local F:\doc-back\app
git fetch backend_local确认远端分支(main/master):
git branch -r --list "backend_local/*"清理工作区(stash)
git stash push -u -m "before subtree add backend"subtree 导入(保留完整历史)
假设后端分支是 main:
git subtree add --prefix=apps/server backend_local main查看导入提交
git log -1确认后端历史链存在(最关键)
从导入提交里找到:git-subtree-split: <hash>
然后:
git log --oneline <hash>你会看到一长串后端提交(你刚才已经看到了:feat/fix/docs/... 那堆 commit)。
恢复 stash
git stash pop如果 pop 冲突:
说明你 stash 里改动和导入后的文件有重叠。通常保留 subtree 导入版为主,再手动把 stash 的改动挑进去。
后续使用指南
以后怎么“看后端历史”?
最可靠:用 git-subtree-split 作为入口
从导入 commit 拿到 split hash(比如 a4b89c8...),然后:
git log --oneline a4b89c80e9cb6e04a28e836f337bc6e883d6a3cc这就是后端历史链。
给 split hash 打个 tag,方便永久引用(推荐)
git tag backend-import-base a4b89c80e9cb6e04a28e836f337bc6e883d6a3cc以后:
git log --oneline backend-import-base为什么 git log -- apps\server 只显示导入那一条?
因为 subtree 导入本质上是通过一个 merge 节点把另一条历史链“挂进来”。
而 git log -- <path> 做路径过滤时,并不会自动“沿着另一个父历史做目录重写追踪”,所以你只看到导入节点。
这不代表历史丢了,只是路径过滤视角比较严格。
如果你想追踪某个文件的完整历史(单文件可以用 --follow)
--follow 只对文件有效,不对目录有效:
git log --follow -- apps\server\package.json导入后 apps\server 里还该不该有 .git?
不该有。导入成功后,apps\server 应该是 monorepo 的普通目录。
导入后我还能从原后端仓库继续同步更新吗?
可以(这就是 subtree 的强项)。
以后后端仓库有新提交,你在 monorepo 里可以拉取:
git fetch backend_local
git subtree pull --prefix=apps/server backend_local main这会把后端增量合进来(仍保留历史)。
如果你不再需要同步,也可以把 remote 留着不用,或者移除:
git remote remove backend_local确认不再用独立后端仓库后的清理清单
确认 apps\server 里没有残留 .git
移除导入用 remote
你之前加了 backend_local,以后不用就删掉:
git remote -v
git remote remove backend_local增量合并
重新把后端仓库加回来
git remote add backend_local F:\doc-back\app
git fetch backend_local如果是远程 GitHub:把 F:\doc-back\app 换成仓库地址
标准拉取步骤(后端仓库有新提交)
假设后端分支是 main:
git fetch backend_local main
git subtree pull --prefix=apps/server backend_local main这一步会把后端新提交合并进来,并且只影响 apps/server 下的内容。
拉完后:
git status
git log -1没问题就提交/推送(subtree pull 通常会自动产生一个合并提交):
git push如果 monorepo 的后端已经改了,还能拉吗?
你改了 monorepo 的 apps/server(后端代码本身)
能拉,但是如果后端独立仓库也改了同一个文件/同几行代码,subtree pull 可能会冲突。
建议先确保工作区干净,如果有未提交改动,先 commit 或 stash(推荐 commit,免得之后混):
git status
git add -A
git commit -m "chore: local server changes before subtree pull"然后再 pull:
git fetch backend_local main
git subtree pull --prefix=apps/server backend_local main如果冲突了:
git status会提示哪些文件冲突。你手动改好冲突文件后:
git add -A
git commitsubtree pull 的冲突处理方式就跟普通 merge 一样:解决冲突 → add → commit。
你改的不是 monorepo 的后端(比如改前端、改别的包)
基本无痛(99% 情况),因为 subtree pull 主要合并 apps/server 的变更;你前端改动不在这个目录下,通常不会冲突。
步骤还是一样:
git fetch backend_local main
git subtree pull --prefix=apps/server backend_local main只拉取后端仓库的某个版本/某个 commit”
你可以先 fetch,然后指定一个 commit/tag:
git fetch backend_local
git subtree pull --prefix=apps/server backend_local <tag或commit>但更推荐基于分支(main/master)拉,心智负担小。