Git命令的背后

作者: yjiyjige | 来源:发表于2016-08-28 02:41 被阅读543次

    git init

    使用git init初始化一个新的目录时,会生成一个.git的目录,该目录即为本地仓库。一个新初始化的本地仓库是这样的:

    ├── HEAD
    ├── branches
    ├── config
    ├── description
    ├── hooks
    ├── objects
    │   ├── info
    │   └── pack
    └── refs
        ├── heads
        └── tags
    
    • description 用于GitWeb程序
    • config 配置特定于该仓库的设置(还记得git config的三个配置级别么)
    • hooks 放置客户端或服务端的hook脚本
    • HEAD 传说中的HEAD指针,指明当前处于哪个分支
    • objects Git对象存储目录
    • refs Git引用存储目录
    • branches 放置分支引用的目录

    其中descriptionconfighooks这些不在讨论中,后文会直接忽略。

    git add

    Gitcommit之前先要通过git add添加文件,这个操作Git内部会做些什么呢?

    执行如下操作:

    • echo "Hello Git" > a.txt生成一个a.txt
    • 再通过git add a.txt添加文件
    • 查看.git目录
    ├── HEAD
    ├── branches
    ├── index
    ├── objects
    │   ├── 9f
    │   │   └── 4d96d5b00d98959ea9960f069585ce42b1349a
    │   ├── info
    │   └── pack
    └── refs
        ├── heads
        └── tags
    

    可以看到,多了一个index文件。并且在objects目录下多了一个9f的目录,其中多了一个4d96d5b00d98959ea9960f069585ce42b1349a文件。

    其实9f4d96d5b00d98959ea9960f069585ce42b1349a就是一个Git对象,称为blob对象

    这个文件名(或者叫对象名)是怎样来的呢?简单的说,就是Git会先生成一个文件头,其中包含这个对象的类型(比如blob)和原始文件长度加上一个空字节。文件头再加上原始文件内容,然后算出一个SHA-1。这个SHA-1有40位,前两位会用于新建目录,后38位用于文件名。所以,完整的对象名应该把上一级目录名给包含进去的。

    可以通过Git的底层命令git cat-file -p查看其内容:

    $ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349a
    Hello Git
    

    可以看到,其中的内容和a.txt文件是一模一样的。

    通过git cat-file -t查看对象的类型:

    $ git cat-file -t 9f4d96d5b00d98959ea9960f069585ce42b1349a
    blob
    

    确实是blob类型。那index文件又是什么鬼?

    小结:

    • tree对象相当于一个目录(或者叫文件夹),其中包含blob对象和其他tree对象。
    • 每一次提交都会有一个commit对象,commit对象中会有一个tree对象和一个指和上一次提交的引用。
    • master分支其实就是一个引用而已,指向某一个提交对象。

    Q&A

    怎么理解每次提交都是一个“快照”

    从上文中我们可能看到,每一个commit对象所引用的tree对象最终可以递归得出提交时的所有的文件,并不是说会把所有的文件都重新备份一次。而Git在add文件时,确实会把文件完整地保存成一个新的blob对象,我们可以验证:

    $ echo "Third" > a.txt
    
    $ git add a.txt
    
    $ git commit -m "third commit"
    

    会多几个对象呢?

    ├── HEAD
    ├── branches
    ├── index
    ├── logs
    │   ├── HEAD
    │   └── refs
    │       └── heads
    │           └── master
    ├── objects
    │   ├── 16
    │   │   └── df5eafaccb32649a890005b3f693fed266fc3d
    │   ├── 20
    │   │   └── d5b672a347112783818b3fc8cc7cd66ade3008
    │   ├── 56
    │   │   └── 9f012efac9a65ee515e488e244b89cbe795d6e
    │   ├── 80
    │   │   └── 0910d78c39017816173b00d3a1074800854612
    │   ├── 88
    │   │   └── 23efd7fa394844ef4af3c649823fa4aedefec5
    │   ├── 8e
    │   │   └── 19c6677af0c3a80d5e2a3d1c1dffe9934431a5
    │   ├── 91
    │   │   └── 0fc16f5cc5a91e6712c33aed4aad2cfffccb73
    │   ├── 9f
    │   │   ├── 4d96d5b00d98959ea9960f069585ce42b1349a
    │   │   └── 7da334be98d63c78ccf1e94414b0664e649e5f
    │   ├── e8
    │   │   └── b5b9a992fe8b5d24b09ef55b97739f35221b1d
    │   ├── info
    │   └── pack
    └── refs
        ├── heads
        │   └── master
        └── tags
    

    多了三个对象,直接通过master一步步看:

    $ cat .git/refs/heads/master
    569f012efac9a65ee515e488e244b89cbe795d6e
    
    $ git cat-file -p 569f012efac9a65ee515e488e244b89cbe795d6e
    tree 9f7da334be98d63c78ccf1e94414b0664e649e5f # 新的tree对象
    parent 800910d78c39017816173b00d3a1074800854612
    author yjiyjige <475500230@qq.com> 1472317420 +0800
    committer yjiyjige <475500230@qq.com> 1472317420 +0800
    
    third commit
    
    $ git cat-file -p 9f7da334be98d63c78ccf1e94414b0664e649e5f
    100644 blob 16df5eafaccb32649a890005b3f693fed266fc3d    a.txt # 文件名一样,但blob对象已经不一样了
    040000 tree e8b5b9a992fe8b5d24b09ef55b97739f35221b1d    temp # 和上次的tree对象是一样的
    
    $ git cat-file -p 16df5eafaccb32649a890005b3f693fed266fc3d
    Third
    
    $ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349a
    Hello Git
    # 可以看到老blob对象还在
    

    可以发现,新生成一个tree对象,指向了一个新的blob对象(还是对应于a.txt)只不过内容变了。原来的temp目录对应的tree对象没有变化,所以直接引用。

    等等,如果每次修改都保存一个完整的文件,那仓库不是很快就变得巨大?

    理论上来说,每次修改只需要保存这个文件diff就行了,但那样就实现不了Git这么优雅的设计了。Git是通过“打包”来实现的。我们调用git gc,然后看下仓库的文件:

    ├── HEAD
    ├── branches
    ├── index
    ├── logs
    │   ├── HEAD
    │   └── refs
    │       └── heads
    │           └── master
    ├── objects
    │   ├── info
    │   │   └── packs
    │   └── pack
    │       ├── pack-b25e184d1a96e5f1bde09c941be14cbe2cdb1289.idx
    │       └── pack-b25e184d1a96e5f1bde09c941be14cbe2cdb1289.pack
    ├── packed-refs
    └── refs
        ├── heads
        └── tags
    

    WTF!!!所有对象都不见了!甚至master都不见了!

    莫方,我们看看packed-refs是什么:

    $ cat packed-refs 
    # pack-refs with: peeled fully-peeled 
    569f012efac9a65ee515e488e244b89cbe795d6e refs/heads/master
    

    看来至少master还是在的。再通过git verify-pack -v看看.idx文件是什么东西:

    $ git verify-pack -v objects/pack/pack-b25e184d1a96e5f1bde09c941be14cbe2cdb1289.idx
    569f012efac9a65ee515e488e244b89cbe795d6e commit 215 147 12
    800910d78c39017816173b00d3a1074800854612 commit 216 148 159
    910fc16f5cc5a91e6712c33aed4aad2cfffccb73 commit 167 117 307
    16df5eafaccb32649a890005b3f693fed266fc3d blob   6 15 424
    20d5b672a347112783818b3fc8cc7cd66ade3008 blob   12 21 439
    9f7da334be98d63c78ccf1e94414b0664e649e5f tree   64 75 460
    e8b5b9a992fe8b5d24b09ef55b97739f35221b1d tree   33 44 535
    8e19c6677af0c3a80d5e2a3d1c1dffe9934431a5 tree   64 75 579
    9f4d96d5b00d98959ea9960f069585ce42b1349a blob   10 19 654
    8823efd7fa394844ef4af3c649823fa4aedefec5 tree   33 44 673
    non delta: 10 objects
    objects/pack/pack-b25e184d1a96e5f1bde09c941be14cbe2cdb1289.pack: ok
    

    原来.idx文件记录了之前的所有对象,而现在的数据保存在了.pack文件中。通过.idx文件记录的起始值、文件长度这些信息就可以把原有的对象提取出来了。如果文件相似,其实是会保留新版本,而老版本保留diff的形式存在!

    回到“快照”这个概念,Git在底层做了脏活,只要通过当时提交的文件对应的blob对象引用,就可以还原出原始文件。所以,从用户角度,blob文件相当于原始文件

    $ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349a
    Hello Git
    

    这部分不好理解,甚至很多书都会直接说“Git保留文件快照,而其他VCS是保存diff”。其实Git底层也会保存diff的,只不过我们感觉不到diff的存在而已。

    关于打包这部分,详细请见Pro git

    未完,可能会续~

    相关文章

      网友评论

        本文标题:Git命令的背后

        本文链接:https://www.haomeiwen.com/subject/nxmtettx.html