Git教程 · 拆分大项目
- 1️⃣ 概述
- 2️⃣ 使用要求
- 3️⃣ 执行过程及其实现
- 3.1 拆分模块版本库
- 3.2 将拆分出的模块作为外部版本库集成
- 4️⃣ 替代解决方案
通常软件项目都是由单体小型系统开始的,在开发过程中项目规模和团队人员不断扩大, 将项目模块化会显得越发重要。第一步是将项目内部结构模块化,最终会需要将各个模块独立开发并拥有不同的提交发布周期。
由于 Git 版本库是以整个版本库作为一个整体来发布版本的,所以每个拥有独立发布周期的模块都需要新的Git 版本库。
为 Git 版本库拆分模块过程中的挑战之处在于要尽可能保留原版本库中文件及其版本信息。同时,新的版本库不应该包含本模块不需要的文件,也不需要包含那些没有更改本模块相关文件的提交。
在主版本库中,模块的历史没有被删除,所以原项目中的历史版本已然存在并可以复现。因此,不同模块的历史数据同时存在于拆分前后两个版本库中。
大部分拆分出的模块依然被主项目所需要,应以外部模块的角色集成到主项目中,这种集成关系,在Git 中被称为子模块 (submodule)。
这段工作流演示了如何在Git 中抽取模块,同时实现这样3种目标。
- 只有该模块所需要的文件被导入到新版本库。
- 模块文件历史将被保留在新版本库中。
- 模块可以作为外来模块再次被集成到主项目中。
1️⃣ 概述
接下来这段操作中,我们使用如图上部显示的项目结构为例。这段实例工作流是基于Java 目录结构的,整个项目有3个模块,每个模块中的文件分别置于源代码 (src) 和测试代码 (test) 两个子目录中。换句话说,也就是每个模块包括两个部分。接下来将模块3分化到独立的版本库中。
第一步,删除所有无用的文件,使用 filter branch
命令在原版本库的一个克隆分支上提交即可。接下来,更新新模块版本库的目录结构用以管理模块3。最后,将模块3从原项目中移除,再将新模块版本库作为子模块合入原项目的外部引用目录中,结果如图下部所示。
新的模块版本库中可以重建文件的修改历史,也就是跟踪记录谁在什么时间做了什么修改,但是不可以完整地重现历史版本。原因是一个模块的文件往往源自另外一些模块。在模块版本库中尝试恢复项目的某一历史版本可能不仅会涉及本模块目录,而是不同目录文件的混杂集合。而且,在过去的版本中本模块可能被用作某些文件的依赖,而这些文件已经不存在了。
在主版本库中,整个项目的旧版本依然可以恢复重现。
2️⃣ 使用要求
- 项目内部需要模块化时:项目内部需要被分为不同的模块,比如当某一模块需要独立开发和发布版本。
- 模块文件被分置于不同的目录中时:这时要提取模块的某一历史版本,文件在不同个目录中将需要不同的处理,如果文件十分分散代价将非常大。
3️⃣ 执行过程及其实现
一个模块从项目中被删除并迁移到独立的版本库中,提交历史将被保留下来,无用的文件和提交历史将被删除。独立模块将以外部子模块的形式回到项目中。
需要注意,部分以下命令将彻底改变版本库。虽然 Git 中改变通常可以撤回,但仍应在开始之前确保你的版本库已备份。
> git clone --no-hardlinks --bare projekt.git projekt.backup.git
使用 --no-hardlinks
选项来保证克隆的版本库和源版本库不共享任何文件。
3.1 拆分模块版本库
-
第1步:克隆主版本库
作为模块版本库的起点,首先将主版本库克隆一份。> git clone --no-hardlinks --bare projekt.git modul3-work.git
-
第2步:删除无用的文件和提交
接下来,必须删除无用的文件和提交,这是最复杂的一步,也是为了保留模块历史至关重要的一步。
删除一个版本库中的部分内容可以用filter-branch
命令。它将针对待修改的提交来创建一次新的提交,通过配置不同的过滤器来改变这次提交的内容。
以下示例filter-branch
命令将删除src/module1
目录下内容。> cd module3 > git filter-branch --force --index-filter 'git rm -r -cached --ignore-unmatch src/module1'--tag-name-filter cat--prune-empty -- --all
参数可以这样配置。
--index-filter 'git rm -r -cached --ignore-unmatch …'
: 通过配置这样的参数,可以将文件从提交中移除。rm
命令逐个提交操作。在如上示例中,将作用于src/module1
文件目录。 如果待清理的项目没有明显的模块化结构层次,可能需要删除多个文件或多个文件目录。--tag-name-filter cat
: 可以为已经存在的或者新建的提交标注标签。--prune-empty
: 将删除经过前面的过滤器后不包含任何文件的空提交。--all
: 将过滤器适用于整个项目的所有分支。
在示例的项目中,如此的操作需要依次在 test/module1 、src/module2 和 test/module2 文件目录下执行。
关于filter branch
命令每项参数的详细描述,可以参照 Git 帮助。
-
第3步:删除无用的分支和标签
不是所有标签和分支在新的模块分支都有意义。例如,那些与模块不相关的文件标签和分支就是无意义的,需要被删除。> git tag -d v1.0.1 > git branch -D v2.0_bf
-
第4步:缩减模块版本库的规模
Git 为了缩减规模在管理数据中删除无用的文件需要重复克隆一次。> git clone --no-hardlinks --bare module3-work.git module3.git
这样,过去的模块版本库 module3-work.git 就不再有效,可以删除了。
> rm -rf modul3-work.git
-
第5步,定制模块版本库文件架构
到目前为止,新版本库的文件结构和主项目一样,只是删除了无关本模块的文件。调整文件目录结构是通过一般的文件操作完成的,为了这个目的,首先应做一份带有工作空间的克隆。> git clone module3.git module3
将源代码目录
src/module3
重命名为src
, 测试代码目录test/module3
重命名为test
。> cd module3 > mv src/module3 module3 > rmdir src > mv module3 src > mv test/module3 module3 > rmdir test > mv module3 test
接下来,修改操作通常是借助于
commit
命令,再通过push
上传到干净的版本库中。> git add --all > git commit -m "Directory structure adapted" > git push ,
如果版本库中有多个分支,那文件操作要在各个分支上依次完成。 .
通常没有必要保留主项目所有的分支。新的版本库有新的分周期,旧分支通常没有意义。 -
第6步:在主项目中删除已被拆分出来的模块目录
当拆分出的模块已迁移到新的版本库中,下一步就是让主项目来做拆分后的调整。删除 无用的源代码目src/module3
和测试目录test/module3
。这里的调整主要是在主版本库中的一些普通的文件操作。如果项目中有多个分支需要集成这一个改变,那也需要分别进行调整。
cherry-pick
命令可以用作将变化调整部署到不同的分支。
3.2 将拆分出的模块作为外部版本库集成
经过前面一系列操作,现在已经拥有两个版本库了,通常原主项目仍需引用拆分后的模块,所以需要集成操作。
集成操作严格依赖于开发平台。例如在 Java Maven项目中,可以将拆分出的模块项目独 立创建编译,并将结果保存在 Maven 项目中,在主项目中将其定义为依赖条件,在创建编译主项目的过程中充 Maven 中获得模块项目。
如果使用 Git 来执行集成操作,那就需要用到子模块 (submodule) 。 有了子模块, 一个Git版本库就可以链接到另外一个 Git版本库了。
在上述示例中,模块3 (module3) 的版本库被链接到了主项目的extern/module3
文件目录下。首先从主项目版本库的一个克隆版开始操作。选定项目的根目录,使用 submodule add
命令加入子模块,该命令有两个参数,第一个是模块版本库的路径或 URL, 第二个参数是在主项目中即将链接的路径。
>git submodule add /global-path-to/module3.git extern/module3
submodule add
命令会在特定的目录下创建一个模块版本库的克隆,这个克隆会在主版本库中作为外部引用。
文件目录 extern/module3
指向外部版本库的最新一次提交 (HEAD) 。
截至目前,子模块只能在工作区中可见,需要只用 commit
命令提交使修改作用于整个版本库。
>git add -all
>git commit -m "Modul3 added"
可以使用 push
命令将添加子模块链接捷径的修改推送到中央版本库。
4️⃣ 替代解决方案
-
何不采用一个全新的版本库
有另外一种可以考虑的替换方案是简单地为模块创建一个新版本库。那么,新版本库中就没有原模块的历史记录了,但是原始版本库中仍然有模块的旧版本信息。如果这种缺陷可以被接受,那这种方案在实现上显得最为简单。 -
为什么不采用
--subdirectory-filter
选项
使用filter-branch
命令和-index-filter
参数可以实现将提交中的文件删除。
filter-branch
命令的--subdirectory-filter
参数可以指定将排除某一文件夹之外的文件全部删除,还可以在删除指定的文件夹后改变项目的根目录节点。只要模块全部独立存储在一个文件目录下,那么这个命令就可以较方便地用来创建模块版本库。在上文示例中,模块文件被分散在两个目录下,因此不能适用这样的操作。
即使模块中的文件所属文件目录被迁移或者被改名,
subdirectory-filter
仍然可以保留部分历史信息。
《【Git教程】(十七)发行版交付 — 概述及使用要求,执行过程及其实现,替代解决方案 ~》