在使用 Git 版本控制的过程中,有些概念我们必须有所了解,这样才能更有效率也更有意义的学下去。
有清楚且正确的概念认知,不但有助于我们学习如何操作 Git 命令,更重要的是,学习 Git 的相关知识也会更加容易上手。
本文的一些概念都是使用英文为主,这是因为在不同文章里可能会存在一些翻译问题,会导致大家有一些理解误差。
我们更多的将其翻译为「仓库」, 我们要使用 Git 进行版本控制,很自然的,我们需要一个「仓库」来储存这些版本信息,这个仓库其实就是用来储存所有版本的一个空间或一个文件夹与一堆文件。
如果有了解过 Git 的人,应该很清楚,建立仓库有很多方法,如果我们要在任意一个文件夹里建立一个 Git 仓库,只要输入以下命令就可以建立完成:
git init
从上图可以看到,我们在执行了 git init 命令后在 demo 目录下创建了 .git 目录,这个文件夹就是一个 Git 仓库,未来所有版本的变更,都会自动储存在这个文件夹里面。
「工作目录」
在上面我们执行了 git init 命令后,这个 demo 文件夹就会自动成为我们的「工作目录」。
所谓「工作目录」的意思,就是我们正在准备开发的项目文件,未来都会在这个目录下进行编辑,无论是新增文件、修改文件、删除文件、文件更名以及所有其他 Git 相关的操作,都会在这个目录下完成,所以才称为「工作目录」。
由于在使用 Git 版本控制时,会遭遇到很多分支的状況,所以「工作目录」很有可能会在不同的分支之间进行切换,有些 Git 命令在执行的时候,会一并更新「工作目录」下的文件。例如当我们使用 git checkout 切换到不同分支时,由于目前分支与想要切换过去的分支的目录结构不太一样,所以很有可能会将我们目前「工作目录」下的文件进行更新,好让目前的「工作目录」下的这些目录与文件,都与另一个要切换过去的分支下的目录与文件一样。
所以,适时的保持「工作目录」的干净,是版本控制过程中的一个基本原则,更尤其是日后要进行合并的时候,这点尤其重要。
「暂存区」,由于在 git 仓库中「暂存区」其实是一个名为 index 的文件( .git/index ),所以也会被叫做「索引」。
「工作目录」下的每一个文件都只有一种状态:已跟踪 或 未跟踪。
已跟踪的文件是指那些被纳入了版本控制的文件,「工作目录」中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有被放入「暂存区」。简而言之,已跟踪的文件就是 Git 已经知道的文件。
在「工作目录」中新增一个文件,这个文件就是未追踪的。
比如我们在「工作目录」中新增一个 index.js 文件,然后通过 git status 查看状态
这里提示了「工作目录」里存在一个未追踪的文件 index.js.
此时我们可以通过 git add index.js 把 index.js 文件添加进「暂存区」中,在运行 git status 命令,会看到 index.js 文件已被追踪,并处于暂存状态:
只要在 Changes to be committed 这行下面的,就说明是已暂存状态。 如果此时执行 git commit 命令,那么该文件在我们执行 git add 时的版本将被留存在后续的历史记录中。
修改 index.js 文件的内容后我们再执行 git status 看看状态。
index.js 文件出现在「暂存区」和非暂存区了,这是因为我们执行 git add 命令后只是把当前版本(文件内容变更后都属于一个新版本)添加进「暂存区」。
如果我们执行 git commit 命令把版本提交出去,版本库中 index.js 的版本就是我们最后一次执行 git add 命令时的那个版本,而不是当前「工作目录」中最新的版本。所以,运行了 git add 之后又作了更改的文件,需要重新运行 git add 把最新版本重新暂存起来。
上图中文件 index.js 出现在 Changes not staged for commit 这行下面,说明已跟踪文件的内容发生了变化,但还没有放到「暂存区」。 要暂存这次更新,就需要再次运行 git add 命令。
「暂存区」的目的主要用来记录「有哪些文件即将要被提交到下一个 commit 版本中」。如果我们想把某个版本提交到 Git 仓库里,那么我们首先就要把这个文件放入「暂存区」中,之后才能将这个变更版本提交出去。
对于已追踪的文件,在工作一段时间后,它们的状态可能是 unmodified、modified 或 staged。
git add 命令,是为了将目前「工作目录」的变更写入到「暂存区」里。
使用 git add -u 则可以仅将「更新」或「删除」的文件变更写入到「暂存区」中。
git add 命令是一个多功能命令,可以:
主要用来获取 「工作目录」中的文件的最新版本与「暂存区」中的差异。
这些差异中一共有三种不同的分组:
对象,Git 的对象分为以下四种类型:
上面的几种类型不需要强行硬背,阅读完下面的部分想必大家都能轻易理解。
我们在执行 git init 后,.git/objects 文件夹里只存在 info、pack 两个空文件。
当我们在根目录下输出下面的命令创建一个 file1.txt 文件,并输入 hello git; 作为内容。
echo hello git; > file1.txt
然后执行 git add . 命令将 file1.txt 文件放入暂存区:
git add .
此时我们看看 objects 文件夹中多了个名为 76 的文件夹
该文件夹里的存在一个 31bcb57512d5989050a08d7c507c8db7eebf8f 文件
我们在通过 git add 命令将文件存入「暂存区」时,都会将文件的内容中取出,通过内容产生一组 SHA1 哈希值,然后依照这个 SHA1 哈希值命名一个文件并放入 objects 文件夹中。这个文件就是 「blob 对象」。
Git 仓库中的每一个「对象」,都是以「文件内容」进行 SHA1 哈希运算出一个 hash 值,并用这个 hash 值当作对象的名称 (文件名)。我们以 7631bcb57512d5989050a08d7c507c8db7eebf8f 为例,Git 会先拿前两个字元(76)当作目录名,然后把剩下的 hash 值当成文件名 (31bcb57512d5989050a08d7c507c8db7eebf8f),这些对象的实体目录与文件也都是放在 .git\objects 目录下。
此时我们打开这个文件,可以发现里边是乱码,这是因为文件内容都是通过 zlib 算法进行压缩过的,这样不但可以有效的提升文件存取效率,在日后进行封装(pack)的时候也可以利用差异压缩(delta compression)演算法来节省空间。他会自动找出相似的 blobs,并自动计算出 blob 之间的变化差异,再将这些差异储存在一个名为 packfile 的文件中,这样就可以大幅节省磁盘空间的耗用)。
如果我们想读取里边的内容,可以使用 node 中的 zlib 库进行解压。
const zlib = require("zlib");
const fs = require("fs");
const path = require("path");
const file = fs.readFileSync(path.resolve(__dirname, "./31bcb57512d5989050a08d7c507c8db7eebf8f"));
zlib.unzip(file, function (err, buffer) {
if (!err) {
console.log(buffer.toString());
}
});
可以看到解压出来的内容并非原来的文本内容,而是多了 blob 12 这几个字符( Git 的处理),其中 blob 就是标识这个文件类型,12 就是文件的大小。
Git 也提供了一个命令来读取这些对象的内容:
git cat-file -p [hashname]
此时读取的文件内容是不包含类型及文件大小的,我们可以使用 -t 标识来读取这个文件对象类型:
使用 -s 标识来读取文件大小:
在把文件放入「暂存区」后,我们通过 commit 命令进行提交:
解析一下上面的信息:
执行了 commit 命令后在 objects 文件夹上新增了一个 bb 文件夹,这个文件里有两个文件。
通过上面的 commit 消息可以看到 bbada81 开头的就是「commit 对象」,也就是 ada81… 这个文件。
通过 git cat-file 看看这个文件对象类型及内容:
可以看到在「commit 对象」里存在以下信息:
在 objects 文件夹中也存在一个 bb/b2b3dbc… 文件。
通过 git cat-file 看看「tree 对象」类型及内容:
在上图中可以看到这个「tree 对象」里包含了一个「blob 对象」的索引以及文件名,这个「tree 对象」就相当于「工作目录」的根目录,根目录下存在一个 file1.txt 文件(也就是「blob 对象」),这样一个「commit 对象」其实就对应了当前提交的「工作目录」的版本。
为了更好的理解「tree 对象」,我们在「工作目录」创建一个 folder1 文件夹
执行 git status 查看状态时会发现提示 nothing to commit,这是因为 Git 是以文件内容进行对比的,文件夹并不会纳入对比。
在 folder1 文件夹下新建一个 file2.txt 文件,内容依旧是 hello git;。此时通过 git status 就可以看到 Git 检测到了「工作目录」存在未追踪的文件。
通过 git add . 把文件放入「暂存区」后,由于 file2.txt 与 file1.txt 的内容是相同的(hash 值也一样),因此在 objects 文件夹中并不会生成新的「blob 对象」,之后我们把文件提交出去。
此时在 objects 目录下生成了3个文件夹:02、55、c9(根据上图的 commit 信息可知,c9 文件夹里的 d4676… 文件就是「commit 对象」)。
解析一下上图中「commit 对象」:
通过 cat-file 查看一下 5505… 这个「tree 对象」的内容:
里面有一个「blob 对象」hash值、名称,以及一个「tree 对象」hash值、名称,我们现在盲猜都可以知道 02fe… 这个「tree 对象」里边的内容就是 file2.txt 文件对应的「blob 对象」hash值、名称。
整理一下当前的「commit 对象」,可以看到跟我们「工作目录」的结构是一样的。
此时 objects 中各对象的引用关系如下图所示:
对于 blob、tree、commit 这三个对象,我们应该都非常理解了吧,还剩最后一个「tag 对象」,这个文件在 .git/refs/tag 文件夹中,文件名就是 tag 名,文件内容就是在生成 tag 时「commit 对象」的 hash 值。
这四个对象的关系如下: