【Git教程】(十四)基于特性分支的开发 — 概述及使用要求,执行过程及其实现,替代方案 ~

在这里插入图片描述

如果团队中的每个人都在相同的分支上做开发,我们会得到一个非常混乱的、带有很多 合并提交的父级有限历史。这样一来,我们就很难就某个指定的特性修改或错误修复进行具体定位。但尤其在代码审核和错误排查这样的工作中,确切知道某个特性在哪几行代码中是非常有用的。通过使用特性分支,我们可以利用Git 提供这些开发信息。

在做特性开发的过程中,增量提交的方式有助于我们在同一时间段内维护旧有功能的版本。但是,如果我们是希望对某一发行版中所包含的新特性,显然还是粗粒度的提交更具有实际意义。在本章将要介绍的工作流中,特性分支上的增量提交与 master 分支上的发行版提交将会被同步创建。并且,粗粒度的master 分支历史还很有可能被用来当作发行版文档的基础,测试人员会非常欢迎这种能清楚引用相关特性的粗粒度提交。本章的工作流将会详细介绍特性分支的使用,以便我们:

  • 能轻松定位实现了某一特性的提交;
  • 且让 master分支上的第一父级历史中只保留能充当发行版文档的粗粒度提交;
  • 并行特性的交付也成为了可能;
  • 以及 master上所发生的重大变更也能在特性开发期间比使用。

1️⃣ 概述

在下图中,我们会看到本章工作流的基本结构,那就是一个团队在特性分支上开发时会创建的工作流。他们会从master分支着手,为每个特性或错误修复都创建一个新的分支(bug 在下面没有被明确地列出来)。该分支会被用于执行所有的修改和改进。 一旦该特性已经可以被集成到 master 分支上时,我们只需要对其执行一次合并即可。当然这里务必要小心一件事,

该合并操作必须要在 master 分支上发起,并且不能使用快进式合并。这样我们就能在 master分支上得到一个清晰的第一父级历史,因为该分支上将只包含特性分支的合并提交。

在这里插入图片描述
如果一些特性之间存在着依赖关系,或者某个特性采用的是增量式的开发,那么它们就应以并行式交付的方式被集成到 master 分支上,并接着在特性分支上做进一步开发。


2️⃣ 使用要求

  • 基于特性的开发方略:我们的项目计划或者产品计划必须要立足于特性开发。也就是说,项目中的各种功能性需求要能被转化成相应的特性工作包。当然,这些特性之间可以有非常少量的重叠。
  • 特性的小型化:即一个特性必须要能在几小时或者几天内开发完成。费时较长的特性开发会与其他部分的开发并行运作。在这种情况下,特性集成的成本越高,其带来的 风险就越大。
  • 在本地执行回归测试:在将新特性提交给 master分支之前,我们应该先在自己的开发机器上进行本地的回归测试。检查一下该特性所带来的变更是否与其他特性兼容,以 及是否有我们不希望看到的副作用。如果我们没有在本地进行这样的回归测试,这些 错误就往往只能在master 分支的合并提交中被发现。而对这些错误的校正操作将会使 master 分支的历史记录不再基于特征分支,从而抵消了使用特性分支的主要优势。

3️⃣ 执行过程及其实现

先用独立的专用分支来开发各个特性或进行错误修复,然后在特性开发或错误修复完成之后将其合并到 master 分支上去。

以下操作将会假设我们始终会有一个中央版本库。就像往常一样,开发工作会在该版本库的本地克隆体中完成,然后中央版本库则会以该克隆体的远程源版本库的角色被访问。
因而在接下来的流程中,我们将会看到 push 命令被频繁地用来将本地修改传送给中央版本库。

一旦在工作中使用了特性分支,本地版本库中往往就会存在多个分支。在没有参数的情 况下, push 命令只会将当前活动分支中的内容传送给到远程版本库。当然我们也可以通过设置 push.default 选项来更改这一行为。

> git config push.default upstream

该选项的默认值匹配所有与远程版本库中有相同分支的本地分支。因此,我们每次在执行 push 命令时都必须明确指定分支,以限定传送的内容。

3.1 创建特性分支

只要某个特性是可以被处理的,我们就可以为其创建一个新的分支。这里最重要的是要确保这些分支必须始终是基于 master 分支来创建的。

  • 第1步: 更新 master 分支
    如果可以访问到中央版本库,先将本地的 master 分支更新到最新状态是很有必要的。这样做有助于我们确保当前没有合并冲突,我们建议在做这件事之前,不要在本地版本库的master 分支上开展任何基于特性的任务。

    > git checkout master
    > git pull --ff-only
    

    这里的 --ff-only 参数表示这里只允许进行快进式合并。换句话说,如果这时存在着一些本地修改,该合并操作就会被取消。

    如果合并操作报错并被终止,那么我们就必须直接在master 分支上做修复错误的工作。该分支上的这些修改就应该先被转移到某个特性分支上。

  • 第2步:创建特性分支
    我们可以用以下命令创建新的分支并开始相关的工作。

    > git checkout -b feature-a
    

    如果整个团队来能对特性分支或bug 修复分支进行统一命名,当然是非常有益的。另外,Git 也支持对分支进行分层命名,例如 feature/a。
    通常情况下,人们会采用一种追踪工具(例如 Bugzilla 或 Mantis) 来对特性开发和 bug 修复的工作进行管理。这些工具会赋予这些相关特性和 bug 一个唯一的编号或令牌。而这些编号也可被用作 Git 的分支名称。

  • 第3步(可选): 在中央版本库上维护特性分支
    通常,特性分支都只创建在本地,当这些分支的生命周期很短时尤为如此。
    但如果某个特性的实现需要较长的时间,或者有多个开发者在开发同一个特性,其中间成功就会显得相当重要。于是,我们会希望将该分支放到中央版本库上去维护。
    为了做到这一点,我们可以用push 命令在中央版本库中创建一个对应的分支。

    > git push --set-upstream origin feature-a
    

    这里的--set-upstream 参数会将本地的特性分支与远程版本库中的新分支连接起来。这也就是说,我们将来执行的所有 pushpull 命令都可以免去显式指定远程分支的操作了。
    origin 参数则指定了远程版本库的名称(即中央版本库的别名), 以及所维护的特性分支的名称。
    这样一来,我们今后所修改的本地特性就可以通过一个简单的 push 命令被同步维护到中央版本库中了。

     > git push
    

3.2 在 master 分支上集成某一特性

根据我们已经定义好的需求,确保相关特性不长期以并行形式存在是很重要的。否则, 出现合并冲突和不兼容的风险就会大大增加。即使该特性不会被纳入到下一发行版中,我们也会建议你尽快完成功能的集成,并禁止切换到该特性分支。

在这一节中,我们将详细介绍如何将一个特性集成到 master 分支。其中的重点在于,相关的合并必须始终在 master 分支上执行。否则 master 分支就无法获得一个第一父级的提交历史,这就毫无意义了。

  • 第1步:更新 master 分支
    在实际执行 merge 命令之前,本地的master 分支必须保持最新状态,如果我们在本地分支上做这些事,就很有可能会出现合并冲突。

    > git checkout master
    > git pull --ff-only
    
  • 第2步:合并特性分支
    我们在特性分支中所做的修改可以通过 merge 命令传送给 master 分支。这样一来,master 分支上第一父级的历史就可以被当作一个特性发展的历史。当然,这里是不允许快进式合并的。

    通过下图来看一下快进式合并可能会带来的问题。如你所见,在合并之前, master 分支所指向的位置是提交B 而特性分支则指向了提交E 。在经过快进式合并之后, master 分支现在也位于提交E 之上。这样一来,master 分支上的第一父级历史中就纳入了两个中间提交的 D 和 C。

    在这里插入图片描述

    你可以用以下命令来执行合并操作,并同时防止其执行快进式合并:

    > git merge feature-a --no-ff --no-commit
    

    在这里, --no-ff 参数的作用就是防止快进式合并。而--no-commit 参数则用于指示 Git, 不要因为接下来可能失败的测试而停止任何提交。

    现在,我们再来看看下图,该图所示范的就是快进式合并被阻止之后的提交历史。我们会看到一个新的合并提交F 。这样提交C~E 就不会被纳入到master分支的第一父级历史中了。

    在这里插入图片描述

    如果我们在本地特性分支上做这些修改的时候,其他特性分支也修改了相同的文件,该合并就可能会引发冲突。而这些冲突则必须要用普通的方式来解决。

  • 第3步:做一下回归测试,并为其创建一次提交
    在执行完合并操作之后,我们通常就应该运行一下回归测试。在这种情况下,如果新特性会引发其他特性的错误,就会被该测试检查出来。
    如果该测试确实引发了这样的错误,我们就必须要这些错误进行分析。为了排除它们,我们就必须要先用 reset 命令丢弃当前所执行的这次合并。

    > git reset --hard HEAD
    

    在这里, --hard 参数的作用是确保暂存区和工作区中所发生的全部修改都会被丢弃。而 之后的 HEAD 则表示当前分支必须是最后一个已关闭的提交。
    紧接着,特征分支会被再次激活。上述错误可以在这里得到纠正,之后只需要从第1步开始再走一遍至今为止的流程即可。
    如果回归测试没有找到错误,那我们就可以直接提交完事。

    > git commit -m "Delivering feature-a"
    

    如果我们想以文档形式来使用 master 分支上的历史记录,就必须要对合并提交的注释内容进行统一设置。具体来说,就是我们必须要对特性设置一个具有关联性的唯一标识,例如 使用编号。这样一来,我们就可以用log 命令加 -grep 参数搜索到 master 分支上的相关特性。

  • 第4步:将 master 分支传递给中央版本库
    在完成上述最后一步操作之后,本地版本库中的 master 分支应该已经与特性分支完成了统一。下一步,我们必须要通过push 命令将 master 分支传递给中央版本库。

     > git push
    

    如果这个命令在执行过程中出现了错误,那么 master 分支上一定有其他特性分支已经被集成了进来,并且不再可能是一次快进式合并了。这时候我们通常的做法是先执行一下 pull命令,将其修改合并到本地来。但这样做的话,我们的第一父级历史就不能再发挥作用了。
    在下图中,顶部和中间这两部分所描述的概况如下:提交C 为远程版本库的当前提交, 而本地版本库当前所应用的是提交 D。如果我们现在执行pull 命令,就会创建出一个新的合 并提交E (见图中的底部)。因此可以看出,提交C 将不会再被纳入到 master 分支的第一个父级历史中了。

    在这里插入图片描述
    但第一父级历史中应该要纳入所有的特性,所以在 push 命令失败之后,本地特性分支上的合并提交必须要用 reset 命令移除掉。

    > git reset --hard ORIG_HEAD
    

    在这里, ORIG_HEAD 所引用的就是当前分支在合并之前所指向的提交。
    然后,我们只需要从第一步开始再执行一遍之前的操作序列即可,也就是用 pull 命令重新将 master 分支中的新提交检索出来。
    当然,如果 push 命令执行成功了,那么新的特性自然就已经被纳入到了中央版本库中。

  • 第5步:删除或继续使用该特性分支(二选一)
    (变化1): 删除该特性分支
    如果在与master 分支合并之后,该特性开发就完成了,那么我们就可以删掉这个特性分支了。

    > git branch  -d  feature-a
    

    这里的-d 选项就是要删除指定的分支。
    如果删除过程中出现了错误信息,那么很可能是我们忘记了将特性分支合并到master 上。 因为-d 选项只有在一个分支上的所有提交都被其他分支引用了的情况下才会删除该分支。如果你是想删除一个没有被 master 分支全面接管其所有提交的特性分支,可以选择改用-D 选项。另外,如果被删除的分支在中央版本库中有对应的维护分支,则那边的分支也会随即被删除。

    > git push origin :feature-a
    

    在这里,分支名称之前的冒号是很重要的。该命令的意图是:不粘贴特性分支上的所有东西。
    (变化2): 继续特性开发
    如果该特性开发还尚未完成(也就是说,它在首轮集成中只有部分交付给了master 分支), 那么该特性分支就还可以继续被使用。
    在下图中,进一步的工作大致上是对部分交付之后那部分的一个概述。在这个过程中,该分支将继续被当作特性分支来使用。

    > git checkout feature-a
    

    在这里插入图片描述
    一旦下一批内容准备就绪了,我们就可以再次将其集成到 master 分支上,这时候 Git 也会显得足够智能化,它只会将新提交中的修改传递给 master 分支。

3.3 将 master 分支上所发生的修改传递给特性分支

在最好的情况下, 一个特性的开发是应该独立于其他特性来进行的。但有时候 master 分支上也会出现一些我们在特性开发过程中所必须跟进的重要变更,例如对于项目基本服务的 大规模重构或修改。在这种情况下,我们就需要及时将 master 分支上所发生的修改传送给特性分支。
下图对这种情况做了直观的说明。对于来自master 分支的合并,我们需要在特性分支上来执行。

在这里插入图片描述

  • 第1步:更新 master 分支
    首先,我们必须要先将 master 分支上所发生的修改导入到本地版本库。

    > git checkout master
    > git pull --ff-only
    

    这里的--ff-only 选项表示该操作只允许执行快进式合并。这样做可以防止我们在 master分支上再执行一次意外的合并。

  • 第2步:将修改引入到特性分支中
    在第2步中,这些修改就必须要通过一个合并操作引入到特性分支中了。

    > git checkout feature-a
    > git merge --no-ff master
    

    在这里,我们得用 --no-ff 选项来禁止快进式合并。快进式合并只有在 master 分支与该特性分支在之前已经合并过的情况下才能发挥作用。之后,快进式合并还会破坏特性分支上的第一父级历史。

    任何可能出现的冲突都必须用普通的方式来解决。
    该特性分支可以从 master 分支上获取任何中间版本。Git 可以很好地处理多次合并的情况,但这会使其提交历史变得非常复杂且难以阅读。

Git-Flow: 即高级 OperationsGit Flow, 我们可以在 https://githum.com/nvie/gitflow 上下载到该工具,这是一个用于简化分支处理的脚本集合,对于特性分支的处理尤为得力。通过这个Git 扩展,我们就可以像这样创建并激活一个新的特性分支:

>git flow feature start feature-a

到最后,我们可以这样将特性分支上的内容传送给 master 分支,并删除该分支:

git flow feature finish feature-a

4️⃣ 替代方案

4.1 直接在部分交付后的合并版本上继续后续工作

如果某一特性被部分交付到了 master 分支上,那么 master 分支上就会有一个合并提交, 其中也将包含其自有特性的修改和来自其他特性分支的更新。另一方面,来自其他特性分支的更新在我们的特性分支上还尚不可用(见下图)。

在这里插入图片描述

那岂不是说明我们应该执行将 master 分支合并到特性分支上的操作,然后在合并后的分支上继续工作就行了吗?
这个问题的简短回答就是:我们没有必要把历史记录搞得过于复杂,而且在大多数情况下,这样的过程并不会给特性开发带来任何好处。

在特性分支上,在执行完部分交付之后再对 master 分支进行合并的时候,往往会执行的是一次快进式合并。也就是说,这两个分支将会指向同一个提交对象。但这会破坏特性分支 的第一父级历史,这样的修改会导致特性开发的过程变得不再可被追溯。因此,我们必须要 防止其执行快进式合并,当然,这会令其产生一次内容为“空”的新合并提交。这意味着,某个特性的每次部分交付都会产生两次合并提交。

但在大多数情况下, master 分支上的版本更新对于某个特性的开发并没有那么重要。如果我们在特性开发过程真的需要引入相关修改,我们当然要这样做。但肯定不是每次执行完部分交付之后总得这样做。

4.2 到发行版即将成型时再集成特性分支

在我们在特性分支上开发的过程中,项目的发行管理通常知道交付日临近之前才会有相关的想法和决定,以明确新版应该会发布哪些特性。

从概念上讲,这似乎也是一种更简单的使用特性分支的方法。这时候,每个特性都在各自的分支上得到完全地开发,但也都还尚未被集成到 master 分支。究竟有哪些特性将会被集成到 master 分支只有在交付日 (D-day) 之前才会决定。

在理想世界里,这些特性都会各自独立存在,并且不会出现任何编程错误。但在现实世界中,这种方法所带来的往往要么是集成过程中的主合并冲突,要么就是一个长期稳定的状态。
此外,这样做还会更复杂化各特性开发中的依赖关系。通常情况下,我们只要对某个特性做一次部分交付给 master 分支即可。而在延后集成特性的解决方案中,分支之间就必须要 彼此交换各自的修改了(详细内容请参见下一节内容)。这将使得我们要选择在版本发行之前才来集成这些特性变得根本不可行。

而且,对于软件的品质验证过程来说,例如在用生成服务器来进行持续集成时,延后集成这种方法就会让我们的集成和重构难以实现。

4.3 交换特性分支之间的提交

本章所描述的工作流是一个特性分支之间没有直接提交交换的过程,其集成总是基于 master 分支上的各部分交付内容来进行的。
那么, 直接在相关的特性分支之间来一次合并,事情岂不是更简单一点吗?

ReReRe——解决冲突的自动化预案
我们在文件中手动解决冲突的过程可以被当作预案记录下来。如果同样的冲突一再发生,该预案就可以自动被应用。这就是所谓的“重用预案记录。”简而言之就是使用ReReRe。

  1. 启用 ReReRe
    这个用于记录冲突预案的特性必须要针对各个版本库单独开启。
    > git config rerere.enabled 1
    由于 ReReRe 是将冲突预案存储在本地的,所以该工具在该版本库的每个克隆体中也必须要被单独开启。
  2. 记录冲突预案
    一旦ReReRe 被启用,我们所有解决冲突的方案都会在执行 commit 命令时被自动保存 下来。但如果提交没有被执行(例如,试图执行 reset 命令时可能被拒绝了), 这时候我们就必须显式调用rerere 命令了:
 > git rerere
	Recorded resolution for 'foo.txt'.
  1. 应用冲突预案
    一旦 ReReRe 被启用, Git 就会去试图自动地反复解决每一个合并中的冲突。例如,在 下面的情况中,尽管该文件在解决冲突的过程中被修改过了,但我们还没有用 add 命令确认该冲突得到了解决。
> git merge featureE
Auto-merging foo.txt
CONFLICT(content):Merge conflict in foo.txt
Resolved 'foo.txt'using previous resolution.
Automatic merge failed;fix conflicts  and then commit the result.

在我们解决了相关冲突之后,就必须要通过add 命令将受影响的文件添加到下一次提交中。

> git add foo.txt

在这里,我们需要显示一下当前还没有被合并到master 分支的特性分支。如果我们在工作中一直会用到特性分支,那么往往版本库中都会存在一个以上的活跃分支。我们可以通过branch 命令来显示出当前所有还尚未被合并的分支。

特性分支的决定性优势主要在于它能带来简单易懂的历史记录。如果我们在特性分支之间直接加入合并操作,这样的优势就不复存在了。

显示某个被合并特性中的所有修改
特性分支所发生的所有修改应该都可以通过diff 命令显示出来。
能找出发生了哪些修改是很重要的,这一点在做代码审查的时候尤为如此。我们可以 通过下图来看一个具体的例子,该图显示了一个特性分支从部分交付到最终交付的过程,接下来,我们就来看看该图中所引用到的这些提交。

在这里插入图片描述

  1. 定位某特性分支中的提交
    对于diff 命令来说,我们需要的是 master 分支上属于该特性分支的所有提交。通常情况下我们只会看到一个合并提交而已,但在部分交付之后情况就会有所不同,我们接下来会看到多个提交。例如在这里,我们就会找到提交H 和 G。
git checkout master
git log --first-parent --oneline --grep="featureC"
c52ce0a Delivery featureC
c3a00bc PartialDelivery featureC

在这里, --grep 选项会负责在日志中搜索某段给定的文本。

  1. 执行 diff 命令
    相关的修改可以通过在找到的提交与其父提交之间执行 diff命令来查看。例如在这里,我们将查看的是提交B 与提交G 之间,以及提交G 与提交H 之间所发生的修改。
> git diff c3a00bc^1 c3a00bc
> git diff c52ce0a^1 c52ceOa

找出某特性中的所有提交
在这里,我们将找出特性开发期间所创建的所有提交。
在执行代码审查的过程中,我们会希望更彻底地理解特性分支在构建过程中所发生的具体修改。在此,能单独查看相关特性分支中的所有提交是很有帮助的。下面,让我们回到上图这个从局部交付到最终交付的特性分支上来。再看看该图所引用到的这些提交。

  1. 找出相关特性的合并提交
    首先,我们需要找出master 分支上与该特性相关的所有提交。通常情况下,我们会看 到其中只有一个合并提交,但如果其中有过部分交付的话,情况就不一样了,我们会看到若干个提交。具体到我们的例子中,就是提交H 和G 将会被找出来。
> git checkout master
> git log --first-parent --oneline -grep="featureC"
c52ce0a Delivery featureC
c3a00bc PartialDelivery featureC

在这里, --grep 选项会负责在日志中搜索某段给定的文本。

  1. 找出相关特性分支的起点
    如果我们要显示属于某特性的所有提交,就必须要先在master 分支中找到分叉出该特 性分支的那个分叉点上的提交。这个起点就是我们最底层的提交,该提交我们在之前的步骤中已经找到(即提交G)。它一定是两个父级提交的合并提交。我们可以通过merge-base 命令找到该提交。它由第一父级提交(提交 B) 和第二父级提交(提交 D) 合并而成(提 交B), 成为后续开发的共同起点。
> git merge-base c3a00bc^2 c3a00bc^1
ca52c7f9bfd010abd739ca99e4201f56be1cfb42
  1. 显示属于该特性的提交
    只要我们找到了该特性分支的起点,就可以用log 命令显示出其中所有的提交了。当然要想做到这一点,我们所要求的就必须是从合并基点(提交 B) 开始,到该特性分支中的最新提交为止的所有提交。其最新提交就是该分支最终交付时所做提交(提交 H) 的第二父级提交(提交F)。
> git log --oneline ca52c7f..c52ce0a^2


温习回顾上一篇(点击跳转)
《【Git教程】(十三)相同分支上的开发 — 概述及使用要求,执行过程及其实现 ~》

继续阅读下一篇(点击跳转)
《》

  • 28
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小山code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值