【AICon】 如何构建高效的 RAG 系统?RAG 技术在实际应用中遇到的挑战及应对策略?>>> 了解详情
写点什么

Git 历险记(四)——索引与提交的幕后故事

  • 2011-03-17
  • 本文字数:4131 字

    阅读完需:约 14 分钟

不一样的索引

我想如果看过《Git 历险记》的前面三篇文章的朋友可能已经知道怎么用 git add git commit 这两个命令了;知道它们一个是把文件暂存到索引中为下一次提交做准备,一个创建新的提交(commit)。但是它们台前幕后的一些有趣的细节大家不一定知晓,请允许我一一道来。

Git 索引是一个在你的工作目录(working tree)和项目仓库间的暂存区域 (staging area)。有了它, 你可以把许多内容的修改一起提交 (commit)。 如果你创建了一个提交 (commit),那么提交的一般是暂存区里的内容, 而不是工作目录中的内容。

一个 Git 项目中文件的状态大概分成下面的两大类,而第二大类又分为三小类:

  1. 未被跟踪的文件(untracked file)
  2. 已被跟踪的文件(tracked file)
  3. 被修改但未被暂存的文件(changed but not updated 或 modified)
  4. 已暂存可以被提交的文件(changes to be committed 或 staged)
  5. 自上次提交以来,未修改的文件 (clean 或 unmodified)

看到上面的这么多的规则,大家早就头大了吧。老办法,我们建一个 Git 测试项目来试验一下:

我们先来建一个空的项目:

复制代码
$rm -rf stage_proj
$mkdir stage_proj
$cd stage_proj
$git init
Initialized empty Git repository in /home/test/work/test_stage_proj/.git/

我们还创建一个内容是“hello, world”的文件:

复制代码
$echo "hello,world" > readme.txt

现在来看一下当前工作目录的状态,大家可以看到“readme.txt”处于未被跟踪的状态(untracked file):

复制代码
$git status
# On branch master
#
# Initial commit
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   readme.txt
nothing added to commit but untracked files present (use "git add" to track)

把“readme.txt"加到暂存区: $git add readme.txt

现在再看一下当前工作目录的状态:

复制代码
$git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#   new file:   readme.txt
#

可以看到现在"readme.txt"的状态变成了已暂存可以被提交(changes to be committed),这意味着我们下一步可以直接执行“git commit“把这个文件提交到本地的仓库里去了。

暂存区(staging area)一般存放在“git 目录“下的 index 文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)。索引是一个二进制格式的文件,里面存放了与当前暂存内容相关的信息,包括暂存的文件名、文件内容的 SHA1 哈希串值和文件访问权限,整个索引文件的内容以暂存的文件名进行排 序保存的。

但是我不想马上就把文件提交,我想看一下暂存区(staging area)里的内容,我们执行 git ls-files 命令看一下:

复制代码
$git ls-files --stage
100644 2d832d9044c698081e59c322d5a2a459da546469 0   readme.txt

我们如果有看过上一篇文章里 的"庖丁解牛", 你会发现“git 目录“里多出了”.git/objects/2d/832d9044c698081e59c322d5a2a459da546469”这么一个文件,再执行“git cat-file -p 2d832d” 的话,就可以看到里面的内容正是“hello,world"。Git 在把一个文件添加暂存区时,不但把它在索引文件 (.git/index) 里挂了号,而且把它的内容先保存到了“git 目录“里面去了。

如果我们执行”git add“命令时不小心把不需要的文件也加入到暂存区中话,可以执行“git rm --cached filename" 来把误添加的文件从暂存区中移除。

现在我们先在"readme.txt"文件上做一些修改后:

复制代码
$echo "hello,world2" >> readme.txt

再来看一下暂存区的变化:

复制代码
$git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#   new file:   readme.txt
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   readme.txt
#

大家可以看到命令输出里多了一块内容:“changed but not updated … modified: readme.txt”。大家可能会觉得很奇怪,我前面不是把"readme.txt"这个文件给添加到暂存区里去了吗,这里怎么又提示我未添加到暂存区 (changed but not updated)呢,是不是 Git 搞错了呀。

Git 没有错,每次执行“git add”添加文件到暂存区时,它都会把文件内容进行 SHA1 哈希运算,在索引文件中新加一项,再把文件内容存放到本地的“git 目录“里。如果在上次执行 “git add”之后再对文件的内容进行了修改,那么在执行“git status”命令时,Git 会对文件内容进行 SHA1 哈希运算就会发现文件又被修改了,这时“readme.txt“就同时呈现了两个状态:被修改但未被暂存的文件(changed but not updated),已暂存可以被提交的文件(changes to be committed)。如果我们这时提交的话,就是只会提交第一次“git add"所以暂存的文件内容。

我现在对于“hello,world2"的这个修改不是很满意,想要撤消这个修改,可以执行 git checkout 这个命令:

复制代码
$git checkout -- readme.txt

现在再来看一下仓库里工作目录的状态:

复制代码
$git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#   new file:   readme.txt
#

好的,现在项目恢复到我想要的状态了,下面我就用 git commit 命令把这个修改提交了吧:

复制代码
$git commit -m "project init"
[master (root-commit) 6cdae57] project init   1 files changed, 1 insertions(+), 0 deletions(-)    create mode 100644 readme.txt

现在我们再来看一下工作目录的状态:

复制代码
$git status
# On branch master
nothing to commit (working directory clean)

大家可以看到“nothing to commit (working directory clean)”;如果一个工作树(working tree)中所有的修改都已提交到了当前分支里(current head),那么就说它是干净的(clean),反之它就是脏的 (dirty)。

SHA1 值内容寻址

正如 Git is the next Unix 一文中所说的一样,Git 是一种全新的使用数据的方式(Git is a totally new way to operate on data)。Git 把它所管理的所有对象(blob,tree,commit,tag……),全部根据它们的内容生成 SHA1 哈希串值作为对象名;根据目前的数学知识,如果两块数据的 SHA1 哈希串值相等,那么我们就可以认为这两块数据是相同 的。这样会带来的几个好处:

  1. Git 只要比较对象名,就可以很快的判断两个对象的内容是否相同。
  2. 因为在每个仓库(repository)的“对象名”的计算方法都完全一样,如果同样的内容存在两个不同的仓库中,就会存在相同的“对象名”。
  3. Git 还可以通过检查对象内容的 SHA1 的哈希值和“对象名”是否匹配,来判断对象内容是否正确。

我们通过下面的例子,来验证上面所说的是否属实。现在创建一个和“readme.txt“内容完全相同的文件”readme2.txt“,然后再把它提交到本地仓库中:

复制代码
$echo "hello,world" > readme2.txt
$git add readme2.txt
$git commit -m "add new file: readme2.txt"
[master 6200c2c] add new file: readme2.txt
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 readme2.txt

下面的这条很复杂的命令是查看当前的提交(HEAD)所包含的 blob 对象:

复制代码
$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p
100644 blob 2d832d9044c698081e59c322d5a2a459da546469    readme.txt
100644 blob 2d832d9044c698081e59c322d5a2a459da546469    readme2.txt

我们再来看看上一次提交(HEAD^)所包含的 blob 对象:

复制代码
$git cat-file -p HEAD^ | head -n 1 | cut -b6-15 | xargs git cat-file -p
100644 blob 2d832d9044c698081e59c322d5a2a459da546469    readme.txt

很明显大家看到尽管当前的提交比前一次多了一个文件,但是它们之间却是在共用同一个 blob 对象:“2d832d9”。

No delta, just snapshot

Git 与大部分你熟悉的版本控制系统,如 Subversion、CVS、Perforce 之间的差别是很大的。传统系统使用的是: “增量文件系统” (Delta Storage systems),它们存储是每次提交之间的差异。而 Git 正好与之相反,它是保存的是每次提交的完整内容(snapshot);它会在提交前根据要提交 的内容求 SHA1 哈希串值作为对象名,看仓库内是否有相同的对象,如果没有就将在“.git/objects"目录创建对应的对象,如果有就会重用已有的 对象,以节约空间。

下面我们来试验一下 Git 是否真的是以“snapshot”方式保存提交的内容。

先修改一下"readme.txt",给里面加点内容,再把它暂存,最后提交到本地仓库中:

复制代码
$echo "hello,world2" >> readme.txt
$git add readme.txt
$git commit -m "add new content for readme.txt"
[master c26c2e7] add new content for readme.txt   1 files changed, 1 insertions(+), 0 deletions(-)

我们现在看看当前版本所包含的 blob 对象有哪些:

复制代码
$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p
100644 blob 2e4e85a61968db0c9ac294f76de70575a62822e1    readme.txt
100644 blob 2d832d9044c698081e59c322d5a2a459da546469    readme2.txt

从上面的命令输出,我们可以看到"readme.txt"已经对应了一个新的 blob 对象:“2e4e85a”,而之前版本的"readme.txt“对应的 blob 对象是:“2d832d9”。下面我们再来看一看这两个”blob“里面的内容和我们的预期是否相同:

复制代码
$git cat-file -p 2e4e85a
hello,world
hello,world2
$git cat-file -p 2d832d9
hello,world

大家可以看到,每一次提交的文件内容还是全部保存的(snapshot)。

小结

Git 内在机制和其它传统的版本控制系统(VCS)间存在本质的差异,所以 Git 的里"add"操作的含义和其它 VCS 存在差别也不足为奇,“git add“不但能把未跟踪的文件(untracked file)添加到版本控制之下,也可以把修改了的文章暂存到索引中。

同时,由于采用“SHA1 哈希串值内容寻值“和”快照存储(snapshot)“,让 Git 成为一个速度非常非常快的版本控制系统(VCS)。

参考

致谢

感谢上帝对我的眷顾,让我可以有写作专栏的这样一个机会。

感谢朋友们在写作过程的无私帮助:张凯峰刘炜许晓斌 Fenng……

特别要感謝家人默默支持:)

2011-03-17 01:147014

评论

发布
暂无评论
发现更多内容

学了阿里P8级架构师的7+1+1落地项目,万字长文!

Java 程序员 后端

PostgreSQL如何查找某一事务中的完整SQL

Qunar技术沙龙

sql postgresql 运维 dba

开发8年的老Java才知道,详细解说

Java 程序员 后端

我们究竟还要学习哪些Java知识?程序员翻身之路

策划Java工程师

Java 程序员 后端

已获千赞,进阶学习资料!

Java 程序员 后端

成功从三线小公司跳进大厂涨薪8K,你值得拥有

策划Java工程师

Java 程序员 后端

宅家36天咸鱼翻身入职腾讯,看完这一篇就够了!

Java 程序员 后端

Apache APISIX 在移动云的应用

API7.ai 技术团队

开源 APISIX

一场“软硬兼施”的数字革新,帮外卖商家和骑手节省时间

脑极体

【前端 · 面试 】HTTP 总结(二)—— HTTP 消息

编程三昧

面试 HTTP HTTP协议 8月日更 http消息

实战SpringCloud通用请求字段拦截处理,成功入职腾讯

Java 程序员 后端

应聘高级Java工程师历程感言,附赠复习资料

Java 程序员 后端

结合源码讲解:Kafka消费者参数配置(解释、定义、引用、注意事项)

石头哥谈架构

大数据 kafka架构 Kafka参数配置 Kafka技术内幕 分布式消息中间件

【LeetCode】删除有序数组中的重复项Java题解

Albert

算法 LeetCode 8月日更

总结2021年最全180道Java岗面试题,系列篇

策划Java工程师

Java 程序员 后端

我凭借这份PDF的复习思路,面试题+笔记+项目实战

策划Java工程师

Java 程序员 后端

Vue进阶(幺玖肆):VantUI 实现 Dialog 弹框动态显示 message

No Silver Bullet

Vue eCharts 8月日更

Vue进阶(幺柒贰):应用 @fullcalendar/vue 实现日程日历

No Silver Bullet

Vue 8月日更 Fullcalendar

弄到一份宝藏级SpringCloud实战文档,成功入职阿里

策划Java工程师

Java 程序员 后端

微信抢红包实战案例,在线面试指南

策划Java工程师

Java 程序员 后端

渣男已经预订大碗牢饭,“科技渣男”怎么还在疯狂套路?

脑极体

避免将 JWT 存储在 localStorage 中

devpoint

Token JWT LocalStorage 8月日更

业务架构训练营学习总结

好吃不贵

有产品思维和数据意识的解决方案架构师?

escray

学习 极客时间 朱赟的技术管理课 8月日更

学习Java开发的步骤,先睹为快

Java 程序员 后端

微信抢红包实战案例,已开源

策划Java工程师

Java 程序员 后端

成功跳槽百度工资从15K涨到28K,面试突击版!

策划Java工程师

Java 程序员 后端

我用2个月的时间破茧成蝶,附赠课程+题库

策划Java工程师

Java 程序员 后端

「SQL数据分析系列」14. 视图

数据与智能

sql 数据 视图

带你彻底搞懂Java启动速度优化!全网独家首发!

Java 程序员 后端

幸亏有这本623页的微服务框架实战笔记,讲的太清楚了

Java 程序员 后端

Git历险记(四)——索引与提交的幕后故事_Java_刘辉_InfoQ精选文章