在上篇文章中,我们介绍了 Git 内部存储对象的方式,以及为什么不要用 Git 去管理大的二进制文件。

本文将继续探讨上篇文章中遗留的两个问题。

  1. 如果两个 Branch 修改同一个文件的同一行代码,各自 Commit 一次,在 Merge 的时候为什么不会有冲突?
  2. 同样是两个 Branch 修改同一个文件的同一行代码,各自 Commit 一次,在 Rebase 的时候为何有一个 Commit 会被优化掉?

Merge 的测试代码

首先,我们新建一个分支,修改 a.txt,添加一行内容“Learn Git”:

git checkout -b testBranch
echo "Learn Git" >> a.txt
git commit -a -m 'commit'

此时,我们用 find 命令查看一下新增的 object 对象:

find .git/objects -type f

2015-09-21-three-git-question-2_git-cat-file-1.png

这里我们可以看到多了 3 个对象:407b6e (commit), 49bf80 (modified a.txt) 和 81f485(tree object)。我们可以用 git cat-file -p hash-code 来查看这 3 个对象的值。(注意这里我们使用的是 6 位数字的简码)

现在,让我们切回主分支,并且对主分支的 a.txt 做同样的修改:

git checkout master
echo "Learn Git" >> a.txt
git commit -a -m 'commit'

此时我们再查看一样新增的 object 对象:

find .git/objects -type f

2015-09-21-three-git-question-2_git-cat-file-2.png

此时我们只新增了一个新的对象 9e2107(commit),我们用 git cat-file 查看其值为:

git cat-file -p 9e2107(如果这里换成 407b6e,结果是一样的

2015-09-21-three-git-question-2_tree-object.png

此时,如果我们 merge testBranch 分支是不会有冲突的。

git merge testBranch
find .git/objects -type f

2015-09-21-three-git-question-2_cat-file-3.png

此时,我们发现多了一个 1bff79 对象,它的值如下:

git cat-file -p 1bff79

2015-09-21-three-git-question-2_tree-object-2.png

我们知道,整个过程中,我们在两个分支上分别对 a.txt 的同一行代码做了同样的修改,但是最终 git 只会保存一份 a.txt 的内容。另外,整个目录树在不同的分支上面是一样的,所以 tree 对象也只有一份。

虽然我们在不同的分支上面做了两次 commit,但是这两次 commit 只是记录了各自的 tree,parent 以及作者等信息。

而在 merge 的时候,由于我们是 two-way merge,所以最终生成的 merge commit 是有两个 parent 的。

由上面的测试代码我们可以清楚地知道 merge 到底干了些什么事。

Rebase 的测试代码

我们先 reset 掉刚刚 merge 的代码,并且切换到 testBranch 分支:

git reset --hard HEAD~1
git checkout testBranch

此时我们运行 find 命令来查找 objects 目录:

find .git/objects -type f

虽然我们的 1bff79 commit 已经被我们用 reset –hard 命令撤销了,但是我们发现它还是在版本里面,我们可以用 git reflog 来找回这个 commit。 (这里扯远了,不过这也说明了,在 Git 里面只要 commit,你做的修改就不会丢。所以,程序猿们,commit early, commit often 吧。不过记得 publish 之前记得整理 commmits,哈哈,这又是另一个话题了。。。)

现在,让我们 rebase 吧!

git rebase master

接下来,让我们看看 objects 的情况:

find .git/objects -type f

2015-09-21-three-git-question-2_cat-file-4.png

What???

这次操作一个新的 objects 都没有生成。

要理解这个,我们需要清楚地知道什么是 rebase。

当你 rebase 的时候,你会把当前分支上的所有的 commit 全部丢弃,然后把这些 commit 的修改依次应用到目标分支上,并且此时会生成新的 tree 对象和 commit 对象。但是,如果进行 rebase 分支的 commit 和目标分支的 commit 是一样的时候(指的是 tree 和 parent 一样),会用当前 commit 替换掉目标 commit。

这段话听起来有点拗口,但是其实很容易理解。

我们拿上面的示例代码来看。当我们 rebase 的时候,9e2107 commit 和 407b6e commit 是一样的内容,此时会用 9e2107 commit 来替换掉 407b6e commit,即我们前面所指的 commit 优化。

小结

当我们对于 Git 的基本原理有了一定的认识之后,像 rebase 和 squash 这样的高级工具就容易掌握了。而且 rebase 和 squash 是打造整洁的 commit 历史必不可少的工具。