「翻译」从里到外搞懂Git

Git from the inside out
git对我来说,依然是一个黑洞啊!!!!

这篇论文旨在解释Git是如何运作的。

我们假设读者已经足够了解Git,并能够使用git来实现对自己项目的版本控制。文章重点讲解了实现Git功能的图形结构(graph structure)以及图形结构如何体现Git的操作内容。从基础入手能够帮助你将自己对git 的理解建立在实现原理上,而非通过对API的反复试错从而获得的对原理的猜想。这样能让你更了解Git做了什么,正在做什么, 会执行什么操作。

创建一个项目

1
2
~ $ mkdir alpha
~ $ cd alpha

用户为自己的项目创建了alpha文件夹

1
2
~/alpha $ mkdir data
~/alpha $ printf 'a' > data/letter.txt

进入到alpha文件夹后,创建了一个叫做data的目录。在data目录下,创建了一个名为letter.txt的文件,它的内容是a。此时alpha目录的路径如下:

1
2
3
alpha
- data
- letter.txt

初始化仓库

1
2
~/alpha $ git init
Initialized empty Git repository

git init将当期路径初始化为一个Git仓库。这一操作会创建一个.git目录,并且对它写入一些文件内容。这些文件定义了关于Git设置和项目历史的所有内容。它们仅仅是普通的文件,并没有什么特别之处。用户可以阅读它们,也可以通过编辑器或shell来编辑它们。也就是说,用户可以轻易地阅读和编辑项目历史,就像编辑其他文件那样。

此时,alpha目录路径如下:

1
2
3
4
5
6
alpha
- data
- letter.txt
- .git
- objects
- etc...

.git目录和它们的内容都属于Git,所有其他的文件都统称为工作副本(working copy)。它们是用户的文件。

添加文件

1
~/alpha $ git add data/letter.txt

当用户对data/letter.txt文件执行git add 操作时,会发生两件事:

第一, 会在.git/objects/路径中新创建一个blob文件

这个blob文件包含了data/letter.txt文件的压缩内容。它的名字来源于对它的内容的hash处理。对一段文本进行hash处理意味着在这段文本上执行一个程序,将它转换成更小片段的文本,同时能够独一无二地识别到原文本。举个例子,Git将ahash处理为2e65efe2a145dda7ee51d1741299f848e5bf752e。开头的两个字符被用作Git对象数据中的路径名:.git/objects/2e。剩余的hash内容作为承载文本内容的blob文件的名字:.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e

由此可以看到添加文件到Git会将文件内容存储到objects路径下。即使用户从工作副本中删除了data/letter.txt文件,文件内容在Git中依然是安全的。

第二,git add将文件添加到索引中。索引(index)是一个包含了所有要求Git跟踪的文件的列表。它被保存在.git/index中。文件的每一行文件都映射到它被添加时的内容的hash值。以下是git add命令执行后的索引:

1
data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e

用户创建了一个叫做data/number.txt的文件,内容是1234

1
~/alpha $ printf '1234' > data/number.txt

工作副本的路径如下:

1
2
3
4
alpha
- data
- letter.txt
- number.txt

用户将文件添加到Git

1
~/alpha $ git add data

git add命令创建了一个包含data/number.txt内容的blob对象。还为data/number.txt添加了一个指向blob文件的索引入口。以下是git add命令第二次执行后的索引:

1
2
data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
data/number.txt 274c0052dd5408f8ae2bc8440029ff67d79bc5c3

尽管用户执行的是git add data,但只有data路径下的文件罗列在索引中,data索引并没有列在其中。

1
2
~/alpha $ printf '1' > data/number.txt
~/alpha $ git add data

当用户最初创建data/number.txt时,他们实际希望文件内容是1,而非1234,于是他们修改了文件的内容,并重新添加到git中。这次操作会创建一个包含新内容的新blob文件,同时在索引中更新data/number.txt文件的入口,使其指向新的blob文件。

提交内容

1
2
~/alpha $ git commit -m 'a1'
[master (root-commit) 774b54a] a1

用户创建了内容为a1的提交。Git会显示关于这次提交的一些信息。这些信息会立马生效。

提交命令实际包含三个步骤:创建一个表示当前版本的提交内容的树形图形;创建一个提交对象;将当前分支指向新的提交对象。

创建树形图形

Git通过索引来创建一个树形图形,从而记录项目的当前状态。这个树形图形会记录项目中每个文件的位置和内容。

这个图形包含两个类型的对象:blobs和trees。

blobs通过git add来创建,它们用来表示文件的内容。

trees是在提交时创建的。一个树表示工作副本中的一个路径。

下面是记录data路径的新提交内容的树对象:

1
2
100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de number.txt

第一行记录了所有用来重新生成data/letter.txt文件的内容。第一部分指文件许可,第二部分表示指向当前内容的是一个blob文件,第三部分是这个blob文件的hash值,第四部分是这个文件的名称。

第二行记录了data/number.txt文件的相关内容。

下面是alpha路径的树对象,即当前项目的根路径的树对象:

1
040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data

这单独的一行将这个树指向了data树。

Tree graph for the `a1` commit

图中,root树指向data树,data树指向表示data/letter.txtdata/number.txt的blob文件。

创建一个提交对象

git commit在创建了树形图形后会创建一个提交对象。提交对象是.git/objects/路径中的另一个文本文件。

1
2
3
4
5
tree ffe298c3ce8bb07326f888907996eaa48d266db4
author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500

a1

第一行指向树形图形,hash值指向表示工作副本中根路径的树对象,也就是alpha路径。最后一行是提交信息。

`a1` commit object pointing at its tree graph

将当期分支指向新提交

最后,提交命令将当期分支指向新的提交对象。哪一个是当前分支呢?Git会到.git/HEAD路径中寻找HEAD文件,并找到:

1
ref: refs/heads/master

这是说HEAD正指向mastermaster是当前分支。

HEADmaster都是引用。引用是Git或用户用来标记一个特定的提交的标签。

代表master引用的文件还不存在,因为这是仓库的第一次提交。Git会在.git/refs/heads/master中创建一个文件,并将内容设置为此次提交对象的hash值:

1
74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd

(如果你正在试着复现以上阅读到的Git操作,你会发现你的a1提交的hash值会和我的不一样。像blob这类内容对象和树对象会总是转换到相同的hash值,但是提交对象不会,因为它们还包含了日期和创建者的名字等信息)

现在让我们添加HEADmaster到Git图形:

`master` pointing at the `a1` commit

HEAD和提交之前一样指向master,但是master现在指向新的提交对象。

再次提交

下面是a1提交之后的Git图形。

`a1` commit shown with the working copy and index

工作副本,索引和a1提交中``data/letter.txtdata/number.txt的内容是相同的。索引和HEAD`的提交都使用hash值来引用blob对象,但是工作副本的内容是用文本的形式存储在其他地方的。

1
~/alpha $ printf '2' > data/number.txt

用户将data/number.txt的内容设为2。这样就更新了工作副本的内容,但是索引和HEAD提交保持不变。

`data/number.txt` set to `2` in the working copy

1
~/alpha $ git add data/number.txt

用户将文件添加到Git中。这时会在objects目录下添加一个内容为2的blob文件,data/number.txt的索引指向新的blob文件。

`data/number.txt` set to `2` in the working copy and index

1
2
~/alpha $ git commit -m 'a2'
[master f07af7e6] a2

用户提交了修改,这一步和之前的提交步骤是一样的。

首先,一个新的树形图形被创建出来,用来表示索引的内容。

data/number.txt的索引入口被修改了。之前的data树不再映射到data目录,新的data树对象必须创建:

1
2
100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 number.txt

新的data树被hash处理为不同于之前的data树的值。新的root树必须被创建,用来记录这个新的hash值:

1
040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data

第二,新的提交对象被创建:

1
2
3
4
5
6
tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556
parent 774b54a193d6cfdd081e581a007d2e11f784b9fe
author Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500

a2

提交对象的第一行执行新的root树对象。第二行执行a1:此次提交的父提交。为了找到父提交,Git找到HEAD,通过HEAD找到master并找到a1提交的hash值。

第三步,master分支的文件内容被修改为新提交的hash值。

`a2` commit

Git graph without the working copy and index

图形解读:内容以对象的形式存储在树上。也就是说只有变动的内容才存储到对象数据库中。上图中可以看到,a2提交复用了a1提交前就创建的ablob文件。类似的,如果整个目录在一次次提交中都未做任何修改,那么它的树和所有的blobs文件以及树下的其他树对象都可以被复用。通常提交过程中都会有一些内容被修改,这意味着Git可以只用很小的空间来存储大量的提交历史。

图形解读:每一次提交都有父提交。这意味着Git仓库可以存储整个项目的历史

图形解读:引用指向一个提交历史中的一部分。也就是说提交可以被赋予有意义的名字。用户可以通过给引用使用有意义的命名,如fix-for-bug-376来安排项目工作。Git提供HEAD,MERGE_HEAD,FETCH_HEAD这类有指向意义的引用来操作提交历史。

图形解读:objects/目录下的节点是不可改变的。也就是说,内容会被修改,不会被删除。每一个被添加的内容和每一次创建的提交都在objects目录中的某个地方。

图形解读:引用是可变的。因此,引用的意义也可以被修改。master分支指向的提交可能是当前项目中最好的版本,但是很快它也可能会被更新和更好的提交所取代。

图形解读:引用指向的工作副本和提交可以读取到,但是其他的提交则不行。这也意味着近期的历史很容易被重现,但是它也会经常更改。

工作副本是历史中最容易复现的,因为它们在仓库的根目录下。复现它们甚至不需要Git命令。它们同时也是历史中保存时间最短的,用户可以创建一个文件的无数个版本,但是Git不会记录它们,除非用户主动添加它们。

HEAD指向的提交也非常容易复现。因为它指向刚刚切换到的分支。要想查看它的内容,用户可以使用stash,并检查工作副本。同时,HEAD也是最经常更改的引用。

一个具体的引用指向的提交也是容易复现的。用户只需要切换到这个分支。这个分支的更改没有HEAD那么频繁。

不被任何引用指向的提交时很难被复现的。用户不再使用一个引用越久,他们理解该引用的提交就越难。但用户不使用的时间越久,其实也不太会有什么新的更改历史。

切换到某次提交

1
2
~/alpha $ git checkout 37888c2
You are in 'detached HEAD' state...

用户通过hash值查看a2提交。

切换包括四个步骤:

第一,Git找到a2提交以及它指向的树形图形

第二,修改树形图形中的文件入口,使其指向工作副本。这不会产生任何变化,工作副本已经有了树形图形的内容,因为HEAD已经通过master指向a2提交

第三,Git在树形图形中将文件入口修改指向索引。这样不会有任何变化,因为已经有了a2提交的内容。

第四,HEAD的内容设置为a2提交的hash值

修改HEAD的内容到某个hash值会让整个仓库处于脱离的HEAD的状态。注意下图中的HEAD直接指向a2提交,而不是指向master

Detached `HEAD` on `a2` commit

1
2
3
4
~/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提交的hash值。

Git更新HEAD直接到a3提交的hash值,仓库依然处于脱离的HEAD状态。提交不再任何一个分支上,因为没有分支指向a3或它的后代。这意味着这些提交很容易丢失。

到目前为止,树和blob会在图形中被忽略。

`a3` commit that is not on a branch

创建一个分支

1
~/alpha $ git branch deputy

用户创建了一个新的deputy分支,这样会在.git/refs/heads/deputy路径创建一个新的文件,文件内容是HEAD指向的提交的hash值,即a3提交的hash值。

图形解读:分支即引用,引用即文件,这意味着Git的分支都是轻量级的。

创建deputy分支让a3提交能够安全地保存在一个分支上,HEAD仍然是脱离的,因为它依然直接指向了一个提交。

`a3` commit now on the `deputy` branch

切换分支

1
2
~/alpha $ git checkout master
Switched to branch 'master'

用户切换到master分支。

首先,Git找到master指向的a2提交以及a2指向的树形图形。

第二步,Git改写树形图形中指向工作副本的文件入口,将data/number.txt的内容设置为2

第三步,Git改写树形图形中指向索引的文件入口,将data/number.txt的入口更新为2blob对应的hash值

第四步,Git通过修改以下内容以将HEAD指向master

1
ref: refs/heads/master

`master` checked out and pointing at the `a2` commit

切换到与工作副本不匹配的分支

1
2
3
4
5
~/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的内容设置为789。当他们想要切换到deputy分支时,Git阻止了这次切换。

HEAD正指向mastermaster指向了a2提交,a2提交中data/number.txt的内容为2deputy指向a3a3提交中data/number.txt的内容为3。工作副本中data/number.txt的当前版本为789。这几个版本都是不一样的,必须先解决这些冲突。

Git可以将工作副本中data/number.txt的内容替换为要切换到的分支的提交中的内容,但它会建立避免数据丢失。

Git也可以合并工作副本的当前版本和切换到的分支的版本,但是这样非常复杂。

所以,Git放弃了这次切换。

1
2
3
~/alpha $ printf '2' > data/number.txt
~/alpha $ git checkout deputy
Switched to branch 'deputy'

用户注意到了他们不小心修改了data/number.txt的内容,并将内容修改会原来的2。然后成功切换到deputy分支。

`deputy` checked out

合并祖先

1
2
~/alpha $ git merge master
Already up-to-date

用户将master合并进deputy。合并两个分支意味着核名两个提交。第一个提交是deputy指向的提交,即合并者。第二次提交是master指向的提交,即被合并者。这次合并中,Git没有做任何事情,所以它提示Already up-to-date

图形截图:图形中的一系列提交可以理解为一些了对仓库内容的修改。也就是说,在合并中,如果被合并者是合并者的祖先,那么Git不会做任何事情,这些修改已经被合并了。

合并后代

1
2
~alpha $ git checkout master
Switched to branch 'master'

用户切换到master

`master` checked out and pointing at the `a2` commit

1
2
~/alpha $ git merge deputy
Fast-forward

用户将deputy合并进master。Git发现合并者的提交a2是被合并者的提交a3的祖先,就会执行一次快进合并。

它找到被合并者的提交和这次提交指向的树形图形,改写树形图形中指向工作副本和索引的文件入口,快进mastera3

`a3` commit from `deputy` fast-forward merged into `master`

图形属性:图中一系列提交可以看做是一些了对仓库内容的修改。也就是说,当合并分支时,如果被合并者是合并者的后代,提交历史就不会被修改,合并者和被合并者直接的一系列提交已经存在。但是,尽管Git的修改历史没有改变,但是Git图形发生了变化。HEAD指向的引用变成了合并者的提交

合并两个不同源提交

1
2
3
4
~/alpha $ printf '4' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a4'
[master 7b7bd9a] a4

用户将number.txt的内容修改为4,并将修改提交给master

1
2
3
4
5
6
~/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` committed to `master`, `b3` committed to `deputy` and `deputy` checked out

图形属性:提交可以共享父提交,也就是说新的源头可以在提交历史中被创建出来。

图形属性:一个提交可以有多个父提交。也就是说不同源的提交可以由一次提交来合并,即一次合并提交

1
2
~/alpha $ git merge master -m 'b4'
Merge made by the 'recursive' strategy

用户将master分支合并进deputy分支

Git发现合并者的b3提交和被合并者的a4提交是不同源的提交。所以需要做一次合并提交,整个过程包括8个步骤:

  1. Git将被合并者的提交的hash值写入到alpha/.git/MERGE_HEAD文件中。这个文件的存在用来告诉Git目前正在合并过程中

  2. Git找到基准提交,即合并者和被合并者共同的最近的一次祖先提交

    `a3`, the base commit of `a4` and `b3`

图形解读:提交都有父提交。也就是说向上追溯可以找到两个源头交汇的地方。Git分别从b3a4向上寻找,直到找到最近的一次共同的父提交a3,那么a3就是基准提交。

  1. Git通过树形图形分别为基准提交,合并者的提交和被合并者的提交创建索引

  2. Git会创建一个对比(diff)来集合合并者和被合并者的提交对基准提交作出的改变。这个对比(diff)就是一个文件路径列表,每一条路径都指向一次修改,包括添加、删除、修改或者冲突

    Git获取到记录了基准提交、合并者提交和被合并者提交的所有文件索引的列表,并逐一对比来确定对文件执行什么修改。Git会为每一条对比写入一个对应的入口,一条对比会有两个入口。

    第一个入口是data/letter.txt,这个文件在基准提交中的内容是a,在合并者的内容是b,在被合并者的内容是a。基准提交和合并者的内容是不同的,但基准提交和被合并者的内容是相同的。Git看到内容被合并者而非被合并者修改,因此data/letter.txt的对比入口被定义为一次修改,而非冲突。

    对比中的第二个内容是data/number.txt。在这个例子中,基准提交和合并者的内容是一样的,但是被合并者的内容是不一样的,data/letter.txt的对比入口也被定义为一次修改。

    图形解读:找到合并的基准提交意味着当合并者或被合并者其中之一基于基准提交修改了文件,Git可以自动合并文件,这就减少了用户需要做的事情。

  3. 对比中的修改入口会被应用到工作副本中。data/letter.txt的内容被改为bdata/number.txt的内容被改为4

  4. 对比中的修改入口会被应用到索引中,data/letter.txt会指向bblob文件,data/number.txt会指向4blob文件

  5. 更新后的索引会被提交

    1
    2
    3
    4
    5
    6
    7
    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

    注意到这次提交有两个父提交

  6. Git指向当前分支deputy的新提交

合并修改了相同文件的不同源提交

1
2
3
4
~/alpha $ git checkout master
Switched to branch 'master'
~/alpha $ git merge deputy
Fast-forward

用户切换到master分支,将deputy分支合并入master分支。这时会将master快进推到b4提交。此时masterHEAD指向同一次提交。

`deputy` merged into `master` to bring `master` up to the latest commit, `b4`

1
2
3
4
5
6
~/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分支。

1
2
3
4
5
6
~/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` commit on `deputy` and `b6` commit on `master`

1
2
3
~/alpha $ git merge deputy
CONFILICT in data/number.txt
Automatic merge failed; fix conficts and commit the result

用户将deputy合并到master分支,此时出现了冲突,合并停止。有冲突的合并过程和没有冲突的合并过程的前6个步骤是相同的:设置.git/MERGE_HEAD,找到基准提交,生成基准提交、合并者提交和被合并者提交的索引,创建对比,更新工作副本,更新索引。由于出现了冲突,第7、8步无法执行,那我们来看看每一步发生了什么:

  1. Git将被合并者提交的hash值写入到.git/MERGE_HEAD

    `MERGE_HEAD` written during merge of `b5` into `b6`

  2. 找到基准提交b4

  3. 生成基准提交、合并者提交和被合并者提交的索引

  4. 生成一个集合了合并者和被合并者提交的基于基准提交的修改对比

    在这个例子中,这个对比只包含一个文件入口:data/number.txt。这个入口被标记为冲突,因为文件内容在基准提交、合并者和被合并者中都不相同

  5. 修改的内容被应用到工作副本中。冲突的部分,Git会将两个版本都写入文件,因此data/number.txt被写为

    1
    2
    3
    4
    5
    <<<<<<< HEAD
    6
    =======
    5
    >>>>>>> deputy
  6. 对比中标记的修改也被应用到索引中,索引中的文件入口标记了文件的路径和阶段。没有冲突的文件标记的阶段为0,在此次合并之前,索引应该如下:

    1
    2
    0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
    0 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb

    此次合并之后,对比写入索引,索引如下:

    1
    2
    3
    4
    0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
    1 data/number.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767
    2 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
    3 data/number.txt 7813681f5b41c028345ca62a2be376bae70b7f61

    data/letter.txt的阶段为0,和合并之前一样。data/number.txt则有3个新的非0的入口出现。阶段为1的文件hash值指向基准提交的data/number.txt的内容,阶段为2的hash值指向合并者的文件内容,阶段为3的hash值指向被合并者的文件内容。这三个入口的存在告诉Gitdata/number.txt目前存在冲突。

    合并停止

    1
    2
    ~/alpha $ printf '11' > data/number.txt
    ~/alpha $ git add data/number.txt

    用户将data/number.txt的内容修改为11,并将文件添加到索引。Git新添加一个内容为11的blob文件,添加一个包含冲突的文件意味着告诉Git冲突已被解决。Git从索引中删除掉data/number.txt的阶段1,2,3的文件,添加data/number.txt阶段为0的入口,现在索引如下:

    1
    2
    0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
    0 data/number.txt 9d607966b721abde8931ddd052181fae905db503
    1. 用户提交。Git看到仓库中的.git/MERGE_HEAD,直到正在合并过程中。于是查看索引,看到并没有冲突,于是新创建一个b11提交,用来记录合并后的内容,并删除.git/MERGE_HEAD中的文件。合并完成。
    2. Git将master指向新的提交

    `b11`, the merge commit resulting from the conflicted, recursive merge of `b5` into `b6`

删除文件

以下Git 图形包含了提交历史、最近一次提交的树形图形和Blob对象,工作副本和索引:

The working copy, index, `b11` commit and its tree graph

1
2
~/alpha $ git rm data/letter.txt
rm 'data/letter.txt'

用户告诉git删除data/letter.txt文件。文件从工作副本中被删除。索引中的文件入口也被删除

After `data/letter.txt` `rm`ed from working copy and index

1
2
~/alpha $ git commit -m '11'
[master d14c7d2] 11

用户提交。作为提交的一个部分,Git创建一个树形图形来记录索引的内容。data/letter.txt不包含在树形吐信中,因为它不再索引中。

`11` commit made after `data/letter.txt` `rm`ed

复制仓库

1
2
~/alpha $ cd ..
~ $ cp -R alpha bravo

用户将alpha/仓库中的内容复制到bravo/路径下。这回创建如下路径结构:

1
2
3
4
5
6
7
~
- alpha
- data
- number.txt
- bravo
- data
- number.txt

bravo路径下会有另一个Git图形

New graph created when `alpha` `cp`ed to `bravo`

连接两个仓库

1
2
			~ $ cd alpha
~/alpha $ git remote add bravo ../bravo

用户切回到alpha路径下,将bravo设置为alpha的远程仓库。这一操作会在alpha/.git/config中添加如下内容:

1
2
[remote "bravo"]
url = ../bravo/

这表明在路径../bravo下有一个名为bravo的远程仓库

从远程仓库获取分支

1
2
3
4
5
~/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,并将修改提交到bravomaster上。

`12` commit on `bravo` repository

1
2
3
4
5
~/bravo $ cd .../alpha
~/alpha $ git fetch bravo master
Unpacking objects: 100%
From ../bravo
* branch master -> FETCH_HEAD

用户进入alpha仓库,他们获取远程bravo仓库的master分支,这个过程包括四个步骤:

  1. Git找到bravo仓库的master分支指向的提交的hash值,即提交12的hash值

  2. Git创建一个新的列表,内容包括12提交依赖的所有内容:提交对象本身,树形图形中的对象,提交12的祖先提交,祖先提交的树形图形对象。alpha对象数据库已有的内容会从整个列表中移除,然后将其余的内容拷贝到alpha/.git/objects中去。

  3. 将引用文件中的alpha/.git/refs/remotes/bravo/master指向提交12

  4. alpha/.git/FETCH_HEAD设置为:

    1
    94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo

    这意味着最近一次的获取命令获取到了bravo仓库的master分支的提交12

    `alpha` after `bravo/master` fetched

    图形解读:对象可以被复制,也就是说提交历史可以被仓库共享

    图形解读:仓库可以储存远程分支的引用,比如alpha/.git/refs/remotes/bravo/master。也就是说一个仓库可以在本地记录远程仓库的分支的状态,它获取到的远程分支的状态在获取的时候是保持同步的,但是远程分支的修改不会自动同步到本地仓库里。

合并FETCH_HEAD

1
2
3
~/alpha $ git merge FETCH_HEAD
Updating d14c7d2..94cd04d
Fast-forward

用户合并了FETCH_HEADFETCH_HEAD只不过另一个引用,它指向被合并者的提交12HEAD指向合并者的提交11。Git执行了一次快进合并,并将master指向提交12

`alpha` after `FETCH_HEAD` merged

从远程仓库拉分支

1
2
~/alpha $ git pull bravo master
Already up-to-date

用户从alpha仓库拉远程bravo仓库的master分支。拉(pull)是获取(fetch)和合并(merge)FETCH_HEAD的简写。Git会执行者两个命令,并显示master分支已经Already up-to-date

克隆一个仓库

1
2
3
~/alpha $ cd ..
~ $ git clone alpha charlie
Cloning into 'charlie'

用户进入上一级路径,将alpha仓库克隆到charlie仓库。克隆到charlie仓库的结果与用户使用cp命令聊创建bravo仓库类似,Git会创建一个新的名为charlie的仓库,初始化charlie为一个Git仓库,添加alpha作为远程仓库,并将远程仓库命名为origin,获取origin并合并FETCH_HEAD

将内容推到远程仓库的分支

1
2
3
4
5
			~ $ 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并提交修改到alphamaster分支

1
~/alpha $ git remote add charlie ../charlie

设置charliealpha仓库的远程仓库

1
2
3
4
5
~/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

用户需要创建一个新的分支,合并提交13再推到推到charlie。但实际上,用户希望有一个能随时推随时拉的仓库,就像GitHub的远程那样,他们需要一个bare仓库。

克隆一个bare仓库

1
2
3
~/alpha $ cd ..
~ $ git clone alpha delta --bare
Cloning into bare repository 'delta'

用户进入到上一级目录,把delta克隆为一个bare仓库。这和普通的克隆仓库有两点不同。config配置文件会表明这个仓库是一个bare仓库。另外,存在.git目录下的文件会存到根目录下:

1
2
3
4
5
delta
- HEAD
- config
- objects
- refs

image-20181202222437115

将分支推到bare仓库

1
2
			~ $ cd alpha
~/alpha $ git remote add delta ../delta

用户回到alpha目录下,将delta设置为alpha的远程仓库

1
2
3
4
~/alpha $ printf '14' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '14'
[master cb51da8] 14

data/number/txt的内容设置为14,并提交到alphamaster分支

![image-20181202222812723](/Users/quanxu/Library/Application Support/typora-user-images/image-20181202222812723.png)

1
2
3
4
~/alpha $ git push delta master
Writing objects: 100%
To ../delta
3238468..cb51da8 master -> master

将内容推到deltamaster分支,总共分三步:

  1. 与提交14相关的所有对象都从alpha/.git/objects/路径拷贝到delta/objects/
  2. delta/refs/heads/master更新到提交14
  3. alpha/.git/refs/remotes/delta/master被设为指向提交14

image-20181202224004269

小结