之前用过git,感觉很神奇。很好用之际,膜拜linux,他能迅速写出这么给力的版本控制系统而我不能理解其原理,惭愧之极。
今儿偶然看到一篇国外网友写的文章介绍git原理,看完茅塞顿开。遂翻译之,以记之!
原文地址:http://maryrosecook.com/blog/post/git-from-the-inside-out
=================================================================================================================================
step by step!
1、新建文件夹,应用git初始化之
~ $ mkdir alpha
~ $ cd alpha
~/alpha $ mkdir data
~/alpha $ printf 'a' > data/letter.txt
~/alpha $ git init
Initialized empty Git repository
现在alpha文件夹是这个样子滴:
alpha
├── data
| └── letter.txt
└── .git
├── objects
etc...
.git文件夹就是git生成的,以后git干的所有事儿都在这里头。
2、添加一些文件
~/alpha $ git add data/letter.txt
用户使用git add命令将data/letter.txt添加到git中。这会产生两个效果:
首先在.git/objects/文件夹中创建一个新的二进制文件。
这个二进制文件就是data/letter.txt文件中的内容。这个二进制文件的名字是由其内容经过哈希运算而来。比如,文件中的内容是a,那么git将a哈希运算得到2e65efe2a145dda7ee51d1741299f848e5bf752e
这一串字符,它是唯一的。这个二进制文件就放在.git/object/2e/文件夹下面。注意到2e/这个文件夹名,它取自那一长串哈希名字的前两个字母,剩下的字符串才是正宗的二进制文件名:.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e
.
上面已经说了,通过git add 命令git已经把文件内容存放在/objects文件夹中,所以这时候用户删除了工作区的/data/letter.txt也不会影响git中/objects中的二进制文件。
其次,git add命令为文件建立索引。索引列表包含了所有git所跟踪的文件。索引文件放在.git/index中。索引文件每一行映射一个文件内容的哈希值(就是刚才说的那个哈希值)。通过git add后,index文件中添加的一行看起来这个样子:
data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
now, 用户又新建文件:
~/alpha $ printf '1234' > data/number.txt
现在文件夹结构如下:
alpha
└── data
└── letter.txt
└── number.txt
git add一下:
~/alpha $ git add data
同样,git为number.txt新建二进制文件,然后添加对应索引。现在索引文件有两行了:
data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
data/number.txt 274c0052dd5408f8ae2bc8440029ff67d79bc5c3
当用户改变number.txt中的内容,再次git add,那么同样地,根据新内容新建二建文件,更新index文件中data/number.txt行对应的哈希值。
3、提交
~/alpha $ git commit -m 'a1'
[master (root-commit) 774b54a] a1
用户提交了一个a1提交。commit命令三步走:
1.建立树图用来表示提交版本的内容。
2.建立commit对象。
3.指向commit时的分支。
* 建立树图
git通过索引建立树图来表示当前项目状态,记录着项目的每个文件的位置和内容。该图有两种类型:二进制文件和树。其中二进制文件就是上面所说的。树是commit命令时生成的。树表示了工作区的目录。
下面这个树对象记录着刚才commit所提交的data/目录中的内容:
100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de number.txt
来看第一行,第一部分表示文件的权限。第二部分表示文件的类型是二进制文件。第三部分表示二进制二进制文件的哈希值。第四部分是文件名。
下面这个树对象记录着刚才commit所提交的alpha/目录中的内容:
040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data
这行的第二部分tree表示这是一个树对象,指向刚才解释的那两行。第四部分的data其实就是上面所说的data/目录。
* 建立commit对象
git commit 经过上面的建图一步后,下一步就是建commit对象了。这个commit Object就是一个text文件,放在.git/objects/ 中:
tree ffe298c3ce8bb07326f888907996eaa48d266db4
author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
a1
a1提交指向了它的树图:
经过前三步,最后就是指向当前分支
什么是当前分支?git在.git/HEAD目录中找到HEAD文件,里面内容:
ref: refs/heads/master
就是说HEAD指向master。master是当前分支。
HEAD和master都是refs。一个ref是git的标签,用来标示用户的commit。
刚才已经创建了提交,所以在文件.git/refs/heads/master中生成一个commit Object对象a1的哈希值:
74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd
现在有了HEAD和master的git图如下:
再次提交
讲过第一次提交后,工作区文件和git管理的文件如下图:
当用户修改/data/number.txt中的内容为2,git中的index和HEAD所管理的文件并不会改变,因为这只是在工作区中的修改。所以图是这样:
好,下面git add一下:
~/alpha $ git add data/number.txt
根据上面已经讲解的,创建对应二进制文件,添加索引。那么图就是这样地:
提交以下,命名为a2:
~/alpha $ git commit -m 'a2'
[master f0af7e6] a2
根上面讲解一样,commit分三步走:
第一步
先建树图用来表示索引中的内容。在index文件中,对应data/number.txt那一行已经改变了。所以代表data那个树应该跟着新建:
100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 number.txt
data树改变了,那么alpha树也跟着变,哈希值重新计算:
040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data
第二步
建立新的commit Object
tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556
parent 774b54a193d6cfdd081e581a007d2e11f784b9fe
author Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
a2
第一行的提交对象利用新的哈希值指向新的alpha树。第二行指向a1提交,也就是当前提交的父提交。为了找到父提交,git在HEAD中找到当前分支master,然后再master文件中找到a1的哈希值。
第三步
当前分支指向当前提交
4、检出一个提交
~/alpha $ git checkout 37888c2
You are in 'detached HEAD' state...
git checkout后面跟的是某次提交的哈希值。该哈希值可以使用git log 查找出来。
在这个checkout过程中有四个步骤:1、git通过哈希值获得a2提交,根据a2提交得到其指向的树图。2、将树图中的文件实体写入到工作区中。3、git将树图中的文件实体写入到index文件中。4、HEAD指向a2 commit。
将HEAD直接指向某个提交的哈希值,会使仓库处于HEAD游离态。注意下面这个图的HEAD直接指向a2 commit,而不再直向master.
现在进行如下操作:
~/alpha $ printf '3' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a3'
[detached HEAD 3645a0e] a3
用户将number.txt的内容改为3,让后提交改变。git通过HEAD找到a3提交的父提交a2。然后更新HEAD使其指向新提交a3.现在仓库仍然处于游离态,因为HEAD没有指向一个分支(比如master)。现在的状态如下图:
5、建立新分支
~/alpha $ git branch deputy
用户新建分支,名为deputy。在这个过程中git其实仅仅生成了一个新文件.git/refs/heads/deputy,这个文件的内容是当前HEAD指向的a3提交的哈希值。
由此可以看出,git新建分支是多么的轻松。国外网友称作lightweight。
HEAD仍然处于游离态,因为HEAD还是直接指向a3 commit。
6、切换到一个分支
~/alpha $ git checkout master
Switched to branch 'master'
用户切换到master分支上。这个过程分四步走:1、git通过master找到a2提交,得到a2所指的树图。2、git将树图中包含的文件全部写入到对应工作区中,这会使data/number.txt的内容变为2。3、更新索引文件,这会使index文件中的data/number.txt对应的项的哈希值有所改变,变为内容为2的二进制哈希值。4、git将HEAD指向master,其实就是将master文件的内容变为:
ref: refs/heads/master
7、合并分支。
这里说的合并分支一般指的是从子孙分支向祖先分支合并。如果反过来,git什么都不会做的。
~/alpha $ git merge deputy
Fast-forward
向master分支中合并deputy分支。git发现a2是a3的祖先,就进行快速合并:将a3的树图中的文件写入到工作区中,更新index,将master指向a3。
8、合并两个来自不同分支的提交
~/alpha $ printf '4' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a4'
[master 7b7bd9a] a4
这是在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分支的一个提交。
图就是这个样子的:
~/alpha $ git merge master -m 'b4'
Merge made by the 'recursive' strategy.
这个过程有点复杂,但是跟前面讲过的原理差不多,只不过步数多了一些。直接看图吧:
~/alpha $ git checkout master
Switched to branch 'master'
~/alpha $ git merge deputy
Fast-forward
~/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
~/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
~/alpha $ git merge deputy
CONFLICT in data/number.txt
Automatic merge failed; fix conflicts and
commit the result.
出现冲突:
<<<<<<< HEAD
6
=======
5
>>>>>>> deputy
这时,索引文件看起来这个样子:
0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
1 data/number.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767
2 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
3 data/number.txt 7813681f5b41c028345ca62a2be376bae70b7f61
由于冲突(多个版本)造成了/data/number.txt有多个索引
处理冲突:
~/alpha $ printf '11' > data/number.txt
~/alpha $ git add data/number.txt
现在index文件就好了:
0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/number.txt 9d607966b721abde8931ddd052181fae905db503
~/alpha $ git commit -m 'b11'
[master 251a513] b11
9、删除文件
下面这个图包括了提交历史,树,二进制文件,最后一次提交,工作区和索引
~/alpha $ git rm data/letter.txt
rm 'data/letter.txt'
~/alpha $ git commit -m '11'
[master d14c7d2] 11
10、连接运程仓库
~ $ cd alpha
~/alpha $ git remote add 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 $ cd ../alpha
~/alpha $ git fetch bravo master
Unpacking objects: 100%
From ../bravo
* branch master -> FETCH_HEAD
这个过程大致是:将12的二进制文件拷贝到alpha/.git/objects/目录下,设置ref文件alpha/.git/refs/remotes/bravo/master的内容为12这个提交的哈希值,并且设置文件alpha/.git/FETCH_HEAD的内容为:
94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo
这行表示通过fetch命令获得的12提交是从bravo得到的。
11、合并FETCH_HEAD
~/alpha $ git merge FETCH_HEAD
Updating d14c7d2..94cd04d
Fast-forward