Git教程 · 基于构建服务器的工作
许多项目都会用到像 Hudson 、Jenkins 、Cruise Control 这样的构建服务器,以便于经常性地对单元测试和集成测试的部分进行自动化构建。Git 自然可以被用来充当这些待构建软件的来源处。有意思的是,但软件成功被构建是指相关信息也可以也再流回到 Git 版本库中。如果版本库能够了解项目中最后被成功构建并通过测试的是哪一个版本,我们开发者就可以用一个简单的 diff
命令来查看本地的开发版与这个版本之间究竟有哪些不同。而且在更理想情况下,我们还可以利用这个特性构建一个分支,以记录这些本版成功通过测试的历史。这在我们日后想修复某个没有被测试捕捉到的棘手错误时是很有帮助的。
在本章的工作流中,我们将演示如何用Git 完成以下任务。
- 当前版本的构建与定期测试。
- 开发者可以随时拿最后成功测试的版本与他们工作区的内容进行比对。
- 构建一份记录成功测试的版本历史,以供日后排错之用。
1️⃣ 概述
对于本章的工作流来说,其大脑部分就是构建服务器(见下图)。在这些需定期构建的版本库中,构建服务器通常会交由中央版本库的 master 分支中的最新提交来负责配置。而在构建服务器中,这些构建和测试代码只是它的输入源。
在构建服务器中,我们通常会设置两个分支。其中,last-build 分支会直接指向 master 分支中最后构建成功的那个版本。而 buildhistory 分支中则将会包含该软件所有被成功构建的版本。
last-build 分支中的内容会在每次中央版本库成功完成构建时被传送给构建服务器。我们可以通过在last-build分支中调用diff
命令,随时查看哪些该版本所做的那些修改没有成功通过测试。
在查找错误相对困难的情况下,开发者也可以从构建服务器中将 build-history 分支下载到自己的本地版本库中,以边该错误第一次出现是在哪一个成功构建的版本之后。
2️⃣ 使用要求
- 中央版本库:该项目必须要有一个可以定义软件当前状态的中央版本库。
- 持续性集成:下一个发行版的开发要能被定去集成到某个公用分支上(即 master 分支)。这种集成将不止发生在开发终止时,而是在完成一个修改之后就立即将其生效。
- 构建服务器:该项目必须要有一个可用于自动化构建与测试的构建服务器。
- 测试套件:必相关的单元测试和集成测试套件必须要有,它们应该可以用一段脚本来启动。
3️⃣ 执行过程及其实现
构建服务器将负责定期对软件的当前版本进行构建和测试。通过这样做,我们可以得到 该软件成功构建的版本历史。除此之外,我们还会早在中央版本库中标记出最后被成功构建的版本。
3.1 预备构建服务器
一个独立的版本库必须要相应地设置一个构建服务器。想要创建一个构建服务器,最简单的方法就使用众所周知的 clone
命令。但如果我们使用了 clone
命令,包含所有分支的整个中央版本库就会一并被克隆过来。而事实上对于构建服务器而言,我们需要的只是 master 分支,然后在上面执行 init
和 fetch
这两个命令即可。
第1步:创建一个空的版本库
首先,我们要新建一个空的版本库。> mkdir buildrepo > cd buildrepo > git init
第2步:获取中央版本库知道 master 分支
我们可以用remote add
命令将构建版本库与中央版本库连接起来,并且为了防止fetch
和pull
命令去获取其所有分支,我们必须要用-t
参数来明确指定一个分支。> git remote add -t master origin <central repo>
在这条命令中,我们用到了以下参数。
-t master
: 表示只有 master 分支将会在将来执行fetch
和pull
命令时被自动传输。origin
: 表示新增远程版本库的名称。我们选择了目标相同的远程名称,以便clone
命令也能使用这个名称。<centrl repo>
: 表示链接到中央版本库的URL。
目前,中央版本库还没有传送任何提交给构建版本库。所以,我们将会先做一次fetch
操作来传输相关的数据。
> git fetch
第3步:创建一个 build-history 分支
接下来是最后一步,我们需要新建一个名称为 build-history 的分支,该分支将记录成功被构建的版本历史。对此,我们会建议你将该分支的起点建立在origin/master 分支的第一次提交上。
如果我们将 build-history 分支构建在了origin/master 分支的当前提交上, origin/master 分支上现有的提交都将会被纳入到构建历史中。
不幸的是,并没有一个简单直接的Git 命令可以用来查找某个分支的第一次提交。对此,我们能想到的最好方法就输出日志,然后查看它的最后一项。> git log --oneline --first-parent origin/master| tail -1 3a05e26 init
在这条命令中,我们用到了以下参数。
--oneline
: 表示只打印一行提交日志。--first-parent
: 表示只打印目标提交的第一父级提交。这可以大大降低该命令要处理 的信息,使其能更快地返回结果。origin/master
: 表示第一次提交应该来自于中央版本库的 master 分支。| tail -1
: 表示只打印日志输出的最后一行。
在找到第一次提交之后,我们就可以构建 build-history 分支并激活它了。另外,我们也 可以用checkout
命令来指定该分支的起点提交。
> git checkout -b build-history 3a05e26
在这里,
-b
选项表示新建一个分支,并在工作区中激活它。
3.2 构建服务器上的 Git
在接下来的步骤中,我们将介绍 Git 构建服务器上是如何工作的。通常情况下,它们会通过实现一段脚本被集成到各自的构建服务器的架构中。
构建服务器通常都是在 build-history 分支开展它的工作的。
第1步:获取中央版本库中的修改
我们可以从中央版本库的 master 分支中获取最近的提交。> git fetch
第2步:检查是否存在构建请求
我们需要通过对当前的 build-history 分支与origin/master 分支执行一次diff
命令,根据比较结果来检查版本库中是否存在新的提交。如果没有检测到差异,构建操作就不必启动,我们的操作过程也可以终止了。> git diff --shortstat --exit-code origin/master 1 files changed,68 insertions(+),144 deletions(-)
在这条命令中,我们用到了以下参数。
--shortstat
: 使用了该选项,命令就不会详细显示所有修改的细节,而只显示被修改文件的简要统计数据了。--exit-code
: 此选项可以确保该命令在发现差异时返回1(否则返回0)。通过这种方式,我们可以轻松地对结果进行评估origin/master
: 表示我们差异检查针对的是中央版本库的 master 分支。
第3步:清理工作区
如果上述构建失败了,就意味着我们本地工作区中包含的是一个针对损坏构建体的合并结果。为安全起见,我们必须要重置工作区。> git reset --hard HEAD
我们也可以用
clean
命令删除所有未被版本化的文件。> git clean --force
--force
选项表示强制执行 clean 命令。
第4步:将修改纳入到本地 build-history 分支中
master 分支上的新提交必须要被纳入到 build-history 分支中去。由于 build-history 分支 上没有开发活动,所以merge
命令通常带来的是一次快进式合并,也就是说,build-history 分支会直接指向 master 分支的当前提交。
但由于我们希望用 build-history 分支上的第一父级历史来检索成功的构建,所以这里应该使用的是带--no-ff
选项的merge
命令。> git merge --no-ff --no-commit origin/master
这条
merge
命令使用了以下参数。--no-ff
: 表示不允许执行快进式合并。--no-commit
: 通过这个选项,合并操作虽然还会在工作区中进行,但最初不会产生提交。只有成功执行完构建和测试动作后,提交才会被创建。
第5步:完成构建
现在,我们可以在当前工作区中使用构建服务器来构建软件并运行其测试了。这个过程不会牵扯到 Git 。当发生错误时,该工作流就会被终止,构建服务器会用Email 将情况通知给开发者。第6步:完成提交
如果构建成功,预备的提交就会被执行。提交信息中将会包含构建服务器分配它的构建编号。> git commit -m "build <build-nummer>"
如果构建或测试失败了,我们可以直接取消这个点上操作。在下一轮循环中,工作区将会被重置(参见第3步)。
第7步:标记最后一次被成功构建的版本
中央版本库上的 last-build 分支应该始终指向 origin/master 分支上最后一次被成功构建的提交。
为此,本地的last-build 分支应该先在构建版本库中被创建,或被设置在正确的提交上。 麻烦的是我们要将该分支设置到origin/master 提交的正确提交上,而不是当前 build-history分支的合并提交。我们可以看到在 Git 中,合并提交的父级提交都被标记上了
^
-符号:^1
表示当前分支中的父级提交,^2
则表示新增分支上的父级提交。
以下图中的提交Z 为例,它的第一父级提交是 build-history 分支上的提交 Y, 第二父级提交是 origin/master 分支上的提交 D。
接下来,我们要用branch
命令创建一个新的last-build 分支,或者修改现有的 last-build 分支,使其指向origin/master 分支上的提交:> git branch --force last-build HEAD^2
这里的
--force
选项用于确保branch
命令始终会新建一个last-build 分支,即使在该分支已经存在的情况下。
而HEAD^2
参数则引用的是 origin/master 分支上的构建提交。在让该分支指向本地正确的提交之后,它还必须要被传送给中央版本库。所以接下来我们要使用到
push
命令。> git push --force origin last-build:last-build
在这里,我们用到了以下参数。
--force
: 该选项用于确保本地的 last-build 分支会始终被置换成中央版本库中的新提交,即使该提交并不是最后一个成功的 Last-Build 提交。origin
: 该参数用于引用中央版本库。last-build
: 该参数用于表示本地的 last-build 分支将会被传送给中央版本库中的 last-build 分支。
3.3 比对本地开发版本与最后成功构建版本之间的差异
在完成对中央版本库的合并之后,每当开发者自己的版本库中出现内容错误时,拿他们所做的修改与上次被成功构建的版本做一个比对,会是个很有帮助的策略。
在接下来的内容中,我们将会介绍如何比对本地版本与最后一次成功构建版本之间的状态差异。
第1步:检查中央版本库中的提交
首先,我们应该要检查一下中央版本库中是否还有来自其他开发者,但又不属于成功构建版本的提交。因为错误也有可能来自于其他人。
为此,我们可以用log
命令来确定一下中央版本库的 origin/master 分支上是否还存在一些尚未被纳入到 origin/last-build 分支中的提交。例如在下图中,我们会发现提交 C 就属于这种情况。> git log origin/last-build..origin/master
我们可以用
diff
命令来比对其中发生的修改。> git diff origin/last-build origin/master
第2步:反思本地提交
下面,我们要检查以下自己的版本与最后一次成功之间发生了哪些修改。> git diff origin/last-build
以下图为例,这条命令会拿提交F 中的内容与提交B 的内容进行比对。这样提交C、D、 E 和F 所做的所有修改都会被显示出来。
3.4 基于构建历史的排错
在大多数情况下,想要在代码中找到引发错误的地方是很容易的。通常只要阅读一下对错误的描述,就基本能够了解这一定是那些事出了差错。但是,偶尔也会出现一些悄无声息 的错误,它们很难被发现。这样一来,如果能确切地知道这些错误在软件中第一次出错的时间,当然对排错工作就很有帮助了。现在,既然我们可以通过 Git 将软件快速恢复到旧版本 上,当然应该可以利用这点来系统性地找出错误。我们可以从没有出现该错误的旧版本开始, 找出每个之后的版本,检查其中是否有错误发生。通过在版本历史中重复这一动作,我们一定可以找到第一次出现该错误的那次提交。如果运气好的话,检查到的差异可能会很小,我们可以非常精确地修复这个错误。
然而,上述过程也有可能非常繁琐。因此 Git 为我们提供了 bisect
命令,以帮助我们来处理这些情况。该命令会获取提交历史的“中间”提交,并对该所影响的区域启动测试。以确定选择位于目前提交的左侧还是右侧的提交来继续处理,以此来找出问题的提交。
当我们只需要考虑之前至少成功被构建并通过测试的版本时,这种方法是最有效的。 如果不是这种情况,我们就可能会因为一些因为严重错误而无法构建的旧版本而浪费大量的时间。
这时候就轮到我们的构建历史“粉墨登场”了,因为该分支中只包含已被成功构建的提交,并且它们也都成功通过了所有的单元测试。但是,构建历史通常既不包含在本地版本库中,也不存在于该分支的中央版本库中。我们也许要通过 fetch
命令,从其他版本库中导入这些提交。
第1步:链接到构建版本库
如果想访问构建版本库,我们就需要在开发者版本库中创建一个名为“build”的远程版本库。> git remote add build <build-repo-url>
第2步:搬运构建历史
接下来,我们要通过fetch
命令将版本历史搬运到本地的开发版本库中。> git fetch build
第3步:创建一个本地分支
基于 build-history 分支创建一个本地分支,对日后的操作会有所帮助。> git checkout -b history build/build-history
第4步:执行 bisect 命令
接下来,我们要定义一个合适的最初状态为 “good” 的提交,并以此为起点执行bisect
命令开始调试。> git bisect start HEAD <good-commit>
现在, Git 会在构建历史中选取一个“中间”提交并对其进行测试。然后我们会依据当前提交被标记为 “good” 还是 “bad” 的结果来执行我们的测试。
> git bisect good
或
> git bisect bad
由于 Git 对于合并提交相关的父级提交都会查看,所以有可能它所选取的提交并非来自于我们的 build-history分支。例如,在下图所示的这段典型的提交历史中,如果 Git 所使用的算法倾向于检查X 与Z 之间的提交,那么它也有可能会选取 master 分支上B~D 之间的提交。
第5步:解释结果
在成功执行一次二分查找排错之后,我们在构建历史中找到了出问题的提交。但该提交带来的问题在构建历史之后的每次提交,以及 master 分支上的若干次提交中都有可能会被弹出来(例如上图中的提交Y) 。所以,为了打印出可能出问题的这些提交的日志信息,我们还需再做一些杂事。
在这里,每个构建提交对象的第二父级提交(即^2
)都在 master 分支上有一个对应的提交对象。而构建提交的第一父级提交(即^1
)对应的则是我们之前构建的提交。
接下来,我们要通过log
命令来确定一下 master分支上这些以某个“问题”构建提交为基础发展起来的提交是否全都会引发错误> git log <bad-commit>^1^2..<bad-commit>^2
如果上图中的提交Y 已被认定为不合格,那么
log
命令机就会显示提交B 和 C 也有可能会引发错误。第6步:清理
在执行完bisect
命令之后,我们应该将所有不必要的分支、提交以及和远程版本库从开 发者版本库中清除出去:> git bisect reset > git checkout master > git branch -D build-history > git remote rm build
4️⃣ 替代解决方案
4.1 使用标签
对于构建历史来说,标签是一种既可以存储成功构建版本,又可以在独立分支中存储合并提交的替代实现方案。这个方案甚至会让整个过程变得更为简单,因为我们不必为其做任何预备工作和执行合并提交。我们只需要考虑以下几点。
- 这种方案会创建大量的标签,可能会导致其他非构建类标签难以被找到。
- 这种方案只有在那些构建和测试都不可能执行的提交上才会给二分查找排错带来更高的效率。
- 在这种方案中,被构建版本(标签)的逻辑顺序只能通过构建编号来隐式表示。
对于last-build 分支也是如此,我们可以用标签来代替分支。但这需要我们在每次成功构建之后都要删除并重建标签。这在本地版本库中是可行的,但一旦我们将标签传送到了中央版本库中,就会出问题了。
虽然就目前的情况来说,我们要在中央版本库中删除并重建一个标签还是有可能的。但 其所有的克隆版本库并不能通过调用 pull
命令来获取更新的标签,因为该命令会忽略掉任何现有的标签。
4.2 将构建历史放在中央版本库中
在我们的实现说明中,构建历史不会随中央版本库一起发布,它只在构建版本库中是可见的。为什么将构建历史存储在中央版本库中是毫无意义的呢?
其主要的原因是这样做会带来很多合并提交,会“污染”正常的提交历史。每一次成功的构建都是基于origin/master 分支和 build-history 分支的一次新的合并提交。在正常的项目历史视角中,什么版本被成功构建过是无关紧要的信息。这些合并提交会给其日志输出带来不少混乱。
最为糟糕的情况是,如果我们的构建服务器不只有一个而是多个的话。例如,因为master 分支和 codefreeze 分支都可以被构建,中央版本库中就会有更多的构建提交。
栅格方法的优势在于,它始终可以将将构建历史中的提交与版本库中的正常项目提交汇集在一起。而要达到这一目的,我们就要既可以在中央版本库中,也可以在构建版本库中执行 fetch
命令。
《【Git教程】(十五)二分法排错 — 概述及使用要求,执行过程及其实现(用二分法人工排错或自动排错),替代解决方案 ~》