作者 | Video++极链科技后端Team杨婕
整理 | 包包
Git作为大家熟悉的,深受欢迎的版本控制工具,和其他同类工具有很多不同之处:
- Git始终保存快照而不是文件差异。
- 任何数据存储前始终使用SHA-1计算校验和,保证内容完整性。
- 使用分布式仓库设计,让大多数操作都在本地进行,保证了使用效率。
- 几乎所有操作都是向数据库增加数据,提交之后就很难丢失数据。
它的本质更像一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面。
Git 有三种状态,你的文件可能处于其中之一:已修改(modified)、已暂存(staged)、已提交(committed)。由此引出三个逻辑区域,他们和文件状态以及部分对应操作的关系如下图。
![baef4d223743e6854275d1eb5a27ec39.png](https://img-blog.csdnimg.cn/img_convert/baef4d223743e6854275d1eb5a27ec39.png)
高层命令和底层命令:Git 最初是一套面向版本控制系统的工具集,它包含很多用于完成底层工作的命令。这些命令被设计成能以UNIX 命令行的风格连接,或由脚本调用来完成更复杂的工作。这部分一般被称作“底层(plumbing)”命令,那些对用户更友好的命令则被称作“高层(porcelain)”命令。
下面新建两个空仓库A 和B,来观察隐藏在Git常见命令下的实际执行过程。
1.git init
此命令初始化一个新本地仓库,它在工作目录下生成一个名为.git的隐藏文件夹。
![031dc4983c9e457cacf2eb90882126cc.png](https://img-blog.csdnimg.cn/img_convert/031dc4983c9e457cacf2eb90882126cc.png)
查看该文件夹结构:
![7966e9011d9c379b9e0dcd1f4ad1baa2.png](https://img-blog.csdnimg.cn/img_convert/7966e9011d9c379b9e0dcd1f4ad1baa2.png)
- config//文件- 包含一些配置选项
- objects//目录- 存储所有Git的数据对象
- HEAD//文件- 指定当前分支
- info //目录- 存放项目信息,默认包含一个全局exclude文件, 用来放置不希望记录在.gitignore 中的忽略模式
- description//文件- 仅供GitWeb 程序使用
- hooks //目录- 存放可在某些指令前后触发运行的钩子脚本(hook scripts),默认包含一些脚本样例
- refs//目录- 存储各个分支指向的目标提交
- branches //目录- 还没发现有什么用处
.git 目录下可能还会包含其他文件,不过对于一个全新的仓库,这将是你看到的默认结构。
其中有四个条目很重要:HEAD 文件、(尚未创建的)index 文件,和 objects 目录、refs 目录。这些条目是Git 的核心组成部分。
本地仓库刚刚新建,Git的三个区域都为空。
2.git add
在A仓库的工作目录创建一个文件file.txt,写入内容version 1,模拟需要管理的代码文件。
执行git add,使用git status查看此时的状态。
![344b529f8b946574586e759596ee9372.png](https://img-blog.csdnimg.cn/img_convert/344b529f8b946574586e759596ee9372.png)
然后另外初始化一个空仓库B,尝试用底层命令来实现以上效果。
创建相同内容的file.txt,执行 git hash-object,计算文件头部信息+文件内容的SHA-1编码,执行后显示出40位的编码结果。-w参数表示将内容写入数据目录。
![9d3bd815d17d73a1d258fd42da3ecf2a.png](https://img-blog.csdnimg.cn/img_convert/9d3bd815d17d73a1d258fd42da3ecf2a.png)
查看写入到数据目录的Git对象文件:
![92376d163317848796716c5c594641d5.png](https://img-blog.csdnimg.cn/img_convert/92376d163317848796716c5c594641d5.png)
Git以SHA1编码前两位作为子目录名,剩余位数作为文件名,存储压缩后的头部信息和原文内容。
可以通过 cat-file 命令查看原始数据。为 cat-file 指定 -p 选项可使该命令自动判断源文件类型。
![95459a59c38bb1a17eb7f23104dc0084.png](https://img-blog.csdnimg.cn/img_convert/95459a59c38bb1a17eb7f23104dc0084.png)
这种存储了数据原文的文件在Git对象中属于blob (Binary Large Object)类型。
此时Git的区域状态如下:
![ef90952fbe91ef4336d7f939b416d344.png](https://img-blog.csdnimg.cn/img_convert/ef90952fbe91ef4336d7f939b416d344.png)
使用git update-index 命令可以修改暂存区,也就是.git/index文件。
由于此文件在暂存区没有记录,需要--add参数。
使用--cacheinfo参数,直接写入数据文本。如果不加此参数,仅使用git update-index --add file.txt 的方式,则与add命令效果完全相同。
![aa6d42b9705b6caaf06194832134593d.png](https://img-blog.csdnimg.cn/img_convert/aa6d42b9705b6caaf06194832134593d.png)
本例中,我们指定的文件模式为100644,表明这是一个普通文件。其他选择包括:100755,表示一个可执行文件;120000,表示一个符号链接。Git的文件模式参考了常见的UNIX 文件模式,但比真正的文件系统简单许多。
此时暂存区index文件已经生成,直接打开会看到二进制字符,可以用 ls-files 命令解析查看。
![2aea44ccd36fa9a9de357dee8005e48f.png](https://img-blog.csdnimg.cn/img_convert/2aea44ccd36fa9a9de357dee8005e48f.png)
显示出刚刚写入的内容。
此时Git的区域状态如下:
![71aece55d5893172001d2d3581bbad7e.png](https://img-blog.csdnimg.cn/img_convert/71aece55d5893172001d2d3581bbad7e.png)
使用git status 查看,此时和A仓库状态相同。
![460a8c5bb34daf73763ddf4d3e7f008b.png](https://img-blog.csdnimg.cn/img_convert/460a8c5bb34daf73763ddf4d3e7f008b.png)
另外,由于 update-index --cacheinfo是直接写入文本,我们也可以添加完全不存在的对象名和文件名。
![567403321781e27e95e59073bcc2021a.png](https://img-blog.csdnimg.cn/img_convert/567403321781e27e95e59073bcc2021a.png)
此时B仓库的状态:
![fd4c9a2dc1784054343fae793eb5c64a.png](https://img-blog.csdnimg.cn/img_convert/fd4c9a2dc1784054343fae793eb5c64a.png)
3.git commit
回到A仓库,在git add 的基础上调用commit生成一个提交。
![0d5d42db7ea75ae7e2f3b754df37a1a5.png](https://img-blog.csdnimg.cn/img_convert/0d5d42db7ea75ae7e2f3b754df37a1a5.png)
再查看暂存区:
![f605edc871e18461dbdccd51c3da431a.png](https://img-blog.csdnimg.cn/img_convert/f605edc871e18461dbdccd51c3da431a.png)
与status的提示不同,提交操作并不会实际清空暂存区,其中始终保存着工作目录的文件结构。
再查看对象文件夹,发现两个新增文件。
![56d81e2ad92ba58a51e6477ce928a302.png](https://img-blog.csdnimg.cn/img_convert/56d81e2ad92ba58a51e6477ce928a302.png)
接下来我们在另一个仓库重现这个操作。
回到B仓库,继续执行 git write-tree。
![ea46d5819359bad4a67c853d9c8d45ab.png](https://img-blog.csdnimg.cn/img_convert/ea46d5819359bad4a67c853d9c8d45ab.png)
git 会在此时检查暂存区内容和数据目录中对象的对应关系,刚刚添加的不存在的文件导致失败。
git update-index --remove 命令可以从暂存区删除这条信息,只有在工作目录中不存在此文件时,才允许从暂存区直接删除相关信息。
![89715ca317fb76d635f0e876602ca276.png](https://img-blog.csdnimg.cn/img_convert/89715ca317fb76d635f0e876602ca276.png)
write-tree 执行成功后同样返回40位哈希值,此命令将暂存区内容写入数据目录,生成一个 tree类型的对象。此对象也可以使用cat-file 命令查看。
![1932acf025405e7f8912de1d647a6ff2.png](https://img-blog.csdnimg.cn/img_convert/1932acf025405e7f8912de1d647a6ff2.png)
使用刚刚生成的tree 对象来继续生成commit 对象,查看内容。
![804fc0feab5debac8c3e170e0a78de3e.png](https://img-blog.csdnimg.cn/img_convert/804fc0feab5debac8c3e170e0a78de3e.png)
其中用户信息使用 git config user.name 和 git config user.email 设置,仅对当前仓库生效,如未指定则使用全局配置。
查看对象目录:
![655308840e2370eb92369af73ca488af.png](https://img-blog.csdnimg.cn/img_convert/655308840e2370eb92369af73ca488af.png)
和A仓库直接git commit生成的文件对比,发现其中一个文件名不同。这是由于commit对象中包含执行时间信息,导致生成了不同的哈希编码。
使用log命令可以看到一个普通的commit信息。
![4695cce98147e10e35e4870de5c33608.png](https://img-blog.csdnimg.cn/img_convert/4695cce98147e10e35e4870de5c33608.png)
此时Git工作区域的状态:
![4d65161092316d5563a96701a039cf2c.png](https://img-blog.csdnimg.cn/img_convert/4d65161092316d5563a96701a039cf2c.png)
继续使用唯一的tree对象创建另一个提交。-p参数指定继承关系,作为标识符的hash值冲突概率较低,在git命令中通常使用前几位简写表示。
![6924cf867e7413bceb6c4afc20a6508f.png](https://img-blog.csdnimg.cn/img_convert/6924cf867e7413bceb6c4afc20a6508f.png)
有继承关系的commit对象多出一条parent信息:
![fed1e4face7e4ad34b96995a706f0318.png](https://img-blog.csdnimg.cn/img_convert/fed1e4face7e4ad34b96995a706f0318.png)
4.refs和HEAD
回到A仓库,使用commit 增加C1提交。
![ab352b8d6263f6af79e8b26ea40edad9.png](https://img-blog.csdnimg.cn/img_convert/ab352b8d6263f6af79e8b26ea40edad9.png)
检查到暂存区并未修改,提交失败。可以使用 --allow-empty 参数放弃检查。生成以下log。
![4aec6553421eda14e04cb3717a918924.png](https://img-blog.csdnimg.cn/img_convert/4aec6553421eda14e04cb3717a918924.png)
可以发现,打印的log信息和B仓库略有不同,并且B 仓库查看log时必须指定commit对象编码。如果不指定就会出现以下错误。
![51997223b4a1e9e042872736cedf25e4.png](https://img-blog.csdnimg.cn/img_convert/51997223b4a1e9e042872736cedf25e4.png)
原因是之前用底层命令生成的提交链并不属于任何分支。分支的本质是指向commit对象的指针,hash编码无意义难以记忆,分支名更方便灵活管理。
而HEAD 文件中保存着当前工作目录所在的分支,可以看到在B 仓库中,HEAD 指向默认分支master,而 master 文件还不存在。
![716057389fc6b1ca26ebc5d369efdee3.png](https://img-blog.csdnimg.cn/img_convert/716057389fc6b1ca26ebc5d369efdee3.png)
可以手动生成 master文件,并指向最新提交。
![1b947e1056b22e7329b53541e65082a2.png](https://img-blog.csdnimg.cn/img_convert/1b947e1056b22e7329b53541e65082a2.png)
虽然可以直接修改分支文件的内容,但这是一种不安全的做法,可以使用git update-ref 命令来达成同一效果。
此时B仓库和A仓库的状态就完全一致了。