Table of Contents:
Part 1 - What is a commit hash?
在学习如何使用Git时,我曾经遇到过的最大困难之一就是merge
和rebase
之间的区别。大多数人很快就理解了merge
的概念,但是当试图理解rebase
的不同之处时,他们会迷失方向。在这篇由三部分组成的文章中,我将以最简单的方式介绍这些差异。为了做到这一点,我们首先需要了解什么是commit hash
。
如果您曾经在Git中查看过提交历史记录,那么您可能会看到类似的情况:
commit a9ca2c9f4e1e0061075aa47cbb97201a43b0f66f
Author: Alex Ford
Date: Mon Sep 8 6:49:17 2014
Initial commit.
您可能已经将长串字母和数字(commit hash 值)视为某一特定提交的唯一ID。虽然这样是对的,但是你可能还不知道它是一个生成的SHA-1 hash
,代表git 的 commit object
。如果不了解Git提交对象的可怕细节,那么你只需知道它是根据它所代表的信息直接生成的,一个很大的加密字符串。因为它是根据提交中包含的信息生成的,所以哈希值不能更改。更改提交哈希值的唯一方法是更改有关提交的详细信息,该提交本质上用一个全新的哈希生成一个全新的提交。
除了提交作者、日期和存储的数据等所有明显的信息外,每个提交还包含上一个提交的hash值。这正是生成提交历史记录的方式。每个提交都知道它前面的提交的哈希值。
在上面的图片中,您可以看到我的SourceTree
窗口中我创建的演示仓库。我在演示仓库做了三次commit
。SourceTree
足够智能,可以读取我的仓库中的每个提交,并构建此历史的图形表示。可以看到commit 2
直接引用commit 1
,而commit 1
直接引用commit 0
。请注意,为了便于讨论,我把提交的commit message
直接写成了序号,真正的commit message
应该描述在该提交中所做的更改。
由于我的演示仓库只包含对主分支的三次提交,所以SourceTree
的分支网络图是从每次提交到下一次提交的一条简单的直线。让我们先创建一个单独的分支来处理一个特性,从而使事情变得更复杂一些。
在上面的屏幕截图中,你可以看到我创建了一个名为feature1
的分支,但两个分支的网络图是相同的。这是因为自创建分支以来,我没有做出任何新的commit
。两个都指向相同的提交的引用(不明白什么是提交的引用可以参考这篇文章:Git仓库.git文件夹目录介绍
)。现在,master
和我的feature1
分支都指向完全相同的commit
。现在,我们进行一些更改,并向feature1
分支添加新的commit
。
可以看到,我们的Feature1
分支已经移动到指向一个新的commit
,commit 3
。还可以看到,我们的分支网络图仍然是一条简单的直线。这是因为到目前为止,总共只有四个提交,每个提交都引用它前面的一个提交。如果我现在将feature1
合并到master
中,那么唯一会发生的事情就是master
分支会跳到与feature1
相同的commit
上,即commit 3
。这称为快进合并(fast-forward merge
),因为它只是将master
分支的指针向上移动,以指向较新的commit
。
好吧,现在我们很高兴地结束了我们的feature1
分支的工作,当老板突然打电话说一个新的bug已经被记录在案,这是首要任务。我们需要停止对feature1
的工作,并立即致力于错误修复。要做到这一点,我们需要切换到master
并作出commit
。如果bug很大,我们可以考虑创建另一个分支并对该分支进行多次commit
,但是我们会假装bug很小,并且很容易在一次commit
中修复。
现在情况有点不一样了。在上面的截图中,我已经把你的注意力集中在仓库网络图上了。注意,现在我们的feature1
分支上的commit 3
已经在图中的暂时远离于主分支了。原因很简单。commit 4
和commit 3
都有完全相同的祖先。还记得commits是如何存储前面的commit
的吗?当我们签出master
时,我们返回commit 2
,因为commit 3
只被feature1
分支的指针引用。主分支指针仍然指向commit 2
。因此,我们的hotfix commit(commit 4)
将commit 2
列为前一个commit
。
该图向我们展示了commit 4
和commit 3
都引用commit 2
作为上一个commit
。在这种情况下,我们将commit 2
称为commit 3
和commit 4
的共同祖先。既然我们的热修复程序已投入使用,我们可以返回到我们的feature1
分支并完成工作。
我已经向我们的特色分支提交了两个新提交的commit 5
和commit 6
。我们的feature1
已经完成,现在是时候将我们的feature1
分支合并回master了。此时,我们可以选择将feature1
分支合并为master,或者将feature1
以master
变基。现在,让我们来探讨 Part 2 - What is a merge?。
Part 2 - What is a merge?
在第1部分中,我们留下了一个演示仓库。我们有一个称为feature1
的功能分支,它已经准备好合并回master。
此时,我们可以选择将feature1
合并到master
中,或者选择rebase
。我们将在第3部分中介绍再rebase
。现在让我们看看如果merge
会发生什么。将分支合并在一起是非常直接的。我们首先需要checkout
到要合并到的branch
。因为我们想将feature1
合并到master
中,所以需要切换到master
。
我切到了master
,然后将feature1
合并到其中。让我们来看看到底发生了什么,以及为什么SourceTree
生成的图看起来是这样的。
还记得part 1中的commit 3
和commit 4
是如何拥有共同的 previous commit
吗?commit 2
是这两个提交的共同祖先,因为commit 3
是在另一个分支上生成的,commit 4
是在主分支上生成的,而主分支不知道commit 3
。在我们的feature1
分支中,我们添加了更多的提交。commit 5
直接引用commit 3
,因为commit 4
只在主分支上可用。commmit 6
引用提交commit 5
。
当我们将feature1
合并到master
时,它并没有以某种方式神奇地将这些提交转移到master
分支。它实际上创建了一个全新的commit,其中包含Feature1分支上所有提交的所有更改。表示“merge branch feature1
”的提交如下所示:
如果你一直关注屏幕截图中的提交差异,那么你会看到我添加到index.txt的乱七八糟的文字。您可能已经注意到,这些行都是在对feature1
分支的单独提交中逐个添加的。但是,在这里您可以在单个diff中看到所有这些更改。
Git所做的就是将所有对feature1
的提交的差异都粉碎成一个提交。这个新的commit
已经做了一些我们还没有讨论过的事情。如果你看这个图,你会发现它有两个祖先。它有来自commit 4和commit 6的行。为什么会这样?提交可以存储对多个以前提交的引用。我现在才提出来,因为我不想过早引起混乱。
创建提交时,它可以存储对单个以前commit hash、多个commit hashs 或者不存储任何commit hash。通常只有第一次提交到存储库时没有以前的提交引用,而合并提交通常是存储多个以前提交引用的唯一提交。
如果您还记得第1部分,那么分支实际上只是指向特定提交的指针。
你可能会注意到feature1
仍然指向commit 6
,而master
现在指向新的合并commit。这仅仅是因为我们将feature1
合并为master
。如果我们切到feature1并将master合并到其中,那么Git所要做的就是另一个fast-forward merge
(快进合并),它将使feature1
指针向上指向同一提交。
如果我们现在完全删除我们的Feature1分支,您可能会期望粉色分支线消失,但您错了。
请记住,Source Tree
和任何其他Git GUI
通过遍历提交并使用引用的提交散列将它们连接在一起来生成图形。分支只是指向特定提交的指针。从远程存储库中提取时,Git所做的一切都是:
-
下载本地计算机没有的任何提交。
-
将丢失的提交合并到本地存储库中,可以通过合并提交,也可以通过快进合并(如果自上一次请求以来没有进行任何更改)。
-
将本地分支指针向上移动到最新提交。
如果你曾经对master
和origin/master
感到困惑,现在你知道它们是什么了。origin/master
显示提交到叫做origin
的远程仓库的master
指向的内容。如果我将一个名为origin的远程仓库添加到我们的演示仓库,然后提交到我的本地仓库,那么历史记录将如下所示:
您可以看到master
的图形指向最新的提交,而origin/master
图形指向上一个合并提交。Source Tree
甚至让我们知道有1个提交要推送到远程存储库。如果我们执行push操作,那么git将上传丢失的commit并更新远程分支指针,以显示origin/master
现在指向与本地主分支相同的commit
。
希望您现在能够更好地理解Git中合并的工作原理。跳到part 3 - What is a rebase?,让我们深入研究rebase,看看它与合并的比较。
Part 3 - What is a rebase?
请看下一篇文章
Git:rebase 是什么