git上传分支的原理_深入理解git版本管理原理

日常项目管理中我们最常使用的git命令有add、commit、push、pull,但其他不常使用的命令往往容易误操作,所以想深入的学习一下git操作命令底层原理到底是怎么样的,在阮一峰大大的日志里面看到了《Git from the inside out》,全文通过树状图的方式表示各分支节点之间的关系,以示例的方式阐述每种操作命令后底层文件及索引的变化。然而是全英文的,于是乎我只能每天抽点时间来翻译加学习,前前后后经历了一周,终于完成了,大家一起学起来吧。

git init

初始化git仓库(该操作会在当前目录下创建一个.git目录,里面可以放git配置或者项目历史记录:.git/objects)。

例如:

~ $ mkdir alpha

~ $ cd alpha

~/alpha $ mkdir data

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

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

~/alpha $ git init

目录结构如下:

alpha

├── data

| └── letter.txt

| └── number.txt

└── .git

├── objects

etc...

.git目录及其内容是git相关的文件,除此之外所有其他文件统称为工作副本,为用户文件。

git add

在git仓库中添加一些文件。

例如:

~/alpha $ git add data/letter.txt

第一步:在.git/objects目录中创建一个新的blob文件(创建的blob文件包含data/letter.txt的压缩内容,文件名是由它的内容哈希得到)

git将data/letter.txt中的内容ahash计算得到2e65efe2a145dda7ee51d1741299f848e5bf752e,前两个字符被用作对象数据库中的目录名:.git/objects/2e/;

hash散列值的剩余部分用作blob文件(被添加的文件中需要保存的内容)的名称:.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e;

第二步:将文件添加到索引中

索引是一个列表,其中包含Git要跟踪的每个文件,它以.git/index文件的形式存储,其中每行指向跟踪的blob文件,包含文件内容的hash散列值。例如:data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e;

注意:git add data命令执行,索引中只列出data目录中的文件,并不会列出data目录;

以同样的方法我们将data/number.txt文件添加到git仓库后,当用户修改data/number.txt文件中内容,并重新执行git add命令时,git会根据更新后的内容创建一个新的blob,同时更新data/number.txt的索引条目以指向新的blob。

例如:

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

~/alpha $ git add data

git commit

通过git commit命令创建a1提交。

~/alpha $ git commit -m 'a1'

[master (root-commit) 774b54a] a1

第一步:创建一个树状图来表示提交的项目版本的内容(git通过索引创建树状图来记录项目的当前状态,这个树状图记录了项目中每个文件的位置和内容)

树状图由两类对象组成:blobs和trees

blobs:通过git add存储,表示文件内容;

trees:通过git commit存储,表示工作副本中的目录;

例如:分别对应文件权限、条目类型、blob文件的hash散列值、文件名称;

100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt

100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de number.txt

040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data;

a1提交的树状图:

root→data→a(data/letter.txt) and 1(data/number.txt)

第二步:创建一个提交对象(git commit在创建树状图之后就会创建一个提交对象,提交对象是.git/objects中的另一个文本文件)

例如:

tree ffe298c3ce8bb07326f888907996eaa48d266db4

author Mary Rose Cook 1424798436 -0500

committer Mary Rose Cook 1424798436 -0500

a1

first line:指向树状图,其中hash散列值由工作副本的根目录生成(也就是alpha);

last line:提交信息;

a1的提交对象,指向它的树状图:

a1→root→data→a(data/letter.txt) and 1(data/number.txt)

第三步:将当前分支指向新的提交对象(git在.git/HEAD的头文件中查找当前分支)

例如:ref: refs/heads/master,表明HEAD指向master,所以master为当前分支;

注意:首次提交时master的ref是不存在的,git会创建.git/refs/heads/master,并将其内容设置为提交对象的hash散列:74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd;

当前分支指向提交对象a1:

HEAD→master→a1→root→data→a(data/letter.txt) and 1(data/number.txt)

git commit(非初次提交)

下面为a1提交后的结构图,工作副本及索引已存在,此时三方的data/letter.txt 和data/number.txt内容一致:

修改data/number.txt的内容,工作副本更新:

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

执行git add命令,在.git/objects目录中创建一个新的blob文件,并将文件添加到索引中:

~/alpha $ git add data/number.txt

执行git commit命令:

~/alpha $ git commit -m 'a2'

创建一个新的树状图来表示索引的内容:

100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt

100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 number.txt

040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data

然后创建一个新的提交对象:

tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556

parent 774b54a193d6cfdd081e581a007d2e11f784b9fe

author Mary Rose Cook 1424813101 -0500

committer Mary Rose Cook 1424813101 -0500

a2

first line:指向新的roottree对象;

second line:指向a1(为了找到父提交,git转到HEAD,跟着它转到master,最终找到a1提交的hash散列);

last line:提交信息;

将当前分支指向新的提交对象:

从结构图可以看出以下特性:

文件内容是以对象树存储的。这意味着只有差异会存储在对象数据库中,上图中a2提交时复用了a1提交之前生成的blob(根据内容a生成)。同理,如果整个目录从一个commit到另一个commit时并没有发生改变,那么它的对象树以及下面的所有blobs对象和trees对象都是可以复用的。通常,从一个commit到另一个commit的内容变更较少,这就是git可以在很小的空间中存储大量提交历史的原因。

每个commit都会有一个parent。这意味着一个仓库可以存储一个项目的所有历史记录。

refs是入口,指向了commit历史的一部分。每项commit都有自己独特的标识,用户通过类似树状结构的“族谱”将他们的工作组织起来,例如:refs具体为fix-for-bug-376。git则使用特殊的符号例如HEAD、MERGE_HEAD、FETCH_HEAD来支持通过用命令行操作提交历史。

.git/objects目录下的节点是不变的。也就是说,内容只能被编辑,不能被删除。添加的每个文件内容和创建的每个提交都能在.git/objects目录中找到。

refs是可变的。因此,ref的含义可以改变,master所指向的commit可能是目前项目的最佳版本,但是很快,它就会被更新更好的commit所取代。

通过ref指向的工作副本和commit很容易回索,但其他的commit就不是。意思是最近的历史记录更容易找到,但也经常改变。换句话说就是git比较健忘,如果想要查找比较久远的提交记录就需要深度索引。

git checkout(检出commit)

通过git checkout命令+a2提交的hash散列值检出a2commit。

例如:

~/alpha $ git checkout 37888c2

You are in 'detached HEAD' state...

第一步:git获取到a2提交及它所指向的树状图。

第二步:git将树状图中的文件条目写入工作副本。

这时内容并不会发生改变。因为此时HEAD就是通过master指向a2提交,所以a2对应的树状图内容也已经被写入工作副本中。

第三步:git将树图中的文件条目写入索引。

这也不会导致任何变化。因为索引已经包含了a2提交的内容。

第四步:HEAD的内容被设置为a2提交的hash散列值。

例如:f0af7e62679e144bb28c627ee3e8f7bdb235eee9

通过设置HEAD的内容为hash散列值,会使Head直接指向a2而不是原本的master(仓库将被至于分离的HEAD):

此时提交的commit很容易丢失。比如修改number.txt文件内容为3并提交修改,git会通过HEAD去获取a3提交的parent,而不是像之前一样利用ref实现跟踪和查找。最终由HEAD直接指向a3的提交对象(仓库仍处于分离的HEAD中,无论是a3还是之后的commit都没有在任何分支上)。

例如:

~/alpha $ printf '3' > data/number.txt

~/alpha $ git add data/number.txt

~/alpha $ git commit -m 'a3'

[detached HEAD 3645a0e] a3

树状图结构如下:

git branch(创建分支)

通过git branch命令创建一个名为deputy的分支,实际就是在.git/refs/heads/deputy目录下创建了一个新文件,其中包含了HEAD所指向的hash散列值(a3提交的hash散列)。

~/alpha $ git branch deputy

树状图结构如下(分支deputy的创建使得a3提交被添加到该分支,安全性就有了,不至于会丢失。但HEAD还是在分离状态,因为它仍然指向了commit):

git checkout(检出分支)

通过git checkout命令检出master分支。

~/alpha $ git checkout master

Switched to branch 'master'

第一步:git获取到master指向的a2提交及a2所指向的树状图。

第二步:git将树状图中的文件条目写入工作副本。

这时会将data/number.txt文件内容写为2。

第三步:git将树图中的文件条目写入索引。

这时会将data/number.txt文件的条目更新为2blob文件的hash散列。

第四步:git将HEAD的内容由hash散列值修改为ref: refs/heads/master,使得HEAD重新指向master。

树状图结构如下:

git checkout(检出与工作副本不兼容的分支)

本地修改文件data/number.txt内容后,通过git checkout命令检出deputy分支。

~/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.

很显然,checkout被git无情拒绝,原因是此时三方的文件内容不一致,必须先解决差异(git如果做覆盖操作会使信息丢失,如果做合并又太复杂):

HEAD指向master,master指向a2,而a2中data/number.txt文件的内容为2;

deputy指向a3,而a3中data/number.txt文件的内容为3;

本地工作空间中data/number.txt文件的内容为789;

所以将误修改复原就可以解决了(假设不是误修改,那你需要将修改先提交到原分支):

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

~/alpha $ git checkout deputy

Switched to branch 'deputy'

树状图结构如下:

git merge(合并父分支到子分支)

通过git merge命令将master分支合并到deputy,合并两个分支其实就是合并两个commit,对于这样的合并git什么也不做。

~/alpha $ git merge master

Already up-to-date.

结构图中一系列的提交其实就是对仓库文件内容做的一系列修改,所以,如果是将父提交合并至子提交,git什么也不做,因为这些改变其实已经被合并了。

git merge(合并子分支到父分支)

先将分支切换回master:

~/alpha $ git checkout master

Switched to branch 'master'

通过git merge命令将deputy分支合并到master:

~/alpha $ git merge deputy

Fast-forward

git获取到子提交及它所指向的树状图,git将树状图中的文件条目写入工作副本和索引,git的fast-forwards操作将master指向了a3(如前面所说的,结构图中一系列的提交其实就是对仓库文件内容做的一系列修改,合并时,如果是将子提交合并至父提交,提交历史是不会改变的,只是合并双方之间差了一些修改,所以最终改变的是被合并分支的指向)。

git merge(合并非直接关联分支)

本地修改文件data/number.txt内容为4,并提交为a4至master分支:

~/alpha $ printf '4' > data/number.txt

~/alpha $ git add data/number.txt

~/alpha $ git commit -m 'a4'

[master 7b7bd9a] a4

切换至deputy分支,本地修改文件data/letter.txt内容为b,并提交为b3至deputy分支:

~/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

从结构图可以看出以下特性:

commit可以共享parent。所以在提交历史中可以创建新的“族谱”。

commit可以包含多个parent。所以不同的“族谱”可以通过一个包含两个parent的commit来连接(commit有两个parent的情况是通过merge实现)。

例如:将master分支合并至deputy分支(由于git发现这两个分支对应的commit属于不同的“族谱”,所以需要合并commit,总共分为8步)

~/alpha $ git merge master -m 'b4'

Merge made by the 'recursive' strategy.

第一步:git将giver commit(也就是a4)的hash散列值写入alpha/.git/MERGE_HEAD文件

这个文件的存在就是告诉git正在做合并操作。

第二步:git查找base commit

是receiver commit(也就是b3)和giver commit(也就是a4)在历史记录中最近的共同祖先(通俗的说:两个“族谱”分道扬镳的节点,也就是a3)。

第三步:git基于树状图为base commit、receiver commit、giver commit生成索引

第四步:git生成一个diff

diff可以理解为差异文件,其中组合了receiver commit和giver commit对base commit的更改,diff是一个指向被修改文件的路径列表(文件修改包括:add、remove、modify、conflict)。

git通过获取出现在base commit、receiver commit、giver commit索引中的所有文件列表,对于每一个都进行比较,确定文件被修改后就向diff写入一个对应的条目,在本例中,diff有两个条目。

一个是data/letter.txt,base commit中是a,receiver commit中是b,giver commit中是a。git可以看到内容是由

receiver修改的,而不是giver,所以diff中data/letter.txt对应的条目是一个modify,而不是conflict。

另一个是data/number.txt,base commit中是3,receiver commit中是3,giver commit中是4,所以diff中data/number.txt对应的条目也是一个modify。

第五步:git将diff中的修改应用于工作副本

data/letter.txt中的内容被设置成b,data/number.txt中的内容被设置成4。

第六步:git将diff中的修改写入索引

data/letter.txt对应的条目指向b的blob文件,data/number.txt对应的条目指向4的blob文件。

第七步:git提交更新后的索引

可以看到此时的提交就有两个parent。

tree 20294508aea3fb6f05fcc49adaecc2e6d60f7e7d

parent 982dffb20f8d6a25a8554cc8d765fb9f3ff1333b

parent 7b7bd9a5253f47360d5787095afc5ba56591bfe7

author Mary Rose Cook 1425596551 -0500

committer Mary Rose Cook 1425596551 -0500

b4

第八步:git将当前分支deputy指向最新的提交b4(将a4合并到b3的递归合并结果)

git merge(合并非直接关联分支,且修改了相同文件)

先切换至master分支,将deputy分支合并至master分支(也就是前面的将子分支合并到父分支,其实只是修改了master分支的commit指向):

~/alpha $ git checkout master

Switched to branch 'master'

~/alpha $ git merge deputy

Fast-forward

此时切换至deputy分支,本地修改文件data/number.txt内容为5,并提交为b5至deputy分支:

~/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

然后切换至master分支,本地修改文件data/number.txt内容为6,并提交为b6至master分支:

~/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

最终,将deputy分支合并到master分支,很显然被git拒绝,原因是data/number.txt文件中的内容冲突了,自动合并失败:

~/alpha $ git merge deputy

CONFLICT in data/number.txt

Automatic merge failed; fix conflicts and

commit the result.

整个过程与上面合并时的前6步是一致的:alpha/.git/MERGE_HEAD文件设置;查找base commit;为base commit、receiver commit、giver commit生成索引;生成diff文件;将diff中的修改应用于工作副本;git将diff中的修改写入索引;但是由于冲突第7步的提交和第8步的ref更新不能正常执行。

下面详细说明一下前面6步到底发生了什么导致最终的结果:

第一步:git将giver commit(也就是b5)的hash散列值写入alpha/.git/MERGE_HEAD文件

同样的,这个文件的存在就是告诉git正在做合并操作。

第二步:git查找base commit

是receiver commit(也就是b6)和giver commit(也就是b5)在历史记录中最近的共同祖先(通俗的说:两个“族谱”分道扬镳的节点,也就是b4)。

第三步:git基于树状图为base commit、receiver commit、giver commit生成索引

第四步:git生成一个diff

diff可以理解为差异文件,其中组合了receiver commit和giver commit对base commit的更改,diff是一个指向被修改文件的路径列表(文件修改包括:add、remove、modify、conflict)。

git通过获取出现在base commit、receiver commit、giver commit索引中的所有文件列表,对于每一个都进行比较,确定文件被修改后就向diff写入一个对应的条目,在本例中,diff只有一个条目。

也就是data/number.txt,base commit中是4,receiver commit中是6,giver commit中是5。条目被标记为conflict,因为data/number.txt的内容在receiver、giver和base中是不同的。

第五步:git将diff中的修改应用于工作副本

对于冲突的部分,git会将两个版本都写入到工作副本的文件中。data/number.txt中的内容被设置成:

<<<<<<< HEAD

6

=======

5

>>>>>>> deputy

第六步:git将diff中的修改写入索引

索引中的条目由其文件路径和stage共同组成唯一标识,对于没有冲突的文件,stage为0。合并前的索引如下(前面的0就是stage):

0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748

0 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb

合并的diff被写入索引后:

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条目已经没有了,在该位置上新增了3个新的条目。stage1条目包含了base(data/number.txt)内容的hash散列,stage2条目包含了receiver(data/number.txt)内容的hash散列,stage3条目包含了giver(data/number.txt)内容的hash散列。这三个条目的存在就是在告诉gitdata/number.txt是冲突的,所以合并就被中断了。

此时,如果用户通过将data/number.txt的内容设置为11来整合两个冲突版本的内容,并通过git add将文件添加至索引中:

~/alpha $ printf '11' > data/number.txt

~/alpha $ git add data/number.txt

整体过程就是:git add命令创建了一个包含11的blob文件,该操作就是就是告诉git冲突解决了,此时git就会从索引中移除stage为1、2、3的条目,并使用新blob文件的散列为data/number.txt添加一个stage为0的条目:

0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748

0 data/number.txt 9d607966b721abde8931ddd052181fae905db503

第七步:用户通过git commit命令提交最新修改

~/alpha $ git commit -m 'b11'

[master 251a513] b11

git在仓库中看到.git/MERGE_HEAD,就知道合并正在进行中,它会检查索引并查看是否存在冲突,如果没有就会创建一个新的提交b11来记录已解决的合并内容,然后删除.git/MERGE_HEAD中的文件,此时合并就完成了。

第八步:git将当前分支master指向新的提交。

git rm(删除文件)

下面是当前状态下最新的结构图:

通过git rm命令删除data/letter.txt文件,文件首先会从本地的工作副本移除,接着文件条目会从索引中移除:

~/alpha $ git rm data/letter.txt

rm 'data/letter.txt'

此时结构图就变成了:

通过git commit命令提交变更:

~/alpha $ git commit -m '11'

[master d14c7d2] 11

和之前一样,作为提交的一部分,git会构建一个表示索引内容的树状图,data/letter.txt不包括在树图中,因为它不在索引中。

复制仓库

~/alpha $ cd ..

~ $ cp -R alpha bravo

用户将alpha/仓库的内容复制到bravo/目录,目录结构就变成了:

~

├── alpha

| └── data

| └── number.txt

└── bravo

└── data

└── number.txt

而此时bravo/目录中也会有一个与之对应的git结构图:

建立两个仓库的链接

用户首先返回到alpha仓库:

~ $ cd alpha

~/alpha $ git remote add bravo ../bravo

如果要将bravo设置为alpha的远程仓库,需要在alpha/.git/config文件中添加一些代码:

[remote "bravo"]

url = ../bravo/

指定在../bravo目录中有一个名为bravo的远程仓库。

从远程仓库上fetch分支

用户首先进入bravo仓库,将data/number.txt的内容设置为12,并将修改提交给bravo上的master:

~/alpha $ cd ../bravo

~/bravo $ printf '12' > data/number.txt

~/bravo $ git add data/number.txt

~/bravo $ git commit -m '12'

[master 94cd04d] 12

此时结构图如下:

然后用户进入alpha仓库,想要把分支master从bravo取过来:

~/bravo $ cd ../alpha

~/alpha $ git fetch bravo master

Unpacking objects: 100%

From ../bravo

* branch master -> FETCH_HEAD

这个过程git有四个步骤:

第一步:获取master在bravo上所指向的commit的散列

也就是12 commit提交的散列。

第二步:将12 commit依赖的所有对象(去除alpha仓库中已存在的)复制到alpha/.git/objects/中

包括提交对象本身、树图中指向的对象、12 commit的父提交以及它在树图中指向的对象。

第三步:alpha/.git/refs/remotes/bravo/master中的ref被设置成12 commit提交的散列值

第四步:alpha/.git/FETCH_HEAD的内容被设置成:

94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo

这表明刚刚的fetch命令从bravo获取了master的12 commit,此时结构图就变成了:

从结构图可以看出以下特性:

对象是可以被拷贝的。也就是说历史记录可以在仓库之间共享。

一个仓库可以存储远程仓库分支的ref,例如alpha/.git/refs/remotes/bravo/master。这意味着一个仓库可以在本地记录远程仓库上分支的状态。它在获取时是正确的,但是如果远程分支发生更改,它就会过期。

合并FETCH_HEAD

用户通过git merge命令合并FETCH_HEAD:

~/alpha $ git merge FETCH_HEAD

Updating d14c7d2..94cd04d

Fast-forward

FETCH_HEAD只是一个ref,它解析为12 commit(giver),HEAD指向11 commit(receiver)。git执行合并后将指向master→12 commit:

从远程仓库上pull分支

用户将master分支从bravo pull到alpha,pull是“fetch FETCH_HEAD和 merge FETCH_HEAD”的缩写,所以最终git执行两个命令并反馈master已经是最新的了。

~/alpha $ git pull bravo master

Already up-to-date.

clone仓库

用户移动到上层目录并clone alpha到charlie:

~/alpha $ cd ..

~ $ git clone alpha charlie

Cloning into 'charlie'

clone到charlie的结果与之前用户为了生成bravo仓库所使用的cp类似,git创建一个名为charlie的新目录,并将它初始化为git仓库,将alpha作为一个名为origin的远程仓库,fetch origin并合并FETCH_HEAD。

将分支push到从远程仓库中checkout的分支上

用户回到alpha仓库,修改data/number.txt的值为13并将修改提交到master分支。

~ $ cd alpha

~/alpha $ printf '13' > data/number.txt

~/alpha $ git add data/number.txt

~/alpha $ git commit -m '13'

[master 3238468] 13

将charlie设置为alpha的远程仓库:

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

将master分支push到charlie:

~/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

13 commit关联的所有对象将被复制到charlie。但从上面的命令行反馈可以看到,push过程被中断,git拒绝push到远程检出(checkout)的分支。其实这是可以理解的,因为这样的push将更新远程索引和HEAD,如果有人正在编辑远程上的工作副本,就会导致混乱。

此时,用户可以创建一个新分支,将13 commit合并到其中,并将该分支push到charlie。但实际上,我们是想要一个可以随时可以push的仓库,一个中央存储库,用于push和pull,但是没有人直接进行commit提交,类似GitHub的远程仓库,想要一个裸(bare)存储库。

clone一个裸(bare)仓库

用户进入到上层目录,clone delta作为裸仓库:

~/alpha $ cd ..

~ $ git clone alpha delta --bare

Cloning into bare repository 'delta'

跟普通的clone有两个不同之处,首先config文件表明存储库是裸仓库,而原本存储在.git目录下的文件则存储在仓库的根目录下:

delta

├── HEAD

├── config

├── objects

└── refs

此时的结构图如下:

将分支push到裸(bare)仓库

用户回到alpha仓库并设置delta作为它的远程仓库:

~ $ cd alpha

~/alpha $ git remote add delta ../delta

修改data/number.txt的值为14并将修改提交到master分支:

~/alpha $ printf '14' > data/number.txt

~/alpha $ git add data/number.txt

~/alpha $ git commit -m '14'

[master cb51da8] 14

提交后结构图如下:

接下来将master push到delta:

~/alpha $ git push delta master

Writing objects: 100%

To ../delta

3238468..cb51da8 master -> master

整个过程有3步:

第一步:从alpha/.git/objects/拷贝14 commit提交相关的所有对象至delta/objects/

第二步:delta/refs/heads/master的指向更新为14 commit

第三步:alpha/.git/refs/remotes/delta/master的指向更新为14 commit,alpha拥有了delta状态的最新记录

现在的结构图如下:

参考文献

你可能感兴趣的:(git上传分支的原理)