深入浅出 Git
作者 Mary Rose Cook
本文为 Coding 用户 @z2xy @kari__ @luanmingyi @lsbbd @zealseeker @八哥 协作翻译。如有修改建议,欢迎提交 Pull Request。
这篇文章解释了 Git 是如何工作的。(如果相关内容的谈话更吸引你,你可以观看链接中的 视频。)
本文假设你已经对 Git 理解到了可以对你的项目进行版本控制的程度。本文专注于支撑 Git 的图结构以及这些图的性质影响 Git 行为的方式。通过了解底层,你可以将你心中对 Git 的模型建立在事实之上,而不是基于通过积累使用经验而建立的假设上。这个更真实的模型可以让你更好的理解 Git 做了什么,正在做什么以及将要做什么。
本文由一系列针对单个项目的 Git 命令构成。时不时的,还将有一些对于 Git 所建立的图数据结构的观察。这些观察阐述了图的性质和相应性质所产生的影响。
读完本文后,如果你希望更深入的了解 Git,可以阅读我关于 Git 的 JavaScript 实现 gitlet.js(heavily annotated source code)。
创建项目
~ $ mkdir alpha
~ $ cd alpha
用户为项目建立一个名为 alpha
的目录。
~/alpha $ mkdir data
~/alpha $ printf 'a' > data/letter.txt
进入目录 alpha
,并在下面建立名为 data
的目录。在这个目录中建立一个名为 letter.txt
的文件,其中包含一个字符 a
。此时目录结构看起来像这样:
alpha
└── data
└── letter.txt
初始化版本库
~/alpha $ git init
Initialized empty Git repository
git init
命令使得当前目录成为一个 Git 版本库。为此这条命令建立了一个名为的 .git
目录并且向其中写入了一些文件。这些文件定义和记录了关于Git配置和项目历史的所有相关内容。它们只是普通的文件,其中并没有什么类似魔法的神奇之处。用户可以使用文本编辑器和命令行阅读或编辑这些文件。也就是说:用户可以像获取和修改项目文件一样简单地获取和修改项目历史。
这时候目录 alpha
的结构看起来像这样:
alpha
├── data
| └── letter.txt
└── .git
├── objects
etc...
.git
目录和其中的内容属于 Git。所有其他的文件一起被称为工作副本,属于用户。
添加一些文件
~/alpha $ git add data/letter.txt
用户对文件 data/letter.txt
运行命令 git add
。这一操作产生两个效果。
首先,这一操作在目录 .git/objects/
新建一个BLOB(binary large object 二进制大对象)文件。
这个BLOB文件包含了文件 data/letter.txt
压缩过的内容。文件名由内容的散列值得到。散列一个文本片段,意味着对其内容运行一个程序将其转变为一段更短小[1]并且唯一地[2]代表原先文本的文本片段。例如,Git 将 a
散列为 2e65efe2a145dda7ee51d1741299f848e5bf752e
。最前面的两个字符被用于对象数据库中目录的命名:.git/objects/2e/
。散列值的其余部分被用于包含添加文件内容的BLOB文件的命名:.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e
。
注意到将一个文件添加到 Git 中时 Git 是如何将文件的内容保存到 objects
目录中的。即便用户从工作副本中删除文件 data/letter.txt
,它的内容在 Git 库中也是安全的。
其次,git add
命令将文件添加到索引中。索引是一个包含所有 Git 所要跟踪文件的列表。它以文件 .git/index
保存。这个文件每一行建立起一个被跟踪文件与这个文件被添加时散列值的对应关系。这是在 git add
命令被执行之后的索引文件:
data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
用户建立一个叫做 data/number.txt
的文件,内容为 1234
。
~/alpha $ printf '1234' > data/number.txt
工作副本看起来像这样:
alpha
└── data
└── letter.txt
└── number.txt
用户将这个文件添加到 Git 中。
~/alpha $ git add data
命令 git add
创建一个包含 data/number.txt
内容的BLOB文件。同时添加一条文件 data/number.txt
的索引项,指向对应的BLOB文件。在 git add
命令第二次被执行之后索引文件如下:
data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
data/number.txt 274c0052dd5408f8ae2bc8440029ff67d79bc5c3
注意到即便用户运行 git add data
,也只有 data
目录中的文件在索引文件中被列出。文件夹 data
没有被单独列出。
~/alpha $ printf '1' > data/number.txt
~/alpha $ git add data
当用户最初建立 data/number.txt
时,他想要写入 1
,而不是 1234
。用户做了更改并且将文件添加到索引中。这次的命令创建了一个新的包含了新内容的BLOB文件。并且更新了文件 data/number.txt
的索引项指向新的BLOB文件。
进行一次提交
~/alpha $ git commit -m 'a1'
[master (root-commit) 774b54a] a1
用户做提交 a1
。Git 显示出一些有关此次提交的数据。很快我们将看懂这些信息。
提交命令分三步执行。首先,命令建立了一个树图来表示被提交的项目版本的内容。其次,建立一个提交对象。最后,将当前分支指向新的提交对象。
创建一个树图
Git 通过从索引建立一张树图来记录项目的当前状态。这张树图记录了项目中每一个文件的位置和内容。
图由两种对象组成:BLOB 文件和树。
BLOB 文件由 git add
存储。它们表示了文件的内容。
树是在提交被进行时被存储的。树表示了工作副本中的目录。
下面就是记录新提交中 data
目录内容的树对象:
100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de number.txt
第一行记录了再现 data/letter.txt
文件所需的一切内容。第一部分表明文件权限。第二部分表明此项内容由BLOB文件表示而不是一个树对象。第三个部分表明对应BLOB的散列值。第四部分表明文件名。
第二行记录了关于 data/number.txt
文件的相同内容。
下面是代表 alpha
的树对象,即项目的根目录:
040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data
树中只有一行并指向 data
树。
a1
提交的树图
在上面的图中,root
树指向 data
树。data
树指向 data/letter.txt
和 data/number.txt
对应的BLOB文件。
创建一个提交对象
git commit
在创建树图之后创建一个提交对象。提交对象是 .git/objects/
目录中另一个文本文件:
tree ffe298c3ce8bb07326f888907996eaa48d266db4
author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
a1
第一行指向树图。散列值对应于代表工作副本根目录的树图,这里是 alpha
目录。最后一行是提交信息。
a1
提交对象指向它的树图
将当前分支指向新提交
最后,提交命令将当前分支指向新的提交对象。
哪一个分支是当前分支?Git 在 .git/HEAD
目录中的 HEAD
文件中寻找相关信息:
ref: refs/heads/master
这代表 HEAD
指向 master
。master
分支是当前分支。
HEAD
和 master
都是引用。引用是 Git 或用户用来标识特定分支的标签。
代表 master
分支的引用并不存在,因为这是版本库的第一次提交。Git 在路径 .git/refs/heads/master
创建文件并且将内容写为提交对象的散列值:
74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd
(如果你在 Git 中输入你所读到的命令,a1
提交的散列值将与我这里的不同。内容对象例如 BLOB 文件和树总是散列到与本文相同的值上。提交对象并不如此,因为其中包含日期和创建者的名字。)
让我们将 HEAD
和 master
添加到 Git 图中:
HEAD
指向 master
并且 master
指向 a1
提交
HEAD
指向 master
,如提交之前一样。但是现在 master
开始存在并且指向新的提交对象。
创建一个非首次提交
下面是 a1
提交之后的 Git 图。其中包括了工作拷贝和索引。
包含工作拷贝和索引的
a1
提交
注意,工作拷贝,索引以及 a1
提交中的 data/letter.txt
和 data/number.txt
文件内容是一样的。索引和 HEAD
提交的散列值都指向的都是 BLOB 对象,但是工作拷贝的内容是作文文本文件存放在不同的地方的。
~/alpha $ printf '2' > data/number.txt
用户将 data/number.txt
的内容设置为 2
。该操作更新了工作拷贝,但是没有改变 HEAD
提交以及索引。
工作拷贝中的
data/number.txt
设置为 2
~/alpha $ git add data/number.txt
用户将文件添加到 Git。该操作在 object
目录中创建了一个内容为 2
的 BLOB 文件。在新的 BLOB 文件中添加了一条指向 data/number.txt
的索引项。
工作拷贝和索引中的
data/number.txt
设置为 2
~/alpha $ git commit -m 'a2'
[master f0af7e6] a2
用户提交。步骤同上。
第一步,创建了一个代表索引内容的树图。
data/number
的索引项发生了改变。旧的 data
树不能再反映当前的 data
目录的索引状态。所以必须创建一个一个新的 data
树:
100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 number.txt
新的 data
树与旧的 data
树散列值不同。必须创建一个新的 root
树来记录当前散列值:
040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data
第二步,创建一个新的提交对象。
tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556
parent 774b54a193d6cfdd081e581a007d2e11f784b9fe
author Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
a2
提交对象的第一行指向了新的 tree
对象。第二行指向 a1
提交:当前提交的父提交。要找到父提交,Git 首先找到头指针 HEAD
,然后顺着找到 master
然后获得 a1
提交的散列值。
第三步,将 master
分支文件的内容设置为新提交的散列值。
a2
提交
不包括工作拷贝和索引的 Git 图
不包括工作拷贝和索引的 Git 图
图属性: 内容被储存为一个树对象。这表明只有差异被储存在对象数据库中。从上图可以看出。a2
提交重复使用了在 a1
提交之前创建的 a
BLOB 文件。类似的,如果整个工作目录的内容在一次次提交中没有发生改变,树对象以及所有的 BLOB 文件都能够被重复使用。通常来说,各次提交之间只有很小的改动。这也就意味着 Git 能够使用很小的空间来储存大量的提交历史。
图属性:每个提交都有一个父提交,也就是说仓库可以储存项目的历史改动。
图属性:引用(refrences 或者 refs,译者注)是指向一部分提交历史或者其他的条目。也就是说可以给提交取一个有意义的名字。用户将它们的工作组织成单行的固定短语,比如 fix-for-bug-376
。Git 使用了一些像 HEAD
,MERGE_HEAD
和 FETCH_HEAD
的符号引用来支持一些用来处理提交历史的命令。
图属性:objects/
目录中的节点都是不可变的。也就是说其中的内容能编辑但是不能删除。你添加到仓库中的所有内容以及你所做的每一个提交都存放在 objects
目录[^3]中的某个地方.
图属性:引用是可变的。因此,可以改变一个引用的意义。master
所指向的提交可能是当前项目的最好的版本,但是过段时间,它将会被一个更新的或者更好的提交所取代。
图属性:由引用所指向的工作拷贝以及提交很容易获取,但是获取其他的引用就不那么简单了。也就是说调出最近的提交历史更加容易,但是那也会时常会改变。
工作拷贝是最容易在历史提交中调出的,因为它是仓库的根节点。调出它甚至不需要执行 Git 命令。同时它也是提交历史中的最早的永久节点。用户可以创建一个文件的许多版本,但是如果没有对它们执行 add
操作的话,Git 将不会记录它们。
头指针 HEAD
所指向的提交很容易被调出。它在检出分支的顶端。要查看其中的内容,用户只需执行 stash[^4] 然后检出工作拷贝。同时,HEAD
改变频率最高的引用。
有固定引用指向的提交很容易被调出。用户可以轻易的检出那个分支。分支的顶端通常没有 HEAD
的改变频率高,but often enough for the meaning of a branch name to be changeable.
要调出没有被任何引用所指向的提交很困难。用户在某个引用上提交得越多,操作之前的提交就越不容易。但我们通常很少操作很久之前的提交[^5]。
检出一个提交
~/alpha $ git checkout 37888c2
You are in 'detached HEAD' state...
用户通过对应的散列值来检出 a2
提交。(你过你照搬了以上的 Git 命令,在你的电脑上不会起作用。请使用 git log
命令来查找 a2
提交的散列值。)
检出有以下 4 个步骤。
第一步,Git 获得 a2
提交以及其指向的树图。
工作区的内容已经和树图保持一致了,因为我们的HEAD之前就已经通过master指向a2提交了。
第二步,将树图中的文件写入工作拷贝中。这不会产生什么变化。工作拷贝的内容已经和树图中的保持一致了,因为头指针 HEAD
早已经通过 master
指向了 a2
提交。
第三步,Git 将树图中的文件写入索引。同样,这也不会产生什么变化,index 早就有 a2
提交的内容了。
第四步,头指针 HEAD
的内容被设置为 a2
提交的散列值:
f0af7e62679e144bb28c627ee3e8f7bdb235eee9
对 HEAD
写入一个散列值会导致仓库进入头指针分离状态。注意下图中的 HEAD
直接指向了 a2
提交,而不是指向 master
。
分离头指针到
a2
提交
~/alpha $ printf '3' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a3'
[detached HEAD 3645a0e] a3
用户将 data/number.txt
的值设置为 3
然后提交更改。Git 找到 HEAD
然后找到 a3
提交的父提交。这回返回 a2
提交的散列值而不是查找一个分支引用。
Git 将 HEAD
更新,使其直接指向 a3
提交的哈希值。此时仓库仍然处于头指针分离状态,而没有在一个分支上,因为没有引用指向 a3
提交亦或是它之后的提交。这意味着它很容易丢失。
从现在起,图示中大多的树和 BLOB 都会省略。
没有在分支上的 `a3` 提交没有在分支上的
a3
提交
创建一个分支
~/alpha $ git branch deputy
用户创建了一个新的叫做 deputy
的分支。这个操作在 .git/refs/heads/deputy
目录下创建了新文件,其包含了 HEAD
指向的散列值:a3
提交的散列值。
图属性:分支其实就是引用,而引用其实就是文件。这也就是说 Git 的分支是很轻量的。
创建 deputy
分支的操作实际上将新的 a3
提交安全地放在了一个新的分支上。HEAD
指针目前仍处于分离状态,因为它现在仍是直接指向了一个提交。
处于
deputy
分支的 a3
提交
检出分支
~/alpha $ git checkout master
Switched to branch 'master'
---
检出'master'分支
用户检出了'master'分支
首先, Git 找到'master'分支所指向的a2
提交对象并获取该提交对象所指向的树对象.
接下来 Git 会将树对象储存的文件写到当前工作副本中, 该操作将覆写data/number.txt
为2
.
第三步, Git 将树对象中的文件入口写入 index, data/number.txt
的文件入口将会被更新为2
blob 的 hash 值
最后 Git 通过将HEAD
中的 hash 值替换为如下内容来使HEAD
指向master
分支:
ref: refs/heads/master
`master`分支被检出, 指向'a2'提交
master
分支被检出, 指向'a2'提交
检出与当前工作副本相冲突的分支
~/alpha $ printf '789' > data/number.txt
~/alpha $ git checkout deputy
Your changes to these files would be overwritten
by checkout:
data/number.txt
Commit your changes or stash them before you
switch branches.
---
对以下文件做出的更改将在检出中被覆盖:
data/number.txt
请在检出前将这些更改提交或储藏.
用户无意中将data/number.txt
的内容更改为789
, 此时他尝试检出deputy
分支, Git 没有执行这次检出.
当前HEAD
指向 master
分支, 其所指向提交a2
中data/number.txt
的内容为2
. deputy
分支指向的提交a3
中data/number.txt
的内容为3
. 当前工作副本中data/number.txt
的内容为789
. 这个文件的各个版本各不相同, 必须通过一种方法来消除这些差异.
如果 Git 将当前版本的data/number.txt
替换成将要检出的分支中的版本则会造成数据遗失, Git 不允许这种情况发生.
如果 Git 将要检出的分支与当前工作副本合并则会导致冲突
因此 Git 中断了此次检出.
~/alpha $ printf '2' > data/number.txt
~/alpha $ git checkout deputy
Switched to branch 'deputy'
用户注意到了对data/number.txt
的意外操作, 在把它的内容修改回2
后成功检出了deputy
分支.
deputy
分支被检出
合并一个祖先分支
~/alpha $ git merge master
Already up-to-date.
用户将master
分支合并到deputy
中. 合并两个分支其本质为合并两个提交对象. 其一-作为接受者(receiver)-是deputy
所指向的提交对象, 其二-作为给予者(giver)-是master
所指向的提交对象. 在这次合并中 Git 不会执行任何操作, 而仅仅打印出Already up-to-date.
.
[图属性]: 图示中的一系列提交对象可以被认为是对仓库内容进行的一些列更改. 因此在一次合并当中, 若想要并入的分支是当前分支的祖先分支, Git不会执行任何操作, 因为想要并入分支中的改动已经被包含在了当前分支中.
合并后代提交
~/alpha $ git checkout master
Switched to branch 'master'
用户检出了 master
分支.
master
被检出并指向了 a2
提交
~/alpha $ git merge deputy
Fast-forward
他们合并 deputy
分支到 master
。Git 发现赠予提交的 a2
是源提交 a3
的祖先提交。这里可以执行 fast-forward 合并。
Git 获取赠予提交和它指向的树图,将树图中的文件写入工作区和 index。这将 master
分支 fast-forward
到了 a3
提交。
deputy
分支的 a3
提交 fast-forward 合并到了 master
分支
图属性:图中的提交系列被视为对仓库内容的一系列更改。这意味着,如果赠予提交是接收提交的后代提交,提交历史不会变。已经存在一序列提交来描述赠予提交和接收提交之间的变化。但是尽管 Git 历史不变,Git 的状态图是会改变的。HEAD
指向的具体引用会更新以指向赠予提交。
合并不同提交线的两个提交
~/alpha $ printf '4' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a4'
[master 7b7bd9a] a4
用户将文件 number.txt
的内容改为 4
然后提交到 master
.
~/alpha $ git checkout deputy
Switched to branch 'deputy'
~/alpha $ printf 'b' > data/letter.txt
~/alpha $ git add data/letter.txt
~/alpha $ git commit -m 'b3'
[deputy 982dffb] b3
用户检出 deputy
分支,将文件 data/letter.txt
内容改为 b
然后提交到 deputy
分支。
a4
提交到 master
, b3
提交到 deputy
, deputy
被检出
图属性: 多个提交可以共用一个父提交,这意味着新提交线可以在提交历史里创建出来。
图属性: 某提交可以有多个父提交,这意味着两个不同的提交线可以被一个合并提交来合并。
~/alpha $ git merge master -m 'b4'
Merge made by the 'recursive' strategy.
用户合并 master
到 deputy
.
Git 发现接收提交 b3
和赠予提交 a4
在不同的提交线上。它创建了一个合并提交。这个过程总共分八步。
第一步,Git 将接收提交的哈希值写入文件 alpha/.git/MERGE_HEAD
。此文件的存在说明 Git 正在做合并操作。
第二步,Git 查找基提交:即接收提交和赠予提交共有的一个最近父提交。
`a3`, the base commit of `a4` and `b3`a3
是 a4
和 b3
的基提交
图属性:每个提交都有一个父提交。这意味着我们可以发现两个提交线分开自哪个提交。Git 查找 b3
和 a4
的所有祖先提交,发现了最近的公共父提交 a3
,即为他们的基提交。
第三步,Git 为基提交、接收提交和赠予提交创建索引。
第四步,Git 产生接收提交和赠予提交相对于基提交的 diff,此处的 diff 是一个文件路径列表,指向一个变化:包括添加、移除、修改或冲突。
Git 获取基提交、接收提交和赠予提交的文件列表,针对每一个文件,通过对比 index 来判断它的状态与写入变更。它将对应条目写入 diff。在这个例子中,diff 包含两个条目。
第一项记录 data/letter.txt
的状态。该文件内容分别是基提交中的 a
、接收提交中的b
和赠予提交中的a
。文件内容在基提交和接收提交不同,但在基提交和赠予提交相同。Git 发现文件内容被接收提交修改了,而不是在赠予提交中。data/letter.txt
的状态是修改,而不是冲突。
第二项记录 data/number.txt
的变更。在这个例子中,该文件内容在基提交和接收提交中是相同的,但在基提交和赠予提交是不同的。 data/number.txt
条目的状态也是修改。
图属性:查找一个合并操作的基提交是可行的。这意味着,如果基提交中的一个文件只在接收提交或赠予提交做了修改,Git 可以自动除了合并文件,这样就减少了用户的工作量。
第五步,Git 将差异中的项被应用到工作区。data/letter.txt
内容被修改为b
,data/number.txt
内容被修改为 4
。
第六步,Git 将差异中的项应用到 index。data/letter.txt
会指向内容为 b
的 blob,data/number.txt
会指向内容为 4
的 blob。
第七步,更新后的 index 被提交:
tree 20294508aea3fb6f05fcc49adaecc2e6d60f7e7d
parent 982dffb20f8d6a25a8554cc8d765fb9f3ff1333b
parent 7b7bd9a5253f47360d5787095afc5ba56591bfe7
author Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
b4
注意,这个提交有两个父提交。
第八步,Git 将当前分支 deputy
指向新提交。
a4
递归合并入 b3
产生 b4
合并不同提交线且有相同修改文件的两个提交
~/alpha $ git checkout master
Switched to branch 'master'
~/alpha $ git merge deputy
Fast-forward
用户检出 master
,他们将 deputy
合并到 master
。此操作将master
fast-forwards 指向 b4
。master
和 deputy
现指向了相同的提交。
deputy
合并到 master
将 master
更新到新提交 b4
~/alpha $ git checkout deputy
Switched to branch 'deputy'
~/alpha $ printf '5' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b5'
[deputy bd797c2] b5
用户检出 deputy
。将 data/number.txt
内容修改为 5
并提交到 deputy
分支。
~/alpha $ git checkout master
Switched to branch 'master'
~/alpha $ printf '6' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b6'
[master 4c3ce18] b6
用户检出 master
。将 data/number.txt
内容修改为 6
并提交到 master
分支。
b5
提交在 deputy
b6
提交在 master
~/alpha $ git merge deputy
CONFLICT in data/number.txt
Automatic merge failed; fix conflicts and
commit the result.
用户将 deputy
合并到 master
。这里存在冲突故合并中止。对于有冲突的合并操作,执行步骤和没有冲突的合并的前六步是相同的:设置 .git/MERGE_HEAD
,查找基提交,创建基提交、接收提交和赠予提交的索引,生成 diff,更新工作区,更新 index。由于冲突,第七步提交和第八步更新 ref 不再执行。让我们再来看看这些步骤,观察到底发生了什么。
第一步,Git 将赠予提交的哈希值写入 .git/MERGE_HEAD
.
MERGE_HEAD
写入在 b5
合并入 b6
第二步,Git 查找到基提交, b4
.
第三步,Git 创建基提交、接收提交和赠予提交的索引。
第四步,Git 生成集合了接收提交和赠予提交相对于基提交的差异列表,这个 diff 是一份指向变更的文件路径:添加、删除、修改或冲突。
在本例中,差异列表仅包含一项: data/number.txt
。它的状态被标为冲突因为其内容在接收提交,赠予提交和基提交中都是变化的。
第五步,差异列表中的文件被写入工作区。对于冲突的部分,Git 将两个版本都写入工作区的文件中。data/number.txt
的内容被变更为:
<<<<<<< HEAD
6
=======
5
>>>>>>> deputy
第六步,差异列表中的文件被写入 index。index 中的项被文件路径和 stage 的组合唯一标识。没有冲突的项 stage 为 0
。在该合并前,index 看起来像下面的样子,标有 0
的是 stage 值:
0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
在合并 diff 写入 index 后,index 变成:
0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
1 data/number.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767
2 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
3 data/number.txt 7813681f5b41c028345ca62a2be376bae70b7f61
stage 0
的 data/letter.txt
项和合并前是一样的。stage 0
的 data/number.txt
项已经不存在,取代的是三个新项。stage 1
的项包含该文件在基提交中内容的哈希值,stage 2
包含接收提交的哈希值,stage 3
包含赠予提交的哈希值。这三项表明文件 data/number.txt
存在冲突。
合并中止了。
~/alpha $ printf '11' > data/number.txt
~/alpha $ git add data/number.txt
用户通过将 data/number.txt
的内容修改为 11
将两个有冲突的文件合并,将文件添加到 index,Git 创建一个包含11
的 blob,创建一个冲突文件以告诉 Git 冲突已经解决了。Git 移除 index 中的 1
, 2
和 3
,并添加 stage 为 0
的 data/number.txt
项,该项指向新创建 blob 的哈希值。现在 index 变为:
0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/number.txt 9d607966b721abde8931ddd052181fae905db503
~/alpha $ git commit -m 'b11'
[master 251a513] b11
第七步,用户进行提交。Git 发现存在 .git/MERGE_HEAD
,意味着合并还在进行中。通过检查 index 发现没有冲突。它创建了一个新提交 b11
,用来记录合并后的内容。然后删除 .git/MERGE_HEAD
。合并完成。
第八步,Git 将当前分支 master 指向新提交。
`b11`, the merge commit resulting from the conflicted, recursive merge of `b5` into `b6`b5
和 b6
递归合并为b11
删除文件
这幅 Git 示意图包含对于最后一次提交的历史提交、树对象、储存对象、工作副本以及索引。
The working copy, index, `b11` commit and its tree graph工作副本、索引,
b11
提交以及它的树对象。
~/alpha $ git rm data/letter.txt
rm 'data/letter.txt'
该用户告诉 Git 删除 data/letter.txt
。 文件将从工作副本中删除,该文件的入口也将从索引中消失。
After
data/letter.txt
rm
ed from working copy and indexdata/letter.txt
从工作副本以及索引中 rm
过后
~/alpha $ git commit -m '11'
[master d14c7d2] 11
该用户进行提交了。 一般说来,Git 会在提交时构建一张树图代表索引内容,data/letter.txt
并不会出现在树中,因为它不在索引里。
data/letter.txt
删除后进行 11
提交
复制一个库
~/alpha $ cd ..
~ $ cp -R alpha bravo
用户复制 alpha/
库中的内容到 bravo/
目录中,使得目录结构如下:
~
├── alpha
| └── data
| └── number.txt
└── bravo
└── data
└── number.txt
bravo
目录即生成另一张 Git 图:
当
alpha
cp
至 bravo
后产生的新图
链接至另一个库
~ $ cd alpha
~/alpha $ git remote add bravo ../bravo
用户回到 alpha
库中,设置 bravo
为一个 alpha
的远程库,这会使 alpha/.git/config
多了这几行:
[remote "bravo"]
url = ../bravo/
这几行说明有个叫 bravo
的远程库在 ../bravo
中。
获取远程分支
~/alpha $ cd ../bravo
~/bravo $ printf '12' > data/number.txt
~/bravo $ git add data/number.txt
~/bravo $ git commit -m '12'
[master 94cd04d] 12
进入 bravo
目录. 覆写 文件 data/number.txt
为 12
,将改动提交到bravo
仓库的分支 master
.
bravo
仓库中的一个提交12
~/bravo $ cd ../alpha
~/alpha $ git fetch bravo master
Unpacking objects: 100%
From ../bravo
* branch master -> FETCH_HEAD
进入 alpha
目录,从 bravo
仓库获取 master
分支到 alpha
仓库到 master
分支。这个过程包含了四个步骤。
第一步,Git 获取 bravo
仓库中 master
分支所指向提交的 hash 值,即 提交 12
所对应的 hash 值。
第二步,Git 会给做出 12
这个提交依赖的所有对象组成的一个列表:包含了提交对象自身,树图中的对象,提交 12
所对应的父提交,父提交树图对象。然后从列表中移除所有在 alpha 仓库对象数据库中已有的对象。复制剩下的对象到目录 alpha/.git/objects/
。
第三步,将 12
这个提交的 hash 值 写入文件 alpha/.git/refs/remotes/bravo/master
。
第四步,文件 alpha/.git/FETCH_HEAD
内容被置为:
94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo
文件内容表明从 bravo
仓库 master
分支获取了 12
这个提交。
从 bravo/master
获取后的alpha
仓库状态
图属性: 对象可以被复制。以为址不同仓库间可以共享对象。
图属性:一个仓库可以存储远程分支的引用,例如 alpha/.git/refs/remotes/bravo/master
。意味着,仓库可以在本地记录远程分支的状态。如果远程分支没有改变,这个状态就一直是正确的。
合并分支 FETCH_HEAD
~/alpha $ git merge FETCH_HEAD
Updating d14c7d2..94cd04d
Fast-forward
用户合并了分支FETCH_HEAD
,这个分支也是一个指向某个提交的一个引用,被解析为指向 12
这个提交的一个引用。
合并前,HEAD
指向此次被合并的引用, 11
这个提交。 完成 fast-forward
合并后, HEAD
指向提交 12
。
alpha
仓库合并FETCH_HEAD
后的状态
拉取远程仓库
~/alpha $ git pull bravo master
Already up-to-date.
拉取远程仓库 bravo
的 master
分支到本地仓库 alpha
。pull
操作是 "获取然后合并FETCH_HEAD
" 这两个命令的一个快捷方式。执行了这个命令后,反馈 Already up-to-date
, 说明本地和远程内容一样。
克隆一个仓库
~/alpha $ cd ..
~ $ git clone alpha charlie
Cloning into 'charlie'
切换到 alpha
的上级目录,克隆仓库alpha
到目录 charlie
,这个操作的结果,如同通过复制到到bravo
仓库。
克隆仓库做的事情包括:创建新目录charlie
; 在目录charlie
下初始化仓库;将 alpha
作为远程仓库的 origin
分支; 获取 origin
分支到本地;合并分支 FETCH_HEAD
。
推送本地分支到远程仓库
~ $ cd alpha
~/alpha $ printf '13' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '13'
[master 3238468] 13
切换回 alpha
仓库, 覆写文件 data/number.txt
为 13
,将改动提交到alpha
仓库的分支 master
。
~/alpha $ git remote add charlie ../charlie
将本地目录charlie
作为本地仓库的alpha
的一个远程仓库。
~/alpha $ git push charlie master
Writing objects: 100%
remote error: refusing to update checked out
branch: refs/heads/master because it will make
the index and work tree inconsistent
推送本地仓库的master
分支到远程仓库 charlie
。
13
这个提交所需要的所有对象,被复制到到远程仓库charlie
中。
至此,推送操作完成。和之前一样,如果操作出错,Git 会提示出错内容。例如,Git 会拒绝推送到一个在远程切出的分支。听起来是不是很有道理 ?由于一次推送操作会更新远程索引和 HEAD
的指向,如果这个时候有人正在编辑远程副本的时候,就会导致冲突,出现不一致。
现在,可以创建一个新分支,合并 13
这个内容到新分支 ,推送到远程仓库 charlie
。但是,希望达到的是可以推送任何想推送的内容到仓库,希望又有一个可以推送和拉取到中心仓库,但是没人能直接推送到此中心仓库。有点像 GitHub 远程操作,最后的解决方案就是,裸库。
克隆一个裸库
~/alpha $ cd ..
~ $ git clone alpha delta --bare
Cloning into bare repository 'delta'
切换到 alpha
上级目录,将裸库到目录 delta
。这个克隆有两点不同。一是config
文件表明这是一个裸库;二是,通常位于 .git
目录下的文件被现在存放在仓库的根目录下:
delta
├── HEAD
├── config
├── objects
└── refs
`alpha` and `delta` graphs after `alpha` cloned to `delta`
克隆alpha
到 delta
后的仓库图
推送分支到裸库
~ $ cd alpha
~/alpha $ git remote add delta ../delta
切换到 alpha
目录。使用 ../delta
目录 创建远程仓库delta
。
~/alpha $ printf '14' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '14'
[master cb51da8] 14
覆写文件 data/number.txt
为 14
,将改动提交到alpha
仓库的分支 master
。
alpha
仓库中的14
提交
~/alpha $ git push delta master
Writing objects: 100%
To ../delta
3238468..cb51da8 master -> master
推送master
到仓库delat
。推送过程包含三步:
第一步,14
提交需要的所有对象,从alpha/.git/objects/
目录,复制到目录delta/objects/
。
第二步,更新文件delta/refs/heads/master
内容,指向提交 14
。
第三步,更新文件alpha/.git/refs/remotes/delta/master
内容,指向提交 14
。本地仓库alpha
就有了远程仓库delta
的一份最新状态记录。
推送alpha
仓库到提交14
到仓库delta
总结
Git 是在基于图的思想上构建的,几乎所有的 Git 命令都在维护这个图。想要深入理解 Git,就需要把精力集中在这个图的属性上,而不是在 Git 操作流程 或者 Git 命令。
想要更多的理解 Git,就去详细剖析 .git
目录,看里面都有些什么文件。通过改变文件内容,观察里面这些文件的变化。手动创建提交,看看可以把这个仓库搞什么鬼样子,然后尝试修复这些问题。
- 通过这个案例,hash 值比原始文件内容要长。但是,所有文件的内容比 hash 值要长,这样的效果就是,表达的意思比原始文件要更加简洁明了。
- 有可能会出现两个不同的文件内容的 hash 值一样,但是,这个 机率很小.。
-
git prune
会删除一个引用不能获取的所有对象。如果使用此命令,可能导致文件内容丢失。 -
git stash
会在一个安全的地方存储HEAD
与当前工作区的差异。因此稍后可以恢复工作区。 -
rebase
可以对历史命令进行操作,达到新增,修改,删除提交的目的。