Git from the inside out
git对我来说,依然是一个黑洞啊!!!!
这篇论文旨在解释Git是如何运作的。
我们假设读者已经足够了解Git,并能够使用git来实现对自己项目的版本控制。文章重点讲解了实现Git功能的图形结构(graph structure)以及图形结构如何体现Git的操作内容。从基础入手能够帮助你将自己对git 的理解建立在实现原理上,而非通过对API的反复试错从而获得的对原理的猜想。这样能让你更了解Git做了什么,正在做什么, 会执行什么操作。
创建一个项目
1 | ~ $ mkdir alpha |
用户为自己的项目创建了alpha
文件夹
1 | ~/alpha $ mkdir data |
进入到alpha
文件夹后,创建了一个叫做data
的目录。在data
目录下,创建了一个名为letter.txt
的文件,它的内容是a
。此时alpha
目录的路径如下:
1 | alpha |
初始化仓库
1 | ~/alpha $ git init |
git init
将当期路径初始化为一个Git仓库。这一操作会创建一个.git
目录,并且对它写入一些文件内容。这些文件定义了关于Git设置和项目历史的所有内容。它们仅仅是普通的文件,并没有什么特别之处。用户可以阅读它们,也可以通过编辑器或shell来编辑它们。也就是说,用户可以轻易地阅读和编辑项目历史,就像编辑其他文件那样。
此时,alpha
目录路径如下:
1 | alpha |
.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将a
hash处理为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 | alpha |
用户将文件添加到Git
1 | ~/alpha $ git add data |
git add
命令创建了一个包含data/number.txt
内容的blob对象。还为data/number.txt
添加了一个指向blob文件的索引入口。以下是git add
命令第二次执行后的索引:
1 | data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e |
尽管用户执行的是git add data
,但只有data
路径下的文件罗列在索引中,data
索引并没有列在其中。
1 | ~/alpha $ printf '1' > data/number.txt |
当用户最初创建data/number.txt
时,他们实际希望文件内容是1
,而非1234
,于是他们修改了文件的内容,并重新添加到git中。这次操作会创建一个包含新内容的新blob文件,同时在索引中更新data/number.txt
文件的入口,使其指向新的blob文件。
提交内容
1 | ~/alpha $ git commit -m 'a1' |
用户创建了内容为a1
的提交。Git会显示关于这次提交的一些信息。这些信息会立马生效。
提交命令实际包含三个步骤:创建一个表示当前版本的提交内容的树形图形;创建一个提交对象;将当前分支指向新的提交对象。
创建树形图形
Git通过索引来创建一个树形图形,从而记录项目的当前状态。这个树形图形会记录项目中每个文件的位置和内容。
这个图形包含两个类型的对象:blobs和trees。
blobs通过git add
来创建,它们用来表示文件的内容。
trees是在提交时创建的。一个树表示工作副本中的一个路径。
下面是记录data
路径的新提交内容的树对象:
1 | 100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt |
第一行记录了所有用来重新生成data/letter.txt
文件的内容。第一部分指文件许可,第二部分表示指向当前内容的是一个blob文件,第三部分是这个blob文件的hash值,第四部分是这个文件的名称。
第二行记录了data/number.txt
文件的相关内容。
下面是alpha
路径的树对象,即当前项目的根路径的树对象:
1 | 040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data |
这单独的一行将这个树指向了data
树。
图中,root
树指向data
树,data
树指向表示data/letter.txt
和data/number.txt
的blob文件。
创建一个提交对象
git commit
在创建了树形图形后会创建一个提交对象。提交对象是.git/objects/
路径中的另一个文本文件。
1 | tree ffe298c3ce8bb07326f888907996eaa48d266db4 |
第一行指向树形图形,hash值指向表示工作副本中根路径的树对象,也就是alpha
路径。最后一行是提交信息。
将当期分支指向新提交
最后,提交命令将当期分支指向新的提交对象。哪一个是当前分支呢?Git会到.git/HEAD
路径中寻找HEAD
文件,并找到:
1 | ref: refs/heads/master |
这是说HEAD
正指向master
,master
是当前分支。
HEAD
和master
都是引用。引用是Git或用户用来标记一个特定的提交的标签。
代表master
引用的文件还不存在,因为这是仓库的第一次提交。Git会在.git/refs/heads/master
中创建一个文件,并将内容设置为此次提交对象的hash值:
1 | 74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd |
(如果你正在试着复现以上阅读到的Git操作,你会发现你的a1
提交的hash值会和我的不一样。像blob这类内容对象和树对象会总是转换到相同的hash值,但是提交对象不会,因为它们还包含了日期和创建者的名字等信息)
现在让我们添加HEAD
和master
到Git图形:
HEAD
和提交之前一样指向master
,但是master
现在指向新的提交对象。
再次提交
下面是a1
提交之后的Git图形。
工作副本,索引和a1
提交中``data/letter.txt和
data/number.txt的内容是相同的。索引和
HEAD`的提交都使用hash值来引用blob对象,但是工作副本的内容是用文本的形式存储在其他地方的。
1 | ~/alpha $ printf '2' > data/number.txt |
用户将data/number.txt
的内容设为2
。这样就更新了工作副本的内容,但是索引和HEAD
提交保持不变。
1 | ~/alpha $ git add data/number.txt |
用户将文件添加到Git中。这时会在objects
目录下添加一个内容为2
的blob文件,data/number.txt
的索引指向新的blob文件。
1 | ~/alpha $ git commit -m 'a2' |
用户提交了修改,这一步和之前的提交步骤是一样的。
首先,一个新的树形图形被创建出来,用来表示索引的内容。
data/number.txt
的索引入口被修改了。之前的data
树不再映射到data
目录,新的data
树对象必须创建:
1 | 100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt |
新的data
树被hash处理为不同于之前的data
树的值。新的root
树必须被创建,用来记录这个新的hash值:
1 | 040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data |
第二,新的提交对象被创建:
1 | tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556 |
提交对象的第一行执行新的root
树对象。第二行执行a1
:此次提交的父提交。为了找到父提交,Git找到HEAD
,通过HEAD
找到master
并找到a1
提交的hash值。
第三步,master
分支的文件内容被修改为新提交的hash值。
图形解读:内容以对象的形式存储在树上。也就是说只有变动的内容才存储到对象数据库中。上图中可以看到,a2
提交复用了a1
提交前就创建的a
blob文件。类似的,如果整个目录在一次次提交中都未做任何修改,那么它的树和所有的blobs文件以及树下的其他树对象都可以被复用。通常提交过程中都会有一些内容被修改,这意味着Git可以只用很小的空间来存储大量的提交历史。
图形解读:每一次提交都有父提交。这意味着Git仓库可以存储整个项目的历史
图形解读:引用指向一个提交历史中的一部分。也就是说提交可以被赋予有意义的名字。用户可以通过给引用使用有意义的命名,如fix-for-bug-376
来安排项目工作。Git提供HEAD
,MERGE_HEAD
,FETCH_HEAD
这类有指向意义的引用来操作提交历史。
图形解读:objects/
目录下的节点是不可改变的。也就是说,内容会被修改,不会被删除。每一个被添加的内容和每一次创建的提交都在objects
目录中的某个地方。
图形解读:引用是可变的。因此,引用的意义也可以被修改。master
分支指向的提交可能是当前项目中最好的版本,但是很快它也可能会被更新和更好的提交所取代。
图形解读:引用指向的工作副本和提交可以读取到,但是其他的提交则不行。这也意味着近期的历史很容易被重现,但是它也会经常更改。
工作副本是历史中最容易复现的,因为它们在仓库的根目录下。复现它们甚至不需要Git命令。它们同时也是历史中保存时间最短的,用户可以创建一个文件的无数个版本,但是Git不会记录它们,除非用户主动添加它们。
HEAD
指向的提交也非常容易复现。因为它指向刚刚切换到的分支。要想查看它的内容,用户可以使用stash,并检查工作副本。同时,HEAD
也是最经常更改的引用。
一个具体的引用指向的提交也是容易复现的。用户只需要切换到这个分支。这个分支的更改没有HEAD
那么频繁。
不被任何引用指向的提交时很难被复现的。用户不再使用一个引用越久,他们理解该引用的提交就越难。但用户不使用的时间越久,其实也不太会有什么新的更改历史。
切换到某次提交
1 | ~/alpha $ git checkout 37888c2 |
用户通过hash值查看a2
提交。
切换包括四个步骤:
第一,Git找到a2
提交以及它指向的树形图形
第二,修改树形图形中的文件入口,使其指向工作副本。这不会产生任何变化,工作副本已经有了树形图形的内容,因为HEAD
已经通过master
指向a2
提交
第三,Git在树形图形中将文件入口修改指向索引。这样不会有任何变化,因为已经有了a2
提交的内容。
第四,HEAD
的内容设置为a2
提交的hash值
修改HEAD
的内容到某个hash值会让整个仓库处于脱离的HEAD
的状态。注意下图中的HEAD
直接指向a2
提交,而不是指向master
1 | ~/alpha $ printf '3' > data/number.txt |
用户将data/number.txt
的内容设为3
,并提交了此次修改。Git通过HEAD
来找到a3
提交的父提交。不像之前需要找到并顺着某个分支引用来查找,这次直接找到并返回a2
提交的hash值。
Git更新HEAD
直接到a3
提交的hash值,仓库依然处于脱离的HEAD
状态。提交不再任何一个分支上,因为没有分支指向a3
或它的后代。这意味着这些提交很容易丢失。
到目前为止,树和blob会在图形中被忽略。
创建一个分支
1 | ~/alpha $ git branch deputy |
用户创建了一个新的deputy
分支,这样会在.git/refs/heads/deputy
路径创建一个新的文件,文件内容是HEAD
指向的提交的hash值,即a3
提交的hash值。
图形解读:分支即引用,引用即文件,这意味着Git的分支都是轻量级的。
创建deputy
分支让a3
提交能够安全地保存在一个分支上,HEAD
仍然是脱离的,因为它依然直接指向了一个提交。
切换分支
1 | ~/alpha $ git checkout master |
用户切换到master
分支。
首先,Git找到master
指向的a2
提交以及a2
指向的树形图形。
第二步,Git改写树形图形中指向工作副本的文件入口,将data/number.txt
的内容设置为2
第三步,Git改写树形图形中指向索引的文件入口,将data/number.txt
的入口更新为2
blob对应的hash值
第四步,Git通过修改以下内容以将HEAD
指向master
1 | ref: refs/heads/master |
切换到与工作副本不匹配的分支
1 | ~/alpha $ printf '789' > data/number.txt |
用户意外地将data/number.txt
的内容设置为789
。当他们想要切换到deputy
分支时,Git阻止了这次切换。
HEAD
正指向master
,master
指向了a2
提交,a2
提交中data/number.txt
的内容为2
。deputy
指向a3
,a3
提交中data/number.txt
的内容为3
。工作副本中data/number.txt
的当前版本为789
。这几个版本都是不一样的,必须先解决这些冲突。
Git可以将工作副本中data/number.txt
的内容替换为要切换到的分支的提交中的内容,但它会建立避免数据丢失。
Git也可以合并工作副本的当前版本和切换到的分支的版本,但是这样非常复杂。
所以,Git放弃了这次切换。
1 | ~/alpha $ printf '2' > data/number.txt |
用户注意到了他们不小心修改了data/number.txt
的内容,并将内容修改会原来的2
。然后成功切换到deputy
分支。
合并祖先
1 | ~/alpha $ git merge master |
用户将master
合并进deputy
。合并两个分支意味着核名两个提交。第一个提交是deputy
指向的提交,即合并者。第二次提交是master
指向的提交,即被合并者。这次合并中,Git没有做任何事情,所以它提示Already up-to-date
。
图形截图:图形中的一系列提交可以理解为一些了对仓库内容的修改。也就是说,在合并中,如果被合并者是合并者的祖先,那么Git不会做任何事情,这些修改已经被合并了。
合并后代
1 | ~alpha $ git checkout master |
用户切换到master
1 | ~/alpha $ git merge deputy |
用户将deputy
合并进master
。Git发现合并者的提交a2
是被合并者的提交a3
的祖先,就会执行一次快进合并。
它找到被合并者的提交和这次提交指向的树形图形,改写树形图形中指向工作副本和索引的文件入口,快进master
到a3
。
图形属性:图中一系列提交可以看做是一些了对仓库内容的修改。也就是说,当合并分支时,如果被合并者是合并者的后代,提交历史就不会被修改,合并者和被合并者直接的一系列提交已经存在。但是,尽管Git的修改历史没有改变,但是Git图形发生了变化。HEAD
指向的引用变成了合并者的提交
合并两个不同源提交
1 | ~/alpha $ printf '4' > data/number.txt |
用户将number.txt
的内容修改为4
,并将修改提交给master
1 | ~/alpha $ git checkout deputy |
用户切换到deputy
分支,将'data/letter.txt
的内容修改为b
,并提交到deputy
分支上。
图形属性:提交可以共享父提交,也就是说新的源头可以在提交历史中被创建出来。
图形属性:一个提交可以有多个父提交。也就是说不同源的提交可以由一次提交来合并,即一次合并提交
1 | ~/alpha $ git merge master -m 'b4' |
用户将master
分支合并进deputy
分支
Git发现合并者的b3
提交和被合并者的a4
提交是不同源的提交。所以需要做一次合并提交,整个过程包括8个步骤:
Git将被合并者的提交的hash值写入到
alpha/.git/MERGE_HEAD
文件中。这个文件的存在用来告诉Git目前正在合并过程中Git找到基准提交,即合并者和被合并者共同的最近的一次祖先提交
图形解读:提交都有父提交。也就是说向上追溯可以找到两个源头交汇的地方。Git分别从b3
和a4
向上寻找,直到找到最近的一次共同的父提交a3
,那么a3
就是基准提交。
Git通过树形图形分别为基准提交,合并者的提交和被合并者的提交创建索引
Git会创建一个对比(diff)来集合合并者和被合并者的提交对基准提交作出的改变。这个对比(diff)就是一个文件路径列表,每一条路径都指向一次修改,包括添加、删除、修改或者冲突
Git获取到记录了基准提交、合并者提交和被合并者提交的所有文件索引的列表,并逐一对比来确定对文件执行什么修改。Git会为每一条对比写入一个对应的入口,一条对比会有两个入口。
第一个入口是
data/letter.txt
,这个文件在基准提交中的内容是a
,在合并者的内容是b
,在被合并者的内容是a
。基准提交和合并者的内容是不同的,但基准提交和被合并者的内容是相同的。Git看到内容被合并者而非被合并者修改,因此data/letter.txt
的对比入口被定义为一次修改,而非冲突。对比中的第二个内容是
data/number.txt
。在这个例子中,基准提交和合并者的内容是一样的,但是被合并者的内容是不一样的,data/letter.txt
的对比入口也被定义为一次修改。图形解读:找到合并的基准提交意味着当合并者或被合并者其中之一基于基准提交修改了文件,Git可以自动合并文件,这就减少了用户需要做的事情。
对比中的修改入口会被应用到工作副本中。
data/letter.txt
的内容被改为b
,data/number.txt
的内容被改为4
对比中的修改入口会被应用到索引中,
data/letter.txt
会指向b
blob文件,data/number.txt
会指向4
blob文件更新后的索引会被提交
1
2
3
4
5
6
7tree 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
的新提交
合并修改了相同文件的不同源提交
1 | ~/alpha $ git checkout master |
用户切换到master
分支,将deputy
分支合并入master
分支。这时会将master
快进推到b4
提交。此时master
和HEAD
指向同一次提交。
1 | ~/alpha $ git checkout deputy |
用户切换到deputy
分支,将data/number.txt
的内容改为5
,并将修改提交到deputy
分支。
1 | ~/alpha $ git checkout master |
用户切换到master
分支,他们将data/number.txt
的内容修改为6
,并将修改提交到master
1 | ~/alpha $ git merge deputy |
用户将deputy
合并到master
分支,此时出现了冲突,合并停止。有冲突的合并过程和没有冲突的合并过程的前6个步骤是相同的:设置.git/MERGE_HEAD
,找到基准提交,生成基准提交、合并者提交和被合并者提交的索引,创建对比,更新工作副本,更新索引。由于出现了冲突,第7、8步无法执行,那我们来看看每一步发生了什么:
Git将被合并者提交的hash值写入到
.git/MERGE_HEAD
找到基准提交
b4
生成基准提交、合并者提交和被合并者提交的索引
生成一个集合了合并者和被合并者提交的基于基准提交的修改对比
在这个例子中,这个对比只包含一个文件入口:
data/number.txt
。这个入口被标记为冲突,因为文件内容在基准提交、合并者和被合并者中都不相同修改的内容被应用到工作副本中。冲突的部分,Git会将两个版本都写入文件,因此
data/number.txt
被写为1
2
3
4
5<<<<<<< HEAD
6
=======
5
>>>>>>> deputy对比中标记的修改也被应用到索引中,索引中的文件入口标记了文件的路径和阶段。没有冲突的文件标记的阶段为
0
,在此次合并之前,索引应该如下:1
20 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb此次合并之后,对比写入索引,索引如下:
1
2
3
40 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
1 data/number.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767
2 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
3 data/number.txt 7813681f5b41c028345ca62a2be376bae70b7f61data/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
20 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/number.txt 9d607966b721abde8931ddd052181fae905db503- 用户提交。Git看到仓库中的
.git/MERGE_HEAD
,直到正在合并过程中。于是查看索引,看到并没有冲突,于是新创建一个b11
提交,用来记录合并后的内容,并删除.git/MERGE_HEAD
中的文件。合并完成。 - Git将
master
指向新的提交
- 用户提交。Git看到仓库中的
删除文件
以下Git 图形包含了提交历史、最近一次提交的树形图形和Blob对象,工作副本和索引:
1 | ~/alpha $ git rm data/letter.txt |
用户告诉git删除data/letter.txt
文件。文件从工作副本中被删除。索引中的文件入口也被删除
1 | ~/alpha $ git commit -m '11' |
用户提交。作为提交的一个部分,Git创建一个树形图形来记录索引的内容。data/letter.txt
不包含在树形吐信中,因为它不再索引中。
复制仓库
1 | ~/alpha $ cd .. |
用户将alpha/
仓库中的内容复制到bravo/
路径下。这回创建如下路径结构:
1 | ~ |
bravo
路径下会有另一个Git图形
连接两个仓库
1 | ~ $ cd alpha |
用户切回到alpha
路径下,将bravo
设置为alpha
的远程仓库。这一操作会在alpha/.git/config
中添加如下内容:
1 | [remote "bravo"] |
这表明在路径../bravo
下有一个名为bravo
的远程仓库
从远程仓库获取分支
1 | ~/alpha $ cd ../bravo |
用户进入bravo
仓库,将data/number.txt
的内容设为12
,并将修改提交到bravo
的master
上。
1 | ~/bravo $ cd .../alpha |
用户进入alpha
仓库,他们获取远程bravo
仓库的master
分支,这个过程包括四个步骤:
Git找到
bravo
仓库的master
分支指向的提交的hash值,即提交12
的hash值Git创建一个新的列表,内容包括
12
提交依赖的所有内容:提交对象本身,树形图形中的对象,提交12
的祖先提交,祖先提交的树形图形对象。alpha
对象数据库已有的内容会从整个列表中移除,然后将其余的内容拷贝到alpha/.git/objects
中去。将引用文件中的
alpha/.git/refs/remotes/bravo/master
指向提交12
alpha/.git/FETCH_HEAD
设置为:1
94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo
这意味着最近一次的获取命令获取到了
bravo
仓库的master
分支的提交12
图形解读:对象可以被复制,也就是说提交历史可以被仓库共享
图形解读:仓库可以储存远程分支的引用,比如
alpha/.git/refs/remotes/bravo/master
。也就是说一个仓库可以在本地记录远程仓库的分支的状态,它获取到的远程分支的状态在获取的时候是保持同步的,但是远程分支的修改不会自动同步到本地仓库里。
合并FETCH_HEAD
1 | ~/alpha $ git merge FETCH_HEAD |
用户合并了FETCH_HEAD
。FETCH_HEAD
只不过另一个引用,它指向被合并者的提交12
,HEAD
指向合并者的提交11
。Git执行了一次快进合并,并将master
指向提交12
。
从远程仓库拉分支
1 | ~/alpha $ git pull bravo master |
用户从alpha
仓库拉远程bravo
仓库的master
分支。拉(pull)是获取(fetch)和合并(merge)FETCH_HEAD
的简写。Git会执行者两个命令,并显示master
分支已经Already up-to-date
克隆一个仓库
1 | ~/alpha $ cd .. |
用户进入上一级路径,将alpha
仓库克隆到charlie
仓库。克隆到charlie
仓库的结果与用户使用cp
命令聊创建bravo
仓库类似,Git会创建一个新的名为charlie
的仓库,初始化charlie
为一个Git仓库,添加alpha
作为远程仓库,并将远程仓库命名为origin
,获取origin
并合并FETCH_HEAD
将内容推到远程仓库的分支
1 | ~ $ cd alpha |
用户回到alpha
仓库,修改data/number.txt
的内容为13
并提交修改到alpha
的master
分支
1 | ~/alpha $ git remote add charlie ../charlie |
设置charlie
为alpha
仓库的远程仓库
1 | ~/alpha $ git push charlie master |
用户尝试将master
分支推到charlie
所有的提交13
相关的对象都被拷贝到charlie
用户需要创建一个新的分支,合并提交13
再推到推到charlie
。但实际上,用户希望有一个能随时推随时拉的仓库,就像GitHub的远程那样,他们需要一个bare仓库。
克隆一个bare仓库
1 | ~/alpha $ cd .. |
用户进入到上一级目录,把delta
克隆为一个bare仓库。这和普通的克隆仓库有两点不同。config
配置文件会表明这个仓库是一个bare仓库。另外,存在.git
目录下的文件会存到根目录下:
1 | delta |
将分支推到bare仓库
1 | ~ $ cd alpha |
用户回到alpha
目录下,将delta
设置为alpha
的远程仓库
1 | ~/alpha $ printf '14' > data/number.txt |
将data/number/txt
的内容设置为14
,并提交到alpha
的master
分支
![image-20181202222812723](/Users/quanxu/Library/Application Support/typora-user-images/image-20181202222812723.png)
1 | ~/alpha $ git push delta master |
将内容推到delta
的master
分支,总共分三步:
- 与提交
14
相关的所有对象都从alpha/.git/objects/
路径拷贝到delta/objects/
delta/refs/heads/master
更新到提交14
alpha/.git/refs/remotes/delta/master
被设为指向提交14
。