说起 Git, 相信大家都很熟悉了, 毕竟作为程序猿, 每天的业余时间除了吃饭睡觉就是逛一下全世界最大的开 (tong) 源(xing)代 (jiao) 码(you)网站 GitHub 了. 在那里 Git 是每个人所要具备的最基本的技能. 今天我们不聊 Git 的基本应用, 来聊一聊 Git 的原理. <!-- more -->
Git 给自己的定义是一套内存寻址文件系统, 当你在一个目录下执行 git init 命令时, 会生成一个. git 目录, 它的目录结构是这样的:
- .git/
- branches
- config
- description
- HEAD
- hooks
- applypatch-msg.sample
- commit-msg.sample
- post-update.sample
- pre-applypatch.sample
- pre-commit.sample
- prepare-commit-msg.sample
- pre-push.sample
- pre-rebase.sample
- update.sample
- info
- exclude
- objects
- info
- pack
- refs
- heads
- tags
其中 branches 目录已经不再使用, description 文件仅供 Gitweb 程序使用, config 文件保存了项目的配置.
需要我们重点关注的是 HEAD 和 index 文件以及 objects 和 refs 目录. 其中 index 中保存了暂存区的一些信息, 这里不做过多介绍.
objects 目录
这个目录是用来存储 Git 对象的(包括 tree 对象, commit 对象和 blob 对象), 对于一个初始的 Git 仓库, objects 目录下只有 info 和 pack 两个子目录, 并没有常规文件. 随着项目的进行, 我们创建的文件, 以及一些操作记录, 都会作为 Git 对象被存储在这个目录下.
在该目录下, 所有对象都会生成一个文件, 并且有对应的 SHA-1 校验和, Git 会创建以校验和前两位为名称的子目录, 并以剩下的 38 位为名称来保存文件.
接下来让我们一起看一下当我们进行一次提交时, Git 具体做了哪些事情.
- $ echo 'test content'>test.txt
- $ git add .
执行上述命令后, objects 目录结构如下:
- .git/objects/
- d6
- 70460b4b4aece5915caf5c68d12f560a9fe3e4
- info
- pack
这里多了一个文件夹, 如上面所述, 这个就是 Git 为我们创建的一个对象, 我们可以使用底层命令来看一下这个对象的类型以及它存储的是什么.
- $ git cat-file -t d670460b4b4aece5915caf5c68d12f560a9fe3e4
- blob
- $ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
- test content
可以看到, 这是一个 blob 对象, 存储内容就是我们刚刚创建的文件的内容. 接下来继续执行提交操作.
- $ git commit -m 'test message'
- [master (root-commit) 2b00dca] test message
- 1 file changed, 1 insertion(+)
- create mode 100644 test.txt
- $ tree .git/objects/
- .git/objects/
- 2b
- 00dcae50af70bb5722033b3fe75281206c74da
- 80
- 865964295ae2f11d27383e5f9c0b58a8ef21da
- d6
- 70460b4b4aece5915caf5c68d12f560a9fe3e4
- info
- pack
此时 objects 目录下又多了两个对象. 再用 cat-file 命令来查看一下这两个文件.
- $ git cat-file -t 2b00dcae50af70bb5722033b3fe75281206c74da
- commit
- $ git cat-file -p 2b00dcae50af70bb5722033b3fe75281206c74da
- tree 80865964295ae2f11d27383e5f9c0b58a8ef21da
- author jackeyzhe <jackeyzhe59@163.com> 1534670725 +0800
- committer jackeyzhe <jackeyzhe59@163.com> 1534670725 +0800
- test message
- $ git cat-file -t 80865964295ae2f11d27383e5f9c0b58a8ef21da
- tree
- $ git cat-file -p 80865964295ae2f11d27383e5f9c0b58a8ef21da
- 100644 blob d670460b4b4aece5915caf5c68d12f560a9fe3e4 test.txt
可以看到一个是 commit 对象, 一个是 tree 对象. commit 对象通常包括 4 部分内容:
工作目录快照的 Hash, 即 tree 的值
提交的说明信息
提交者的信息
父提交的 Hash 值
由于我是第一次提交, 所以这里没有父提交的 Hash 值.
tree 对象可以理解为 UNIX 文件系统中的目录, 保存了工作目录的 tree 对象和 blob 对象的信息. 接下来我们再来看一下 Git 是如何进行版本控制的.
- echo 'version1'>version.txt
- $ git add .
- $ git commit -m 'first version'
- [master 702193d] first version
- 1 file changed, 1 insertion(+)
- create mode 100644 version.txt
- $ echo 'version2'>version.txt
- $ git add .
- $ git commit -m 'second version'
- [master 5333a75] second version
- 1 file changed, 1 insertion(+), 1 deletion(-)
- $ tree .git/objects/
- .git/objects/
- 1f
- a5aab2a3cf025d06479b9eab9a7f66f60dbfc1
- 29
- 13bfa5cf9fb6f893bec60ac11d86129d56fcbe
- 2b
- 00dcae50af70bb5722033b3fe75281206c74da
- 53
- 33a759c4bdcdc6095b4caac19743d9445ca516
- 5b
- dcfc19f119febc749eef9a9551bc335cb965e2
- 70
- 2193d62ffd797155e4e21eede20897890da12a
- 80
- 865964295ae2f11d27383e5f9c0b58a8ef21da
- d6
- 70460b4b4aece5915caf5c68d12f560a9fe3e4
- df
- 7af2c382e49245443687973ceb711b2b74cb4a
- info
- pack
- $ git cat-file -p 1fa5aab2a3cf025d06479b9eab9a7f66f60dbfc1
- 100644 blob d670460b4b4aece5915caf5c68d12f560a9fe3e4 test.txt
- 100644 blob 5bdcfc19f119febc749eef9a9551bc335cb965e2 version.txt
- $ git cat-file -p 2913bfa5cf9fb6f893bec60ac11d86129d56fcbe
- 100644 blob d670460b4b4aece5915caf5c68d12f560a9fe3e4 test.txt
- 100644 blob df7af2c382e49245443687973ceb711b2b74cb4a version.txt
Git 将没有改变的文件的 Hash 值直接存入 tree 对象, 对于有修改的文件, 则会生成一个新的对象, 将新的对象存入 tree 对象. 我们再来看一下 commit 对象的信息.
- $ git cat-file -p 5333a759c4bdcdc6095b4caac19743d9445ca516
- tree 2913bfa5cf9fb6f893bec60ac11d86129d56fcbe
- parent 702193d62ffd797155e4e21eede20897890da12a
- author jackeyzhe <jackeyzhe59@163.com> 1534672270 +0800
- committer jackeyzhe <jackeyzhe59@163.com> 1534672270 +0800
- second version
- $ git cat-file -p 702193d62ffd797155e4e21eede20897890da12a
- tree 1fa5aab2a3cf025d06479b9eab9a7f66f60dbfc1
- parent 2b00dcae50af70bb5722033b3fe75281206c74da
- author jackeyzhe <jackeyzhe59@163.com> 1534672248 +0800
- committer jackeyzhe <jackeyzhe59@163.com> 1534672248 +0800
- first version
此时的 commit 对象已经有 parent 信息了, 这样我们就可以顺着 parent 一步步往回进行版本回退了. 不过这样是比较麻烦的, 我们一般习惯用的是 git log 查看提交记录.
refs 目录
在介绍 refs 目录之前, 我们还是先来看一下该目录结构
- $ tree .git/refs/
- .git/refs/
- heads
- master
- tags
- 2 directories, 1 file
- $ cat .git/refs/heads/master
- 5333a759c4bdcdc6095b4caac19743d9445ca516
在一个刚刚被初始化的 Git 仓库中, refs 目录下只有 heads 和 tags 两个子目录, 由于我们刚刚有过提交操作, 所以 git 为我们自动生成了一个名为 master 的引用. master 的内容是最后一次提交对象的 Hash 值. 看到这里大家一定在想, 如果我们对每次提交都创建一个这样的引用, 不就不需要记住每次提交的 Hash 值了, 只要看看引用的值, 复制过来就可以退回到对应版本了. 没错, 这样是可以方便的退回, 但是这样做的意义不大, 因为我们并不需要频繁的退回, 特别是比较古老的版本, 退回的概率更是趋近于 0.Git 用这个引用做了更有意义的事, 那就是分支.
当我新建一个分支时, git 就会在. git/refs/heads 目录下新建一个文件. 当然新建的引用还是指向当前工作目录的最后一次提交, 一般情况下我们不会主动去修改这些引用文件, 不过如果一定要修改, Git 为我们提供了一个 update-ref 命令. 可以改变引用的值, 使其指向不同的 commit 对象.
tags 目录下的文件存储的是标签对应的 commit, 当为某次提交打上一个 tag 时, tags 目录下就会被创建出一个命名为 tag 名的文件, 值是此次提交的 Hash 值.
HEAD
新建分支的时候, Git 是怎么知道我们当前是在哪个分支的, Git 又是如何实现分支切换的呢? 答案就在 HEAD 这个文件中.
- $ cat .git/HEAD
- ref: refs/heads/master
- $ git checkout test
- Switched to branch 'test'
- $ cat .git/HEAD
- ref: refs/heads/test
很明显, HEAD 文件存储的就是我们当前分支的引用, 当我们切换分支后再次进行提交操作时, Git 就会读取 HEAD 对应引用的值, 作为此次 commit 的 parent. 我们也可以通过 symbolic-ref 命令手动设置 HEAD 的值, 但是不能设置 refs 以外的形式.
Packfiles
到这里我们在文章开头所说的重点关注的目录和文件都介绍完毕了. 但是作为一个文件系统, 还存在一个问题, 那就是空间. 前文介绍过, 当文件修改后进行提交时, Git 会创建一份新的快照. 这样长久下去, 必定会占用很大的存储空间. 而比较古老的版本的价值已经不大, 所以要想办法清理出足够的空间供用户使用.
好消息是, Git 拥有自己的 gc(垃圾回收)方法. 当仓库中有太多松散对象时, Git 会调用 git gc 命令(当然我们也可以手动调用这个命令), 将这些对象进行打包. 打包后会出现两个新文件: 一个 idx 索引文件和一个 pack 文件. 索引文件包含了 packfile 的偏移信息, 可以快速定位到文件. 打包后, 每个文件最新的版本的对象存的是完整的文件内容. 而之前的版本只保存差异. 这样就达到了压缩空间的目的.
Ending
本文只介绍了 Git 的原理, 如果对 Git 基本操作不熟悉的话, 可以点击阅读原文学习 Pro Git .
来源: http://www.tuicool.com/articles/22yyeaU