内容简介:Git是程序员工作中使用频率非常高的工具,要提高日常的工作效率,就需要熟练掌握Git的使用方法。相对于传统的版本控制系统而言,Git更为强大和灵活,其各种命令和命令参数也非常多,如果不了解Git的内部原理,要把Git使用得顺手的话非常困难。本文将用一个具体的例子来帮助理解Git的内部存储原理,加深对Git的理解,从掌握各种Git命令,以在使用Git进行工作时得心应手。Git的本质是一个文件系统,其工作目录中的所有文件的历史版本以及提交记录(Commit)都是以文件对象的方式保存在.git目录中的。首先创建一
Git是 程序员 工作中使用频率非常高的工具,要提高日常的工作效率,就需要熟练掌握Git的使用方法。相对于传统的版本控制系统而言,Git更为强大和灵活,其各种命令和命令参数也非常多,如果不了解Git的内部原理,要把Git使用得顺手的话非常困难。本文将用一个具体的例子来帮助理解Git的内部存储原理,加深对Git的理解,从掌握各种Git命令,以在使用Git进行工作时得心应手。
Git 目录结构
Git的本质是一个文件系统,其工作目录中的所有文件的历史版本以及提交记录(Commit)都是以文件对象的方式保存在.git目录中的。
首先创建一个work目录,并采用git init命令初始化Git仓库。该命令会在工作目录下生成一个.git目录,该目录将用于保存工作区中所有的文件历史的历史版本,提交记录,branch,tag等信息。
$ mkdir work $ cd work $ git init
其目录结构如下:
├── branches 不这么重要,暂不用管 ├── config git配置信息,包括用户名,email,remote repository的地址,本地branch和remote | branch的follow关系 ├── description 该git库的描述信息,如果使用了GitWeb的话,该描述信息将会被显示在该repo的页面上 ├── HEAD 工作目录当前状态对应的commit,一般来说是当前branch的head,HEAD也可以通过git checkout 命令被直接设置到一个特定的commit上,这种情况被称之为 detached HEAD ├── hooks 钩子程序,可以被用于在执行git命令时自动执行一些特定操作,例如加入changeid │ ├── 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 保存git对象的目录,包括三类对象commit,tag, tree和blob │ ├── info │ └── pack └── refs 保存branch和tag对应的commit ├── heads branch对应的commit └── tags tag对应的commit
Git Object存储方式
目前Object目录中还没有任何内容,我们创建一个文件并提交。
$ echo "my project" > README $ echo "hello world" > src/file1.txt $ git add . $ git commit -sm "init commit" [master (root-commit) b767d71] init commit 2 files changed, 2 insertions(+) create mode 100644 README create mode 100644 src/file1.txt
从打印输出可以看到,上面的命令创建了一个Commit对象,该Commit包含两个文件。 查看.git/objects目录,可以看到该目录下增加了5个子目录 06,3b, 82, b7, ca,每个子目录下有一个以一长串字母数字命令的文件。
.git/objects ├── 06 │ └── 5bcad11008c5e958ff743f2445551e05561f59 ├── 3b │ └── 18e512dba79e4c8300dd08aeb37f8e728b8dad ├── 82 │ └── 424451ac502bd69712561a524e2d97fd932c69 ├── b7 │ └── 67d7115ef57666c9d279c7acc955f86f298a8d ├── ca │ └── 964f37599d41e285d1a71d11495ddc486b6c3b ├── info └── pack
说明:Git Object目录中存储了三种对象:Commit, tree和blob。Git为对象生成一个文件,并根据文件信息生成一个SHA-1哈希值作为文件内容的校验和,创建以该校验和前两个字符为名称的子目录,并以(校验和)剩下38个字符为文件命名,将该文件保存至子目录下。
查看Git Object存储内容
通过git cat-file命令可以查看Git Object中存储的内容及对象类型,命令参数为Git Object的SHA-1哈希值,即目录名+文件名。在没有歧义的情况下,不用输入整个Hash,输入前几位即可。
当前分支的对象引用保存在HEAD文件中,可以查看该文件得到当前HEAD对应的branch,并通过branch查到对应的commit对象。
$ cat .git/HEAD ref: refs/heads/master cat .git/refs/heads/master b767d7115ef57666c9d279c7acc955f86f298a8d
使用-t参数查看文件类型:
$ git cat-file -t b767d7 commit
使用-p参数可以查看文件内容:
$ git cat-file -p b767d7 tree ca964f37599d41e285d1a71d11495ddc486b6c3b author Huabing Zhao <zhaohuabing@gmail.com> 1548055516 +0800 committer Huabing Zhao <zhaohuabing@gmail.com> 1548055516 +0800 init commit Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
可以看出这是一个Commit对象,Commit对象中保存了Commit的作者,Commit的描述信息,签名信息以及该Commit中包含哪些tree对象和blob对象。
b767d7这个Commit中保存了一个tree对象,可以把该tree对象看成这次提交相关的所有文件的根目录。让我们来看看该tree对象中的内容。
$ git cat-file -p ca964f 100644 blob 065bcad11008c5e958ff743f2445551e05561f59 README 040000 tree 82424451ac502bd69712561a524e2d97fd932c69 src
可以看到该tree对象中包含了一个blob对象,即README文件;和一个tree对象,即src目录。 分别查看该blob对象和tree对象,其内容如下:
$ git cat-file -p 065bca my project $ git cat-file -p 824244 100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad file1.txt
查看file1.txt的内容。
$ git cat-file -p 3b18e51 hello world
从上面的实验我们可以得知,Git中存储了三种类型的对象,commit,tree和blob。分别对应git commit,此commit中的目录和文件。这些对象之间的关系如下图所示。
HEAD---> refs/heads/master--> b767d7(commit)
+
|
v
ca964f(tree)
+
|
+---------+----------+
| |
v v
065bca(blob) 824244(tree)
README src
+
|
v
3b18e5(blob)
file1.txt
Git branch和tag
从refs/heads/master的内容可以看到,branch是一个指向commit的指针,master branch实际是指向了b767d7这个commit。
$ git checkout -b work Switched to a new branch 'work' $ tree .git/refs/ .git/refs/ ├── heads │ ├── master │ └── work └── tags $ cat .git/refs/heads/work .git/refs/heads/master b767d7115ef57666c9d279c7acc955f86f298a8d b767d7115ef57666c9d279c7acc955f86f298a8d
上面的命令创建了一个work branch。从其内容可以看到,该branch并没有创建任何新的版本文件,和master一样指向了b767d7这个commit。
从上面的实验可以看出,一个branch其实只是一个commit对象的应用,Git并不会为每个branch存储一份拷贝,因此在git中创建branch几乎没有任何代价。
在work branch上进行一些修改,然后提交。
$ echo "new line" >> src/file1.txt $ echo "do nothing" >> Makefile $ git commit -sm "some change" [work 4f73993] some change 2 files changed, 2 insertions(+) create mode 100644 Makefile
查看当前的HEAD和branch内容。
$ cat .git/HEAD ref: refs/heads/work huabing@huabing-xubuntu:~/work$ cat .git/refs/heads/work .git/refs/heads/master 4f73993cf81931bc15375f0a23d82c40b3ae6789 b767d7115ef57666c9d279c7acc955f86f298a8d
可以看到HEAD指向了work branch,而work branch则指向了4f73993这个commit,master branch指向的commit未变化,还是b767d7。
查看4f73993这个commit对象的内容。
$ git cat-file -p 4f73993 tree 082b6d87eeddb15526b7c920e21f09f950f78b54 parent b767d7115ef57666c9d279c7acc955f86f298a8d author Huabing Zhao <zhaohuabing@gmail.com> 1548069325 +0800 committer Huabing Zhao <zhaohuabing@gmail.com> 1548069325 +0800 some change Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
可以看到commit有一个parent字段,指向了前一个commi b767d7。该commit也包含了一个tree对象,让我们看看其中的内容。
$ git cat-file -p 082b6d 100644 blob 8cc95f278445722c59d08bbd798fbaf60da8ca14 Makefile 100644 blob 065bcad11008c5e958ff743f2445551e05561f59 README 040000 tree 9aeacd1fa832ca167b0f72fb1d0c744a9ee1902f src $ git cat-file -p 9aeacd 100644 blob 79ee69e841a5fd382faef2be2f2eb6e836cc980a file1.txt
可以看到该tree对象中包含了该版本的所有文件和目录,由于README没有变化,还是指向的065bca这个blob对象。Makefile是一个新建的blob对象,src和file1.txt则指向了新版本的对象。
增加了这次commit后,Git中各个对象的关系如下图所示:
(parent)
HEAD--> refs/heads/work--> 4f7399(commit) +-------> b767d7(commit)<---refs/heads/master
+ +
| |
v v
082b6d(tree) ca964f(tree)
+ +
| |
+-----------------------------+ +--------+-----------+
| | | | |
v v v v v
9aeacd(tree) 8cc95f(blob) 065bca(blob) 824244(tree)
src (version 2) Makefile README src (version 1)
+ +
| |
v v
79ee69(blob) 3b18e5(blob)
file1.txt (version 2) file1.txt (version 1)
从上图可以看到,Git会为每次commit时修改的目录/文件生成一个新的版本的tree/blob对象,如果文件没有修改,则会指向老版本的tree/blob对象。而branch则只是指向某一个commit的一个指针。即Git中整个工作目录的version是以commit对象的形式存在的,可以认为一个commit就是一个version,而不同version可以指向相同或者不同的tree和blob对象,对应到不同版本的子目录和文件。如果某一个子目录/文件在版本间没有变化,则不会为该子目录/文件生成新的tree/blob对象,不同version的commit对象会指向同一个tree/object对象。
Tag和branch类似,也是指向某个commit的指针。不同的是tag创建后其指向的commit不能变化,而branch创建后,其指针会在提交新的commit后向前移动。
$ git tag v1.0 $ cat .git/refs/tags/v1.0 .git/refs/heads/work 4f73993cf81931bc15375f0a23d82c40b3ae6789 4f73993cf81931bc15375f0a23d82c40b3ae6789
可以看到新创建的v1.0 tag和work branch都是指向了4f7399这个commit。
Git Stash实现原理
Git stash的功能说明:经常有这样的事情发生,当你正在进行项目中某一部分的工作,里面的东西处于一个比较杂乱的状态,而你想转到其他分支上进行一些工作。问题是,你不想提交进行了一半的工作,否则以后你无法回到这个工作点。解决这个问题的办法就是git stash命令。
“‘储藏”“可以获取你工作目录的中间状态——也就是你修改过的被追踪的文件和暂存的变更——并将它保存到一个未完结变更的堆栈中,随时可以重新应用。
Git是如何实现Stash的呢?理解了Commit, Tree, Blog这三种Git存储对象,我们就可以很容易理解Git Stash的实现原理。因为和bransh及tag类似,Git Stash其实也是通过Commit来实现的。
通过实验来测试一下:
$ echo "another line" >> src/file1.txt $ git stash
通过上面的命令,我们在file1.txt中增加了一行,然后通过git stash命令将这些改动“暂存”在了一个“堆栈”中,让我们来看看.git目录发生了什么变化。
$ tree .git/
.git/
├── branches
├── COMMIT_EDITMSG
├── 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
├── index
├── info
│ └── exclude
├── logs
│ ├── HEAD
│ └── refs
│ ├── heads
│ │ ├── master
│ │ └── work
│ └── stash
├── objects
│ ├── 06
│ │ └── 5bcad11008c5e958ff743f2445551e05561f59
│ ├── 08
│ │ └── 2b6d87eeddb15526b7c920e21f09f950f78b54
│ ├── 11
│ │ └── a6d1031e4fa2d4da0b6303dd74ed8e85c54057
│ ├── 33
│ │ └── f98923002cd224dabf32222c808611badd6d48
│ ├── 3b
│ │ └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
│ ├── 4f
│ │ └── 73993cf81931bc15375f0a23d82c40b3ae6789
│ ├── 6a
│ │ ├── 1474c4da0653af0245970997b6fab0a0a7c1df
│ │ └── d88760c3be94d8cb582bf2d06b99083d034428
│ ├── 75
│ │ └── e170cc1d928ae5a28547b4a3f2f3394a675b9a
│ ├── 79
│ │ └── ee69e841a5fd382faef2be2f2eb6e836cc980a
│ ├── 82
│ │ └── 424451ac502bd69712561a524e2d97fd932c69
│ ├── 8c
│ │ └── c95f278445722c59d08bbd798fbaf60da8ca14
│ ├── 90
│ │ └── c43dbb1e71c271510994d6b147c425cbffa673
│ ├── 9a
│ │ └── eacd1fa832ca167b0f72fb1d0c744a9ee1902f
│ ├── b7
│ │ └── 67d7115ef57666c9d279c7acc955f86f298a8d
│ ├── ca
│ │ └── 964f37599d41e285d1a71d11495ddc486b6c3b
│ ├── e8
│ │ └── 83e779eb08e2d9bca1fc1ee722fc80addac312
│ ├── info
│ └── pack
├── ORIG_HEAD
└── refs
├── heads
│ ├── master
│ └── work
├── stash
└── tags
└── v1.0
可以看到objects目录中增加了一些对象文件,refs中增加了一个stash文件。通过命令查看该文件内容:
$ cat .git/refs/stash 11a6d1031e4fa2d4da0b6303dd74ed8e85c54057 $ git cat-file -p 11a6 tree 90c43dbb1e71c271510994d6b147c425cbffa673 parent 4f73993cf81931bc15375f0a23d82c40b3ae6789 parent 6a1474c4da0653af0245970997b6fab0a0a7c1df author Huabing Zhao <zhaohuabing@gmail.com> 1548326421 +0800 committer Huabing Zhao <zhaohuabing@gmail.com> 1548326421 +0800 WIP on work: 4f73993 some change $ git cat-file -p 90c4 100644 blob 8cc95f278445722c59d08bbd798fbaf60da8ca14 Makefile 100644 blob 065bcad11008c5e958ff743f2445551e05561f59 README 040000 tree 33f98923002cd224dabf32222c808611badd6d48 src $ git cat-file -p 33f9 100644 blob 75e170cc1d928ae5a28547b4a3f2f3394a675b9a file1.txt $ git cat-file -p 75e1 hello world new line another line
从命令行输出可以看到,git stash实际上创建了一个新的commit对象11a6d1,该commit对象的父节点为4f7399。commit对象中包含了修改后的file1.txt blob对象75e170。通过git log可以查看:
$ git log --oneline --graph stash@{0}
* f566001 WIP on work: 4f73993 some change
|\
| * 0796ced index on work: 4f73993 some change
|/
* 4f73993 some change
* b767d71 init commit
备注:git stash生成的commit对象有两个parent,一个是前面一次git commit命令生成的commit,另一个对应于保存到stage中的commit。
从该试验可以得知,git stash也是以commit,tree和object对象实现的。Git stash保存到“堆栈”中的修改其实一个commit对象。
Git reset实现原理
在进行一些改动以后并通过git commit 将改动的代码提交到本地的repo后,如果你测试发现刚才的改动不合理,希望回退刚才的改动,应该如何处理?
我们先提交一个错误的改动:
$ echo "I did something wrong" >> src/file1.txt $ git add . $ git commit -sm "This commit should not be there" [work ccbc363] This commit should not be there 1 file changed, 1 insertion(+)
你可以通过git revert回退刚才的改动,或者修改代码后再次提交,但这样的话你的提交log会显得非常凌乱;如果不想把中间过程的commit push到远程仓库,可以通过git reset 回退刚才的改动。
先查看目前的log:
$ git log commit ccbc3638142191bd68454d47a0f67fd12519806b Author: Huabing Zhao <zhaohuabing@gmail.com> Date: Fri Jan 25 12:35:31 2019 +0800 This commit should not be there Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com> commit 4f73993cf81931bc15375f0a23d82c40b3ae6789 Author: Huabing Zhao <zhaohuabing@gmail.com> Date: Mon Jan 21 19:15:25 2019 +0800 some change Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com> commit b767d7115ef57666c9d279c7acc955f86f298a8d Author: Huabing Zhao <zhaohuabing@gmail.com> Date: Mon Jan 21 15:25:16 2019 +0800 init commit Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
通过git reset回退到上一个commit。注意这里HEAD是一个指向当前branch最后一个commit指针,因此HEAD~1表示之前的一个commit。git reset命令也可以直接使用commit号作为命令参数。
$ git reset HEAD~1 Unstaged changes after reset: M src/file1.txt $ git log commit 4f73993cf81931bc15375f0a23d82c40b3ae6789 Author: Huabing Zhao <zhaohuabing@gmail.com> Date: Mon Jan 21 19:15:25 2019 +0800 some change Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com> commit b767d7115ef57666c9d279c7acc955f86f298a8d Author: Huabing Zhao <zhaohuabing@gmail.com> Date: Mon Jan 21 15:25:16 2019 +0800 init commit Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
可以看到刚才的commit被回退了,但修改的文件还存在,处于Unstaged状态,你可以对这些文件进行改动后再次提交。
如果你不想保留修改的文件,可以使用–hard参数直接回退到指定的commit,该参数会将HEAD指向该commit,并且工作区中的文件也会和该comit保持一致,该commit后的修改会被直接丢弃。
$ git reset HEAD --hard HEAD is now at 4f73993 some change $ git status On branch work nothing to commit, working directory clean
Git object存储方式
Git object是通过下面的方式处理并存储在Git内部的文件系统中的:
- 首先创建一个header,header的值为 “对象类型 内容长度\0”
- 将header和文件内容连接起来,计算得到其SHA-1 hash值
- 将连接得到的内容采用zlib压缩
- 将压缩后的内容写入到以 “hash值前两位命令的目录/hash值后38位命令的文件” 中
可以通过 Ruby 手工创建一个Git object来验证上面的步骤。
$ irb
irb(main):001:0> content = "what is up, doc?" //文件内容
=> "what is up, doc?"
irb(main):002:0> header = "blob #{content.length}\0" //创建header
=> "blob 16\u0000"
irb(main):003:0> store = header + content //拼接header和文件内容
=> "blob 16\u0000what is up, doc?"
irb(main):004:0> require 'digest/sha1'
=> true
irb(main):005:0> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37" //计算得到hash值
irb(main):006:0> require 'zlib'
=> true
irb(main):007:0> zlib_content = Zlib::Deflate.deflate(store) //压缩header+文件内容
=> "x\x9CK\xCA\xC9OR04c(\xCFH,Q\xC8,V(-\xD0QH\xC9O\xB6\a\x00_\x1C\a\x9D"
irb(main):008:0> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
=> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37" //通过hash值计算文件存储路径
irb(main):009:0> require 'fileutils'
=> true
irb(main):010:0> FileUtils.mkdir_p(File.dirname(path)) //写文件
=> [".git/objects/bd"]
irb(main):011:0> File.open(path, 'w') { |f| f.write zlib_content }
=> 32
irb(main):012:0>
文件以及写入到Git的内部存储中,我们尝试通过git cat-file验证并读取该文件内容:
$ git cat-file -p bd9dbf5aae1a3862dd1526723246b20206e5fc37 what is up, doc?
可以看到,可以通过git cat-file文件读取该文件内容,因此该文件是一个合法的git object,和通过git 命令写入的文件格式相同。
总结
Git围绕三种Object来实现了版本控制以及Branch,Tag等机制。
- Commit:Commit可以看作Git中一个Version的所有目录和文件的Snapshot,可以通过git checkout查看任意一个commit中的内容。
- Tree:目录对象,内部包含目录和文件
- Blob:文件对象,对应一个文件
理解了Git object的存储机制,就可以理解Git的各个命令的实现原理,更好地使用Git来实现源代码管理。
参考: https://git-scm.com/book/en/v2 ... jects
原文链接: Git内部存储原理 (作者:赵化冰)
以上所述就是小编给大家介绍的《Git内部存储原理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
HTML & CSS设计与构建网站
[美] Jon Duckett / 刘涛、陈学敏 / 清华大学出版社 / 2013-1 / 59.80元
欢迎您选择一种更高效的学习HTML和CSS的方式。不管您设计和建立新网站,还是想更好地控制现有网站,都可以在《HTML & CSS 设计与构建网站》一书的指导下创建出用户友好、令用户赏心悦目的Web内容。我们知道,编码是一项令人望而生畏的工作,而本书却采用有别于许多传统编程书籍的新颖编排方式,将使您收到事半功倍的学习效果。 每一页都在短小精悍的示例代码的引导下,简明直观、直截了当地阐述一个新......一起来看看 《HTML & CSS设计与构建网站》 这本书的介绍吧!