5. 分支与合并2

分支是我最喜欢的 Git 特性之一。如果你用过其他版本控制系统,把你所知的分支给忘记,倒可能更有帮助些 —— 事实上,以我们使用分支的方式,把 Git 的分支看作 上下文 反而更合适。 当你检出分支时,你可以在两三个不同的分支之间来回切换。

简而言之,你可以执行 Git branch (branchname) 来创建分支, 使用 git checkout (branchname) 命令切换到该分支,在该分支的上下文环境中, 提交快照等,之后可以很容易地来回切换。当你切换分支的时候,Git 会用该分支的最后提交的快照替换你的工作目录的内容, 所以多个分支不需要多个目录。使用 Git merge 来合并分支。你可以多次合并到统一分支, 也可以选择在合并之后直接删除被并入的分支。

Git log 显示一个分支中提交的更改记录

到目前为止,我们已经提交快照到项目中,在不同的各自分离的上下文中切换, 但假如我们忘了自己是如何到目前这一步的那该怎么办?或者假如我们想知道此分支与彼分支到底有啥区别? Git 提供了一个告诉你使你达成当前快照的所有提交消息的工具,叫做 git log

要理解日志(log)命令,你需要了解当执行 git commit 以存储一个快照的时候,都有啥信息被保存了。 除了文件详单、提交消息和提交者的信息,Git 还保存了你的此次提交所基于的快照。 也就是,假如你克隆了一个项目,你是在什么快照的基础上做的修改而得到新保存的快照的? 这有益于为项目进程提供上下文,使 Git 能够弄明白谁做了什么改动。 如果 Git 有你的快照所基于的快照的话,它就能自动判断你都改变了什么。而新提交所基于的提交,被称作新提交的“父亲”。

某分支的按时间排序的“父亲”列表,当你在该分支时,可以执行 git log 以查看。 例如,如果我们在本章中操作的 Hello World 项目中执行 git log,我们可以看到已提交的消息。

$ git log
commit 8d585ea6faf99facd39b55d6f6a3b3f481ad0d3d
Merge: 3cbb6aa 3ac015d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jun 4 12:59:47 2010 +0200

    Merge branch 'fix_readme'

    Conflicts:
        README

commit 3cbb6aae5c0cbd711c098e113ae436801371c95e
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jun 4 12:58:53 2010 +0200

    fixed readme title differently

commit 3ac015da8ade34d4c7ebeffa2053fcac33fb495b
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jun 4 12:58:36 2010 +0200

    fixed readme title

commit 558151a95567ba4181bab5746bc8f34bd87143d6
Merge: b7ae93b 3467b0a
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jun 4 12:37:05 2010 +0200

    Merge branch 'change_class'
...

我们可以用 --oneline 选项来查看历史记录的紧凑简洁的版本。

$ git log --oneline
8d585ea Merge branch 'fix_readme'
3cbb6aa fixed readme title differently
3ac015d fixed readme title
558151a Merge branch 'change_class'
b7ae93b added from ruby
3467b0a changed the class name
17f4acf first commit

这告诉我们的是,此项目的开发历史。如果提交消息描述性很好,这就能为我们提供关于有啥改动被应用、或者影响了当前快照的状态、以及这快照里头都有啥。

我们还可以用它的十分有帮助的 --graph 选项,查看历史中什么时候出现了分支、合并。以下为相同的命令,开启了拓扑图选项:

$ git log --oneline --graph
*   8d585ea Merge branch 'fix_readme'
|\
| * 3ac015d fixed readme title
* | 3cbb6aa fixed readme title differently
|/
*   558151a Merge branch 'change_class'
|\
| * 3467b0a changed the class name
* | b7ae93b added from ruby
|/
* 17f4acf first commit

现在我们可以更清楚明了地看到何时工作分叉、又何时归并。 这对查看发生了什么、应用了什么改变很有帮助,并且极大地帮助你管理你的分支。 让我们创建一个分支,在里头做些事情,然后切回到主分支,也做点事情,然后看看 log 命令是如何帮助我们理清这俩分支上都发生了啥的。

首先我们创建一个分支,来添加 Erlang 编程语言的 Hello World 示例 —— 我们想要在一个分支里头做这个,以避免让可能还不能工作的代码弄乱我们的稳定分支。 这样就可以切来切去,片叶不沾身。

$ git checkout -b erlang
Switched to a new branch 'erlang'
$ vim erlang_hw.erl
$ git add erlang_hw.erl 
$ git commit -m 'added erlang'
[erlang ab5ab4c] added erlang
 1 files changed, 5 insertions(+), 0 deletions(-)
 create mode 100644 erlang_hw.erl

由于我们玩函数式编程很开心,以至于沉迷其中,又在“erlang”分支中添加了一个 Haskell 的示例程序。

$ vim haskell.hs
$ git add haskell.hs 
$ git commit -m 'added haskell'
[erlang 1834130] added haskell
 1 files changed, 4 insertions(+), 0 deletions(-)
 create mode 100644 haskell.hs

最后,我们决定还是把 Ruby 程序的类名改回原先的样子。与其创建另一个分支,我们可以返回主分支,改变它,然后直接提交。

$ git checkout master
Switched to branch 'master'
$ ls
README  ruby.rb
$ vim ruby.rb 
$ git commit -am 'reverted to old class name'
[master 594f90b] reverted to old class name
 1 files changed, 2 insertions(+), 2 deletions(-)

现在假设我们有段时间不做这个项目了,我们做别的去了。 当我们回来的时候,我们想知道“erlang”分支都是啥,而主分支的进度又是怎样。 仅仅看分支的名字,我们是无从知道自己还在里面有 Haskell 的改动的,但是用 git log 我们就可以。 如果你在命令行中提供一个分支名字,它就会显示该分支历史中“可及”的提交,即从该分支创立起可追溯的影响了最终的快照的提交。

$ git log --oneline erlang
1834130 added haskell
ab5ab4c added erlang
8d585ea Merge branch 'fix_readme'
3cbb6aa fixed readme title differently
3ac015d fixed readme title
558151a Merge branch 'change_class'
b7ae93b added from ruby
3467b0a changed the class name
17f4acf first commit

如此,我们很容易就看到分支里头还包括了 Haskell 代码(高亮显示了)。 更酷的是,我们很容易地告诉 Git,我们只对某个分支中可及的提交感兴趣。换句话说,某分支中与其他分支相比唯一的提交。

在此例中,如果我们想要合并“erlang”分支,我们需要看当合并的时候,都有啥提交会作用到我们的快照上去。 我们告诉 Git 的方式是,在不想要看到的分支前放一个 ^。 例如,如果我们想要看“erlang”分支中但不在主分支中的提交,我们可以用 erlang ^master,或者反之。

$ git log --oneline erlang ^master
1834130 added haskell
ab5ab4c added erlang
$ git log --oneline master ^erlang
594f90b reverted to old class name

这为我们提供了一个良好的、简易的分支管理工具。它使我们能够非常容易地查看对某个分支唯一的提交,从而知道我们缺少什么,以及当我们要合并时,会有什么被合并进去。

简而言之 使用 git log 列出促成当前分支目前的快照的提交历史记录。这使你能够看到项目是如何到达现在的状况的。

Git tag 给历史记录中的某个重要的一点打上标签

如果你达到一个重要的阶段,并希望永远记住那个特别的提交快照,你可以使用 git tag 给它打上标签。 该 tag 命令基本上会给该特殊提交打上永久的书签,从而使你在将来能够用它与其他提交比较。 通常,你会在切取一个发布版本或者交付一些东西的时候打个标签。

比如说,我们想为我们的 Hello World 项目发布一个“1.0”版本。 我们可以用 git tag -a v1.0 命令给最新一次提交打上(HEAD)“v1.0”的标签。 -a 选项意为“创建一个带注解的标签”,从而使你为标签添加注解。绝大部分时候都会这么做的。 不用 -a 选项也可以执行的,但它不会记录这标签是啥时候打的,谁打的,也不会让你添加个标签的注解。 我推荐一直创建带注解的标签。

$ git tag -a v1.0 

当你执行 git tag -a 命令时,Git 会打开你的编辑器,让你写一句标签注解,就像你给提交写注解一样。

现在,注意当我们执行 git log --decorate 时,我们可以看到我们的标签了:

$ git log --oneline --decorate --graph
* 594f90b (HEAD, tag: v1.0, master) reverted to old class name
*   8d585ea Merge branch 'fix_readme'
|\
| * 3ac015d (fix_readme) fixed readme title
* | 3cbb6aa fixed readme title differently
|/
*   558151a Merge branch 'change_class'
|\
| * 3467b0a changed the class name
* | b7ae93b added from ruby
|/
* 17f4acf first commit

如果我们有新提交,该标签依然会待在该提交的边上,所以我们已经给那个特定快照永久打上标签,并且能够将它与未来的快照做比较。

不过我们并不需要给当前提交打标签。如果我们忘了给某个提交打标签,又将它发布了,我们可以给它追加标签。 在相同的命令末尾加上提交的 SHA,执行,就可以了。 例如,假设我们发布了提交 558151a(几个提交之前的事情了),但是那时候忘了给它打标签。 我们现在也可以:

$ git tag -a v0.9 558151a
$ git log --oneline --decorate --graph
* 594f90b (HEAD, tag: v1.0, master) reverted to old class name
*   8d585ea Merge branch 'fix_readme'
|\
| * 3ac015d (fix_readme) fixed readme title
* | 3cbb6aa fixed readme title differently
|/
*   558151a (tag: v0.9) Merge branch 'change_class'
|\
| * 3467b0a changed the class name
* | b7ae93b added from ruby
|/
* 17f4acf first commit