git

git 系列1/3:通过探索.git文件夹来理解git

Posted by jygao on March 29, 2020

Git的初学者像是一个不会读或写当地语言的,初到一个新的国家的人。一旦你知道你在哪里,你要去哪里,一切都很好,但你某个时刻迷路了,大的麻烦就来了。(#坏隐喻)


对于网上许许多多先介绍学习git的基础命令的文章,本文并不打算效他们。我打算从一个不同的角度去尝试介绍git。

新手们大抵对git有所恐惧,事实上,要不害怕也有点难。毫无疑问它一个强大的工具但对用户而言没有那么友好。 诸多新的概念,一个文件是否作为参数传递会做不同的事情,神秘的返回结果...

我认为克服这些第一拦路虎的方法是:比只会git commit/push的人多走一步。如果我们花点时间去理解Git背后的原理,日后会避免不少麻烦。

走进 .git

让我们开始进入正题。当你使用git init命令,新建一个git仓库,git会帮你创建一个特殊的目录:.git。这个文件包含了git工作所需的所有的内容。也就是说,如果你不想在你的项目中使用git,把.git文件夹删掉便是。但问题是,你为啥要那么做?


├── HEAD
├── branches
├── config
├── description
├── hooks
│ ├── pre-commit.sample
│ ├── pre-push.sample
│ └── ...
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags

在你的第一个提交之前你的.git看起来是这样的:

  • HEAD

然后我们看到这个

  • config

这个文件包含了你的仓库的设置,这里保存着远程仓库的url,例如你的邮件,用户名等信息。每次你在命令行中敲入‘git config ...’,最后持久化在这里。

  • description

被gitweb(某种类似github的祖先)用来展示仓库描述信息的文件。

  • hooks

这是个有趣的特性。Git定义了一系列的脚本,在每个有意义的阶段自动运行。这些脚本,被称作钩子,可以在commit/rebase/pull...前或者后自动执行。脚本的命名定义了在什么时候执行。一个pre-push的钩子可以在push前测试所有的风格的规则是否被满足,这样来保证远程仓库的一致性。

  • info — exclude

这里你可以放入你不想在.gitignore文件处理的文件列表。这样这些文件一样被排除了但是配置不会被共享。例如,如果你不想让你自定义的IDE相关的配置文件被git管理,在某种情况下 ,即使大部分情况.gitignore足够了(请在评论中回复,什么时候非要在exclude定义不可)

Commit中包含什么?

每次你新建一个文件,把它加入git仓库,git会压缩并以自己的数据结构去存取它。压缩后的这个对象会有一个唯一的名字,一个hash值,并且会保存在objects的目录下。

在探究objects目录之前,我们必须问自己一个问题:什么是一个commit?据上可知,commit是你的工作目录的某种快照。但比这还要复杂一点。

事实上,当你提交以创建快照时git只做了两件事:1.如果文件没有更改,git只是把压缩文件名字(hash值)放到快照中。2.如果文件有更改,git压缩它,把它存放到object的文件夹下。最后它把这个压缩的文件的名字(hash值)放入快照。

以上是一个简单化的说明,整个流程会稍微更复杂一点点,我们在以后文章中在详细介绍。

一旦快照被创建,它也会被压缩,也对应一个hash值,然后所有这些压缩过的对象会去了哪里?objects文件夹下。

├── 4c 
│└── f44f1e3fe4fb7f8aa42138c324f63f5ac85828 // hash
├── 86
│└── 550c31847e518e1927f95991c949fc14efc711 // hash
├── e6
│└── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 // hash
├── info // let's ignore that
└── pack // let's ignore that too

以上是当我创建了一个空file_1.txt文件后,objects文件夹的样子。请注意到你的文件的hash值是“ 4cf44f1e… ”这样开头的,git会把它放在“4c”的子目录下,然后把该文件以“f44f1...”的形式命名。这个小的技巧减少了相当于255个/objects目录的占用的空间。

可以看到3个hash值。一个对应我的file_1.txt的,另外一个对应当我提交时产生的快照。那第三个呢?因为一个commit本身也是一个object,它也被压缩并也存放在object文件夹下。

你需要记住的是,一个commit有以下四样东西组成:

  1. 当前的工作目录的快照的名字(hash值)
  2. 注释
  3. 提交者的信息
  4. 父commit的hash值

就是这样子的,你可以自己看下发生了什么,如果我们解压提交的文件:

// by looking at the history you can easily find your commit hash
// you also don't have to paste the whole hash, only enough 
// characters to make the hash unique

git cat-file -p 4cf44f1e3fe4fb7f8aa42138c324f63f5ac85828

我看到以下这些信息:

tree 86550c31847e518e1927f95991c949fc14efc711
author Pierre De Wulf <test[@gmail.com](mailto:pierredewulf31@gmail.com)> 1455775173 -0500
committer Pierre De Wulf <[test@gmail.com](mailto:pierredewulf31@gmail.com)> 1455775173 -0500

commit A

在这里,我们看到所期待的快照hash,作者,还有我提交的注释。这里有两样东西很重要:

  1. 和设想的一样,快照的hash值“86550...”也是一个对象,在对应的object文件夹下可以被找到。
  2. 因为这是我的第一个提交,所有没有父节点。

那我的快照里面真正是什么呢?

git cat-file -p 86550c31847e518e1927f95991c949fc14efc711

100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file_1.txt

在这里,我们找到了先前在对象存储中的最后一个对象,我们快照中的唯一一个对象。它被称作斑点块(blob),但这又是另外一个故事了。

branch, tags, HEAD 都一样

所以现在你可以明白到git中所有的东西都可以通过一个正确的hash值来访问。让我们现在回到HEAD,什么是HEAD?

cat HEAD ref: refs/heads/master

好吧,现在不是一个hash值了。它是有意义的,因为HEAD可以视为你指向当前工作的branch的尖端的一个指针的。现在我们来看看refs/heads/master中是什么东西,我们将看到:

cat refs/heads/master
4cf44f1e3fe4fb7f8aa42138c324f63f5ac85828

这是不是有点熟悉?是的,它跟我们第一次commit是同一个hash值。这告诉你说branches和tags不过只是一个指向某个commit的指针而已。这就代表你可以删掉你想要删掉的所有branches,所有tags,那些它们曾指向的commit不会被删掉,仍然保留在仓库里,只是要重新访问它可能有点麻烦了。关于这个,如果你想了解得更多,请参照 the git book.

最后一点

所以到了这里,你应该明白git做的事是:当你提交时,“压缩”你的当前工作目录,带上一系列额外的信息,并把它保存在objects的文件夹下。但如果你对这个工具比较熟悉的话,你可以完全控制哪些文件应该被包含进去,哪些不应该被包含在内。

我的意思的是,一个commit并不真的是你的当前工作目录的快照,它是你要提交文件的快照。那么当你做的commit真正生效之前,git把它存在哪里呢?git会把它们存在索引的文件里面(index)。我们现在不打算深挖下去了。如果你特别好奇的话,你可以先看下这里。

本文为中文翻译版,原博文链接:
https://www.daolf.com/posts/git-series-part-1/