深入理解 Git

深入理解 Git

Git 使我们日常使用的开发工具,用于代码的版本管理,但是我们常用的各种命令 git add, git commit, git push, git pull 等等究竟是啥样子,带着好奇心,趁着空重新读了下 Git 的官方文档。发现了一些除了刚刚提到的高级命令以外的低级命令,以及 Git 究竟是如何运转的。(当然不是源码解读)

基本概念

git 对于文件的存储位置进行了3层分割,用于不同状态下的文件。我们可以理解为3个箱子。

  • Work Directory
  • Index/Stage
  • Git Directory

Work Directory(俗称工作区)

工作区就是我们文件正常编辑的地方,我们在这进行代码的编写。当我们完成之后,我们会发现我的的 cli 会提示我们这些文件被修改,从而方便我们区分哪些文件被修改了,哪些没有被修改。

Index/Stage(暂存区)

暂存区就是我们在执行了 git add 之后,文件存放的地方,说明这些文件准备进行提交了。之所以有这么个地方,就是能够将需要提交的内容进行一次性提交,从而避免了每次提交都存在歧义的情况。同时也方便我们对将要提交的地方进行调整。

Git Directory

Git 的区域,用于将历史记录进行统一的进行管理。方便后续迭代的时候进行一定的调整。git 中使用文件的形式进行管理。

前面所说可能比较抽象,用一个简单的话来说,当我们大脑中有一个好的 idea 的时候,我们的大脑就是一个 work directory,这个时候为了避免忘记,或者它丢失,我们需要将他用笔写下来,这个时候纸就是 Index/Stage。同时我们会进行不断的思考,那么就是不断的修改 work directory 的内容,并不断 add 到 Index/Stage 当中。当这个 idea 整理清楚的时候,我们需要将这张纸整理起来,就像是放到档案室,这个时候档案室就是我们的 Git Directory

Git 基本对象

说完这些,就可以进入我们的正题了。

Git 实际上有自己对于对象的定义。 Git 当中存在 3种对象,blobtreecommit。而所有对象都有自己的身份标记 —— SHA-1码。(对于 SHA-1 的解释可以参见维基百科

blob

blob 就是我们所说的简单文件,在谷歌中的翻译 blob 指的是大的二进制文件。在 git 中你可以认为他就是文件对象。

tree

既然有了文件对象,那么需要对于文件对象进行层级排列。这个时候就需要引入 tree 的概念了。Git 模仿了 Unix 的文件管理体系,不过他没有系统的那么沉重,相对而言更加轻量级,仅仅包含了里面有哪些文件。

commit

当文件结构已经确定了,剩下的就是将这些内容进行提交了。每次当我们将 tree 提交的时候,就创建了一个 commit,而有 SHA-1 的 commit 便决定了 Git 的整个体系,能够通过 SHA-1 进行追踪。

在清楚了上面的几个关键点之后,就可以进入我们的正题了。从我们创建 git 到提交完成整个工程,具体发生了些什么。

深入理解 Git

创建 git

当我们完成了 git init 之后,我们创建了一个包含了 .git 的文件目录

➜  test git:(master) la
total 0
drwxr-xr-x  10 zkhcreator  staff   320B Jan 31 18:40 .git

这个时候我们会发现 .git 文件下的结构如下:(附带功能描述)

.git
├── HEAD    (用于管理 HEAD 所在的位置)
├── branches    (有哪些分支)
├── config  (配置文件)
├── description (对于这个工程的描述文件)
├── hooks   (本地的 git 的所有钩子的配置)
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude (用于在 git 层面告知 git 哪些文件不需要进行版本控制)
├── objects (所有 git 对象)
│   ├── info    (git 对象的信息,对象以及他的 SHA-1 存储的地方)
│   └── pack    (git 执行 gc 操作打包后的存储地)
└── refs    (所有引用(references)的地方)
    ├── heads   (所有分支头所在的位置)
    └── tags    (所有标签所在的位置)

上图的文件树结构构成了一个简单的 git

创建 blob

按照往常的思路,我们需要创建文件并执行 git add filename 但是这里我们不用这些高级命令,转而使用低级命令。

首先我们创建一个文件 echo 'test1' | git hash-object -w --stdin 这样就创建完成了一个文件,其中 -w 直接将数据写入数据库(准确的说是文件当中,因为 git 没有数据库这个概念,都是以文件进行存储的)当中,--stdin 表示使用标准输入输出数据流格式进行读取。

此时我们会发现他返回了一条 SHA-1: a5bce3fd2565d8f458555a0c6f42d0504a848bd5,同时再次打印我们 .git 中的文件路径,我们会发现他的 objects 路径下多了 a5/bce3fd2565d8f458555a0c6f42d0504a848bd5 这样的一个文件,这个和生成的 SHA-1 有一些微妙的联系,实际上 git 取了 git 的前两位作为一级目录,并将后面的位数作为文件名。

如果我们尝试去打开这个文件,我们会发现他是一堆乱码,因为 git 已经帮我转我一次了,如果需要读取里面的内容,我们需要使用 git cat-file -p a5bce3fd2565d8f458555a0c6f42d0504a848bd5 去进行展示,很明显,他就会输出我们刚刚通过数据流输入输出的 test1。上面命令中的 git cat-file -p 就是用来打印 SHA-1 对象的实际内容。

有人可能会说,我们 git 不都是添加文件的么?你这样直接写数据库算什么?

那我们来新建一个文件来重复以上操作。我们执行以下命令

$ echo "version 1" > just_for_test.txt
$ git hash-object -w just_for_test.txt
83baae61804e65cc73a7201a7252750c76066a30

这个时候我们发现又多了一个文件 .git/objects/83/baae61804e65cc73a7201a7252750c76066a30,然后我们再执行一次写入操作,尝试去修改里面的内容

echo "version 2" > just_for_test.txt
git hash-object -w just_for_test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

这个时候我们会发现,结构路径除了我们刚刚生成的 83baae61(为了方便理解,取前几位),还有一条新的 .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a。很明显,文件更新的情况,已经被写入到 git 的文件中。

但是我们执行 git status 会发现,文件还没有提交,说明 git 在添加到 commit 前,会将文件进行缓存。(不过什么时候进行存储数据库的,可能需要看下源码)

既然已经有了 sha-1 并且我们已经将文件存储到数据库,这个时候我们做数据恢复操作就很简单了。只需要执行 git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt 即可将前面的 version 1 恢复到文件目录当中。

这个时候如果我们重新执行 git has-object -w just_for_test.txt 我们会发现目录结构没有发生改变,因为 git 对于文件进行存储是基于文件内部的内容的,和其他的东西并无关系。

前面提到,git 当中总共有3种类型,但是 SHA-1 是通的,所以,有时候我们需要确认对应的 SHA-1 是什么类型,我们就可以使用 git cat-file -t SHA-1 进行打印,很明显,这个文件是 blob 对象。

创建 tree

如果单纯的文件,肯定是不能构成树目录的,就像我们脑子中的点子,顺序很乱,只有当我们写下来的时候,才能将它的顺序理清楚。所以只有当需要提交到 stash 才能确定谁再哪。

那么我们需要首先给文件提供一个暂存,故需要执行 git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 just_for_test.txt 将在数据库当中指定的文件进行存储到暂存区。--add 标识将工作区域的文件注册到暂存区当中,但是如果已经添加,则不会重复添加。--cacheinfo 标识需要添加的数据的详细信息,比如说添加指定文件,以及他们的权限。其中的文件名是放到缓存中的文件名,一般我们通过 add 进去的 work directory 和 stage 是一致的,但是可以手动指定不同,SHA-1 用来确定文件的内容具体是啥。此时我们执行 git status 会发现,文件已经被添加到暂存区了。

然后通过 git write-tree 将当前暂存区域的对象写到树对象当中。这个时候我们会发现它又给了我们一个 SHA-1:ffb66e4709bf8e9c0b101e63c6dbea3780293ff3 这个是生成的树对象的标识符,我们可以通过 git cat-file -t SHA-1 来获得它的类型就是我们想要的 tree。 当然通过搜索 $ find .git/objects/ff 也能在 objects 中很容易找到我们需要的文件。

那么树对象能进行嵌套么?答案当然是肯定的。

我们可以通过 git read-tree --prefix=bak ffb66e4709bf8e9c0b101e63c6dbea3780293ff3 来将原有的 tree 进行备份到当前的 tree 下面,由于老的 tree 是 object 对象,所以当你读取出来之后,你在 git status 下你会发现他被写入暂存区了,但是在 work directory 当中是需要删除的,也就是这个文件本身是不存在的,这都是因为无中生有,或者说强制读取对象的造成的。

创建 commit

既然完成了 stage 的写入,最后一步就是需要将当前的 tree 进行提交,并创建提交对象。这时候只需要执行 echo "first commit" | git commit-tree f9b1ec32e2c3b591a72aeec583da3dced8eaa2aa 这个时候,我们通过 git log 会发现,我们已经提交了 commit。但是 git status 还是有文件没有更新。这个原因也很简单,就是因为我们没有创建分支,这就导致 commit 不知道该提交到哪里。 只需要 git branch new_branch_name $(echo "commit message" | git commit-tree f9b1ec32e2c3b591a72aeec583da3dced8eaa2aa) 即可。

最后

当我们提交完上面的操作之后,也就是完成了我们最基本的常用命令的底层的操作。这个时候我们重新打印下我们的 .git 文件。会发现里面多了好多东西,除了我们自己的 objects 还有很多刚刚注释里面提到的,但是文章当中没有提到的内容。 比如说:

  • COMMIT_EDITMSG:最近一次 commit 的内容
  • HEAD:当前的头的位置,ref: refs/heads/master 即引用的位置为 refs/heads/master 文件夹中的内容。
  • INDEX:放置过 index 之后,即执行 git update-index 之后 index 的内容,编码过后的文件,不能容易的看懂。
  • logs/HEAD:HEAD 切换的日志文件。
  • logs/refs/heads/master: master 的 git 修改日志情况。左边为 0,右边有值寿命这个是新添加的。相反右边为 0说明文件是移除的。同时数字前4位标识所有内容长度的十六进制数,主要方便上传下载的智能协议的同步操作。具体内容可见 Git 传输协议
0000000000000000000000000000000000000000 d6f0ab93a960f0aa1127ce4a3cf2fb00c5cee78f zkhCreator <zkhCreator@gmail.com> 1548937962 +0800    commit (initial): test
d6f0ab93a960f0aa1127ce4a3cf2fb00c5cee78f b6542b871b52bdf66481d03127620e1d7b7d37c9 zkhCreator <zkhCreator@gmail.com> 1548938032 +0800    commit: test2
  • refs/heads/master:当前这个 master 对应的提交对象的 SHA-1 值。
  • refs/tags:对应 tag 的指向的提交对象的 SHA-1 值存储的地方。

最后的最后

当然其中还存在 objects/infoobjects/pack 这两个。你可以尝试在工程中执行 git gc 来对当前内容进行整理。你会发现文件当中的 objects/ 目录下干净了很多,仅仅留下了 objects/info/packsobjects/pack 。这个时候实际上 git 将你的文件进行了打包操作,从而减少了整个工程的体积。路径中的 pack-SHA1.idx 就是这个包的索引,里面包含了打包之后的文件信息(主要用于上传下载的过程中的不常用的文件索引,可以通过 git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx 进行查看文件目录),pack-SHA1.pack 则是压缩的 git 对象的集合。对于细节想了解的同学可以查看官网的Git 内部文件

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容