把子目录拆成独立项目的同时保留GIT历史提交信息
起因
在我的日常开发或课程项目中,我常常会把相关联的多个模块放在一个总仓库里来方便开发和推进。
例如在我的 Java 课程作业仓库 (java-courseworks) 里,除了有一些单独的课程作业目录外,
还有一个 practicum 目录,里面是一个完整的学生管理系统实训项目。
在完成后续的维护和展示过程中,我发现把实训项目和课程作业放在一起,虽然方便了初期的开发,但随着时间推移,出现了一些问题:
- 记录复杂:查看
practicum相关改动时会被其它目录提交干扰。 - 边界模糊:实训项目与课程作业是两类资产,其实并不适合放在一起。
- 发布困难:在其他大型项目中,一个模块想独立维护、打包、发布时,会受父仓库结构影响。
- 演示麻烦:给他人演示时,单独仓库更易理解与上手。
因此,本次我们将要一步步实现:
- 将
practicum拆到新目录并初始化独立 Git 仓库; - 复原指定区间的历史提交(包含提交信息与时间);
- 解除对父 POM 的依赖,使其可独立构建;
- 配置独立远程仓库地址并推送到 GitHub 。
开始操作
迁移前务必做好以下准备:
- 确认源仓库工作区干净(避免混入无关改动)。
- 确认目标目录不存在或可安全覆盖。
- 确认提交区间正确(尤其是起止 commit)。
- 准备本地工具:Git。
Step 1:从源仓库导出指定区间的补丁
我们需要先从源仓库导出仅 practicum 路径的补丁文件(保留提交顺序和元信息)。
1 | $patchDir = "D:\@DEV\java-courseworks\.tmp_patches" |
注意:这里的文件目录路径
D:\@DEV\java-courseworks、子目录名称practicum与 commit 区间均为示例,请根据实际情况替换为所需要数值。
其中,
format-patch会按提交生成补丁,保留作者、时间、提交说明;--binary确保图片等二进制文件也可迁移;-- <子目录名称>只提取目标子目录相关改动。
Step 2:创建独立仓库目录并初始化 Git
这里我选择在原仓库同级目录下创建 StudentUI 目录作为新仓库,并初始化 Git:
1 | $target = "D:\@DEV\StudentUI" |
Step 3:回放补丁并去掉子目录前缀
由于目标仓库希望将 practicum 内容作为根目录,需要把路径前缀剥离。这里使用 git am -p2:
1 | $patches = Get-ChildItem "D:\@DEV\java-courseworks\.tmp_patches\*.patch" | Sort-Object Name | Select-Object -ExpandProperty FullName |
其中,
-p2:将a/practicum/...或b/practicum/...裁剪成...;--committer-date-is-author-date:使提交时间与作者时间保持一致,最大化还原历史。
Step 4:迁移后处理
此时已经成功迁移了历史提交,剩下的就是针对实际情况的一些后续处理了。
在本项目中,StudentUI 的 pom.xml 需要从“子模块”改为“独立项目”,简单修改一下 pom.xml 即可。
Step 5:配置新仓库远程地址
1 | git -C "D:\@DEV\StudentUI" remote add origin "https://github.com/CarmJos/StudentUI" |
若已存在 origin,使用 remote set-url 即可。
1 | git -C "D:\@DEV\StudentUI" remote set-url origin "https://github.com/CarmJos/StudentUI" |
Step 6:验证结果
1 | git -C "D:\@DEV\StudentUI" --no-pager log --oneline --decorate --max-count=20 |
建议比对点:
- 提交顺序是否与源区间一致;
- 提交时间是否保留;
- 图片等资源是否完整;
Step 7:推送到远程仓库
1 | git -C "D:\@DEV\StudentUI" push origin HEAD:main |
Done! 现在 StudentUI 仓库已经成功迁移了 practicum 相关的历史提交,并且可以独立维护了。
欢迎参观 StudentUI 仓库,查看我本次迁移后的结果。
归纳总结
为了方便后续复用,这里总结一下核心步骤:
- 导出补丁:
git format-patch --binary --output-directory <补丁目录> <起始提交>^..<结束提交> -- <子目录> - 初始化新仓库:
git init - 回放补丁:
git am -p2 --committer-date-is-author-date <补丁目录>/*.patch - 配置远程:
git remote add origin <远程地址> - 验证:查看日志、状态,确认历史与资源完整。
快速脚本
如果你只要一个可快速复刻的版本,只需替换以下命令中的路径、提交区间和远程地址即可:
1 | $srcParent = "D:\@DEV\java-courseworks" |
完成后别忘了手动进行后处理,并做一次日志与构建验证。
同时,我还将相关操作整合成了一个可复用的 Python 脚本,方便在类似场景下快速执行,
详见 git-dir-exporter 项目 (欢迎Star~) 。
FAQ(常见问题)
如果有其他问题,欢迎在评论区留言,我会持续更新这个 FAQ。
Q1:为什么这里选择 git format-patch + git am,而不是直接复制目录?
A: 直接复制目录只能拿到“当前快照”,拿不到历史;format-patch + am 可以逐条回放提交,保留提交说明、作者与时间,更适合教学、审计和后续追踪。
Q2:为什么不是用 git subtree split?
A: subtree split 也能完成子目录拆分,且会生成仅包含该子目录的新历史分支。本文选择 format-patch + am 的原因是:
- 更直观展示“迁移区间”和“提交回放”的过程;
- 方便在迁移中插入额外处理(例如路径裁剪、单独补一条 POM 解耦提交);
- 对教学场景更友好,读者容易理解每一步在做什么。
如果你更偏向“一条命令出结果”,subtree split 也是可选方案。
Q3:git am -p2 里的 -p2 是怎么确定的?
A: 补丁路径通常是 a/practicum/... 与 b/practicum/...。需要去掉两层前缀(a/b + practicum),所以用 -p2
,最终让文件落在新仓库根目录。
Q4:回放补丁时提示冲突怎么办?
A: 常规处理流程:
- 查看冲突文件并手工解决;
git add <冲突文件>;git am --continue继续回放。
如果发现本次迁移策略不对,可中止后重来:
1 | git -C "D:\@DEV\StudentUI" am --abort |
Q5:如何确认提交时间是否真的保留了?
A: 可以分别查看源仓库与新仓库日志并对比时间戳:
1 | git -C "D:\@DEV\java-courseworks" --no-pager log --reverse --date=iso-strict --pretty=format:"%H|%ad|%cd|%s" 7b47ec1755525f05cd76683215dad5ed3585fe6d^..1effd2672b9a396db04668c0b1d9d89043f1aacd -- practicum |