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

未完,可能会续~

相关文章

  • 01-git基础知识查漏补缺

    1、git的几种状态 2、git add 和 git commit 两个命令的背后的原理 3、基于git log ...

  • Git命令的背后

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

  • Github学习文档-3

    目录 1.Git 的基本的命令git init命令git status命令git add命令git commit命...

  • git实用命令

    git实用命令 1 git init 命令 初始化 git 仓库 repository 2 git add 命令 ...

  • git管理工具

    git分支命令 git提交命令

  • git 多条命令一次执行

    上代码git命令 && git命令

  • git入门

    这里对git init、git add、git commit命令进行一个总结。 $ git init 命令,该命令...

  • GIT 常用命令总结

    GIT 常用命令总结 GIT 初始化命令 命令描述git init初始化本地 git 仓库git config -...

  • Git命令整理

    Git命令 ———————————————— git配置: git基本步骤: git分支管理: 创建分支命令: 切...

  • Git常用命令整合

    Git命令 git init 在命令行输入git init,会在当前的目录创建新仓库。 git clone 在命令...

网友评论

    本文标题:Git命令的背后

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