全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/12966398.html, 多谢,=。=~
(如果对你有帮助的话请帮我点个赞啦)
日常项目管理中我们最常使用的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
中的内容a
hash计算得到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 <mary@maryrosecook.com> 1424798436 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 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 <mary@maryrosecook.com> 1424813101 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
a2
- first line:指向新的
root
tree对象; - 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散列值检出a2
commit。
例如:
~/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
文件的条目更新为2
blob文件的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 <mary@maryrosecook.com> 1425596551 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 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个新的条目。stage
1条目包含了base
(data/number.txt
)内容的hash散列,stage
2条目包含了receiver
(data/number.txt
)内容的hash散列,stage
3条目包含了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 from the inside out:https://codewords.recurse.com/issues/two/git-from-the-inside-out