Git分支机制

分支意味着偏离开发主线并继续你自己的工作而不影响主线开发。Git的分支模型被称为是Git的“杀手锏特性”,而这也使得Git在众多版本系统中,脱颖而出。

3.1 分支机制简述

首先,我们应该深刻理解一些Git存储数据的原理,当你发起一次提交,Git存储的是提交对象,其中包含了指向暂存区快照的指针。提交对象也包含作者姓名和邮箱地址、已输入的提交信息以及指向其父提交的指针。初始提交没有父提交,而一般的提交会有一个父提交;对于两个或者更多分支的和并提交来说,存在着多个父提交。

Git的分支只不过是一个指向某次提交的轻量级的可移动指针。Git默认的分支名称是master。当你发起提交时,就有了一个指向最后一次提交的master分支。每次提交时,都会自动向前移动。(Git中的master分支并不是一个特殊的分支,它与其他分支并没有什么区别,几乎每个Git仓库都有该分支,这只是因为git init命令会默认创建该分支,而大多数人都懒得去更改它)。

 

3.1.1 创建新分支

可以通过git branch来实现:

$ git branch testing

这会创建一个指向当前提交的新指针。

Git如何知道你当前处于哪一个分支上呢?Git实际上维持着一个名为HEAD的特殊指针。HEAD指向当前所在的本地分支的指针。在上述操作后,你仍然可以处在master分支上,因为这是git branch命令只会创建新分支,而不会切换到新的分支上去。

可以简单的通过git log命令来查看各个分支当前所指的对象,这需要用到选项--decorate

$ git log --oneline --decorate

 

3.1.2 切换分支

要切换已有的分支,可以执行git checkout命令,现在让我们切换到testing分支上。

$ git checkout testing

 这条命令会改变HEAD指针,使其指向testing分支。

然后我们再提交一次。

$ vim test.rb
$ git commit -a -m 'made a change'

现在testing分支已经向前移动,然而master分支仍然指向之前执行git checkout切换分支时所在的提交,让我们再切换到master分支:

$ git checkout master

 

这条命令一共做了两件事。它把HEAD指针移回到master分支,还会把工作目录的文件恢复到master分支指向的快照的状态。这就意味着,你从现在开始所作的修改将基于项目较老的版本。总而言之,上述操作回滚了你在testing分支上所做的操作,是你能向另一个方向进行开发工作。

注意:分支切换会更改工作目录文件,如果你切换到较旧的分支,工作目录会被恢复到该分支上最后一次提交的状态。如果Git在当前状态下无法干净地完成恢复操作,就不会允许你切换分支。

然后我们在当前分支再做出一些改动之后,项目的历史就会产生分叉,你在新创建的分支上做了一些修改,然后在主分支上又做了一些修改,这两次修改是在不同的分支上做出的,彼此互相分离。你可以在分支间自由切换。

 

3.2 基本的分支与合并操作

现在我们展示一个简单的分支和合并案例。

  1. 在开发某个项目;
  2. 为新需求创建分支;
  3. 在新分支上展开工作;

  这是,你接到电话,说项目有个严重的问题需要紧急修复,你随后会这样做:

  1. 切换到你的生产环境分支;
  2. 创建新的分支来进行此问题的热修补工作;
  3. 通过测试后,合并热修补分支并推送到生产环境中;
  4. 切换回之前的需求分支上继续工作。

 

3.2.1 基本的分支操作

 

首先,假设你所在的工作的项目已经完成了一些提交。

 

这时,你决定要修复公司所用的问题跟踪系统中的#53问题。可以使用带有-b选项的git checkout命令来创建并切换到新分支上:

$ git checkout -b iss53

 上面这条命令相当于:

$ git branch iss53
$ git checkout iss53

 

 

 

接下来继续工作,并又进行了几次提交。这么做会让iss53分支指针向前移动,这时因为你当前所在的就是iss53分支(即HEAD指针当前就指向这条分支);

$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'

 

 

 

 现在,你接到一个电话,说网站有个问题需要立即修复。如果没有Git,你可能需要花费大量的精力去恢复之前针对iss53所作的工作,方便后续让你制作的修复补丁单独上线,而现在,你只需要切换回master分支即可。

首先,在切换分支之前,需要注意,如果你的工作目录或者暂存区存在着未提交的更改,并且这些更改与你要切换到的分支冲突,Git就不允许你切换分支。在切换分支时,最好是保持一个干净的工作区域。

假如现在你已经提交了所有的修改,这样你就可以切换回master分支了:

$ git checkout master

 此时项目的工作目录就与你开始处理#53问题之前的状态一模一样了,你就可以集中精力制作热补丁了。这里有一点要强调:当你切换分支时,Git会把工作目录恢复到你切换到的分支上最后一次提交时的状态。

接下来需要制作热补丁。我们创建hotfix分支并在这个分支上展开修复工作:

$ git checkout -b hotfix
$ vim index.html
$ git commit -a -m 'fixed the broken email address'

你可以运行测试来确保热补丁的效果无误,然后将其合并到master分支,以便部署到生产环境。使用git merge命令来完成上述操作:

$ git checkout master
$ git merge hotfix

 你会注意到合并时出现了“fast-forward"的提示。由于当前所在的master分支所指向的提交是要并入的hotfix分支的直接上游,因而Git会将master分支指针向前移动。换句话说,当你试图去合并两个不同的提交,而顺着其中一个提交的历史可以直接到达另一个提交时,Git就会简化合并操作,直接把分支指针向前移动,因为这种单线历史不存在有分歧的工作。这就叫做”fast-forward“。

现在你的变更已经进入了master分支所指向的提交快照,可以部署补丁了。

 

在部署了这次极其重要的补丁之后,你准备要切换回之前被打断的工作上去。不过先别急,首先你要把已经用不着的hotfix分支删除,该分支和master分支指向的位置相同。使用git branch的-d选项来删除这个分支:

$ git branch -d hotfix

 现在你可以切换回之前未完成的#53问题分支,并且继续进行工作:

$ git checkout iss53
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'

 

值得注意的是,iss53分支并不包括你在hotfix分支上所作的工作。如果需要上述修补工作并入iss53,就需要执行git merge master使得master分支合并到iss53中,或者可以等到要把iss53合并回master分支时再把热修补的工作整合进来。

 

3.2.2 基本的合并操作

假设现在#53的工作已经完工,可以合并回master分支了。这次合并操作实现起来与之前合并hotfix分支的操作差不多。只需要切换到master分支上,并执行git merge命令即可。

$ git checkout master
$ git merge iss53

 这次合并看起来与之前hotfix的合并有点不一样。在这次合并中,开发历史从某个早先的时间点开始有了分叉。由于当时master分支指向的提交(C4)并不是iss53分支的直接祖先,因为Git必须要做一些额外的工作。本例中,Git执行的操作时简单的三方合并。三方合并操作会使用两个待合并分支上最新提交的快照,以及这两个分支的共同祖先的提交快照(如下图所示)。

 与之前的简单地向前移动分支指针地做法不同,这次Git会基于三方合并的结果创造新的快照,然后在创建一个提交指向新建的快照。这个操作叫做”合并提交“。合并提交的特殊性在于它不止拥有一个父提交。

 

值得注意的是,Git会自己判断最优的共同祖先并将其作为合并基础。

现在你的工作成果已经合并进来了,你就不需要iss53分支了。你可以在问题追踪系统里面关闭这个问题并删除分支。

$ git branch -d iss53

 3.2.3 基本的合并冲突处理

有时候,上述合并过程并不会那么顺利。如果你要在合并的两个分支上都改了同一个文件的同一部分内容,Git就没办法干净地合并这两个分支。假设你在#53问题上的工作和在hotfix分支上的工作都修改了同一文件的同一部分,那么就会引起合并冲突,你会看到类似下面的输出:

$ git merge iss53
Auto-merge index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

 Git并没有自动创建新的合并提交。它会暂停整个合并过程,等待你来解决冲突。在发生了合并冲突后,要查看哪些文件没有被合并,可以执行git status:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

  both modified:  index.html

no changes added to commit (use "git add" and/or "git commit -a")

 任何存在着为解决的合并冲突的文件都会显示成未合并状态。Git会给这些有冲突的文件添加标准的待解决冲突标记,以便你手动打开这些文件来解决冲突。

你可以选择任一版本的内容或者是自己整合两者的内容来解决冲突。这种解决方式实际上是把两个版本的内容各取一部分整合到一起。在解决了每个冲突文件的所有冲突部分后,就可以执行git add来把每个文件标记为冲突已解决状态。在Git中,这可以通过把文件添加到暂存区来实现。

若要使用图形化工具解决冲突,可以执行git mergetool, 该命令会启动相应的图形化合并工具,并引导你一步步解决冲突。当退出合并工具时,Git会询问合并是否已经成功完成。如果合并成功完成,它就会将合并后的文件添加到暂存区,并将其标记为冲突已解决的状态。可以再次执行git status 来确认所有的冲突都已解决。

如果觉得满意了,并确认了所有冲突都已解决,相应的文件也进入了暂存区,就可以通过git commit命令来完成此次合并提交。

如果想给审批此次合并的人一点提示,那么可以修改上述的合并信息,提供更多的关于你如何进行此次合并的细节,比如你做了什么,你为什么要这样做。

 

3.3 分支管理

现在,我们来试试一些分支管理工具,这些工具在经常使用分支时会很有用。

git branch命令不只是可以用来创建和删除分支。如果你执行不带参数的git branch命令,就会得到当前所有分支的简短列表,如下所示:

$ git branch
  iss53
* master
  testing

请留意master前面的*字符,它表明了你当前所在的分支(即HEAD指向的分支)。这意味着如果你现在进行一次提交,master分支指针会随着你的新提交向前移动。要看到每个分支上的最新提交,可以执行git branch -v。

另外两个有用的选项:--merged是筛选已并入当前分支的所有分支;

          --no-merged,筛选尚未并入的所有分支。

要查看哪些分支已经并入当前分支,可以执行git branch --merged:

$ git branch --merged
  iss53
* master

由于之前iss53已被合并,因此它出现在了上述列表中。一般来说,对于前面没有*的分支,可以使用git branch -d把它们全部删除。你已经把这些分支上的工作全部纳入到了其他分支中,所以不会因此而丢失任何东西。

要查看尚未合并的工作的所有分支,可以使用git branch --no-merged:

$ git branch --no-merged
  testing

上述命令会显示出另一个分支。因为该分支包含了尚未合并到主线的工作,所以git branch -d并不能成功删除它:

$ git branch
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.

如果你确实想要删除该分支并丢弃其上的所有工作,可以按照上述输出的提示信息使用-D选项强制删除。

 

3.4 与分支有关的工作流

现在我们讲一些简单的工作流。他们之所以存在,得益于Git的轻量级分支机制。你可以根据自己项目的实际情况来自由选用他们。

3.4.1 长期分支

由于Git简洁的三方合并机制,在较长的一段时间内多次把一个分支合并到另一个分支是很容易的操作。这意味着你可以拥有多个开放的分支,以用于开发周期的不同阶段;你也可以经常性地把其中某些分支合并到其他的分支中去。

很多使用Git的开发者都喜欢用这种方式构建他们自己的工作流。例如,其中一种流程就是在master分支只存放稳定版本的代码,即已经发布版本或即将发布版本的代码。他们还会使用另一个叫做develop或next的平行分支用于开发,或是用于测试代码的稳定性。这个分支不会一直保持稳定版本,不过一旦它达到稳定版本的状态,就可以把它合并到master分支去。这样的分支也被用来接受主题分支(短期分支,例如之前的iss53分支)的合并,来确保这些新开发的特性能够通过所有的测试而不会引发新的错误。

实际上,我们刚才谈论的是随着你的提交操作而不断移动的分支指针。稳定的分支会在提交历史中较为靠后,而前沿的开发分支会较为靠前。

 

 

 可以把这些分支认为是不同的工作简仓(work silo),几组提交经过完整的测试后,就会从一个简仓移动到另一个更稳定的简仓中去。

 

可以按照上述 方式构建几个不同稳定级别的分支。有些大型项目有名为proposed(提议)或pu(proposed updates,提议的更新)的分支。这个分支会整合那些还没有准备好并入next或master的分支。这么做背后的缘由是不同的分支拥有不同程度的稳定性。当分支达到更高的稳定程度时,它就被合并到更高级别的分支中去。所以,虽然拥有多个长期分支并非必须,但这样很实用,特别是当你开发大型项目或复杂项目时更是如此。

3.4.2 主题分支

与上述长期分支有所不同,在任何规模的项目上主题分支(topic branch)都非常有用。主题分支是指短期的、用于实现某一特定功能及其相关工作的分支...

 

 

待续,

posted @ 2022-01-11 22:07  禅主  阅读(950)  评论(0编辑  收藏  举报