关于git:Git-内部原理

4次阅读

共计 13175 个字符,预计需要花费 33 分钟才能阅读完成。

三个状态

git 的三个状态(三个区域):

  • Working Directory:工作区,间接编辑的区域,肉眼可见,间接操作;
  • Staging Area:暂存区,数据临时寄存的区域;
  • .git directory(Repository):版本库,寄存曾经提交的数据;

很多命令都和这三个状态有关系,比方:

  • git diff
  • git checkout
  • git reset

《图解 Git》中对这些命令有具体解说:https://marklodato.github.io/visual-git-guide/index-zh-cn.html

底层命令与下层命令

Git 实质上是一个内容寻址文件系统(content-addressable filesystem),并在此之上提供了一个版本控制系统的用户界面。

Git 最后是一套面向版本控制系统的工具集,而不是一个残缺的、用户敌对的版本控制系统,所以它蕴含了一部分用于实现底层工作的子命令。这些命令被设计成能以 UNIX 命令行的格调连贯在一起,或是由脚本调用,来实现工作。这部分命令一半被称作“底层(plumbing)”命令,而哪些更敌对的命令则被称作“下层(porcelain)”命令。

查看所有底层命令请移步:https://git-scm.com/docs

Git Objects 和 Git References

Git 实质上是一个内容寻址文件系统(content-addressable filesystem),Git Objects 和 Git References 是这个内容寻址文件系统的外围。

Git Objects

Git 通过四种 Object 来形容:文件、目录构造、提交(Commits)、附注标签(Annotated Tags)。

blob object

数据对象。形容文件,存储压缩后的文件内容。

tree object

树对象。形容目录构造,每个目录都是一个树对象。一个树对象蕴含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。

例子:

040000 tree 23c018695a85aaa4413817349abad68634df935c    dir_1
100644 blob b176602ea4b9de59db75aeb527bcda8507bd68ea    file_1

Git 以一种相似于 UNIX 文件系统的形式存储内容,但作了些许简化。所有内容均以 tree object(树对象)和 blob object(数据对象)的模式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大抵上对应了 inodes 或文件内容。

commit object

提交对象。形容提交(Commit),提交对象的格局:

  • tree:顶层树对象。代表以后我的项目快照;
  • parent:可能存在的父提交(我的项目的第一次提交没有该字段);
  • author:作者信息;
  • commiter:提交者信息;
  • 换行、提交形容信息

例子:

tree 88b50ad8cdf97b5cce6fdfb29d3da5c6097c39ea
parent ab03bad2084b41191c12dd08f412b25fb5a535a0
author Lin Zhang <zhanglin3@foxmail.com> 1613702136 +0800
committer Lin Zhang <zhanglin3@foxmail.com> 1613702136 +0800

2nd commit

tag object

Git 存在两种类型的标签:附注标签和轻量标签。附注标签通过 Git Objects 来实现,轻量标签通过 Git References 来实现。

标签对象。形容附注标签(Annotated Tags),标签对象与提交对象十分类似,它们的次要区别在于,标签对象通常指向一个提交对象,而不是一个树对象。它像是一个永不挪动的分支援用——永远指向同一个提交对象,只不过给这个提交对象加上一个更敌对的名字罢了。

标签对象的格局:

  • object:Tag 指向的 Object 的 SHA-1 指针;
  • type:类型;
  • tag:Tag 名称;
  • tagger:打 Tag 的人的信息;
  • 换行、Tag 形容信息

例子:

object 935e454f8ca4bd06ca2742538722ea82392214c9
type commit
tag v0.1
tagger Lin Zhang <zhanglin3@foxmail.com> 1613702521 +0800

version v0.1

Git References

其实,Git Objects 曾经实现了 Git 的外围能力,那么 Git References 的意义何在?援用 Git 官网的一段话来答复这个问题:

如果你对仓库中从一个提交(比方 1a410e)开始往前的历史感兴趣,那么能够运行 git log 1a410e 这样的命令来显示历史,不过你须要记得 1a410e 是你查看历史的终点提交。如果咱们有一个文件来保留 SHA-1 值,而该文件有一个简略的名字,而后用这个名字指针来代替原始的 SHA-1 值的话会更加简略。

Git 通过四种 Renference 来实现:分支(Branch)、HEAD、轻量标签(Lightweight Tags)、近程援用(Remotes),而这些 Renference 能够了解为是 Object 的指针。

分支援用

Git 分支的实质:一个指向某一系列提交之首的指针或援用。

当运行相似于 git branch 这样的命令时,Git 实际上会运行 update-ref 命令,获得以后所在分支最新提交对应的 SHA-1 值,并将其退出你想要创立的任何新援用中。

HEAD 援用

HEAD 文件通常是一个符号援用(symbolic reference),指向目前所在的分支。所谓符号援用,示意它是一个指向其余援用的指针。

然而在某些常见的状况下,HEAD 文件可能会蕴含一个 git 对象的 SHA-1 值。当你在检出一个标签、提交或近程分支,让你的仓库变成“拆散 HEAD”状态时,就会呈现这种状况。

标签援用

Git 存在两种类型的标签:附注标签和轻量标签。附注标签通过 Git Objects 来实现,轻量标签通过 Git References 来实现。

轻量标签的全部内容只是一个固定的援用。须要留神的是,标签对象并非必须指向某个提交对象,能够对任意类型的 Git 对象打标签。

近程援用

如果增加了一个近程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保留在 refs/remotes 目录下。

近程援用和分支(位于 refs/heads 目录下的援用)之间最次要的区别在于,近程援用是只读的。尽管能够 git checkout 到某个近程援用,然而 Git 并不会将 HEAD 援用指向该近程援用。因而,你永远不能通过 commit 命令来更新近程援用。Git 将这些近程援用作为记录近程服务器上各分支最初已知地位状态的书签来治理。

演示

Git Objects 和 Git References 单纯的概念介绍会难以了解,所以接下来会进行演示。在演示过程中我会顺带介绍下 Index File。

新建一个我的项目,并初始化 Git

$ cd /home/user/my_project
$ git init

.git 目录简介

此时,咱们创立了一个叫做 .git 的文件夹,对于 git 的一切都在这个文件夹中。执行命令 tree .git,能够看到 .git 文件夹的构造:

$ tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

8 directories, 17 files

简略介绍下这些文件 / 文件夹的作用:

  • HEAD 文件:符号援用,指向目前所在的分支;
  • config 文件:git 仓库的配置文件;
  • description 文件:git 仓库的形容信息,次要给 gitweb 等 git 托管零碎应用;
  • hooks 文件夹:一些 shell 脚本,在特定事件产生时主动执行;
  • info 文件夹:git 仓库的一些信息
  • objects 文件夹:git 的外围,寄存所有 git 对象;
  • refs 文件夹:git 的外围,寄存所有援用;

因为是新建的我的项目,所以 .git 文件夹下还有一些临时未生成的文件 / 文件夹,后续用到了再介绍。

咱们持续。

新建文件夹和文件,为了演示 tree object,咱们创立多层文件夹和多个文件。

$ mkdir -p dir_1/dir_1_1; echo "file_1 content" > dir_1/dir_1_1/file_1; echo "file_2 content" > dir_1/dir_1_1/file_2

而后将其增加到 Starging Area:

$ git add .

好,此时,咱们察看一下 .git 文件夹:

$ tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── objects
│   ├── 2f
│   │   └── 1f63b009307da4ed7e965940a2eb843235b3a9
│   ├── b1
│   │   └── 76602ea4b9de59db75aeb527bcda8507bd68ea
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

10 directories, 20 files

会发现有一些变动:

  • .git 文件夹中多了一个文件:index
  • .git/objects 文件夹中多了两个子文件夹,以及每个子文件夹中多了一个文件;

Index File

咱们先来介绍一下 index 文件。其实这个 index 文件就是 Staging Area 的实现。该文件中蕴含:

  • 门路名称的排序列表;
  • 每个门路名称的权限;
  • blob object 的 SHA-1 值;

能够通过命令 git ls-files -s 来查看该文件的内容:

$ git ls-files -s
100644 b176602ea4b9de59db75aeb527bcda8507bd68ea 0   dir_1/dir_1_1/file_1
100644 2f1f63b009307da4ed7e965940a2eb843235b3a9 0   dir_1/dir_1_1/file_2

对于 Index File,有一些文章的解说十分具体,咱们就不多开展了。

  • Git: Understanding the Index File:https://mincong.io/2018/04/28/git-index/#1-understand-index-via-git-ls-files
  • Understanding Git — Index:https://medium.com/hackernoon/understanding-git-index-4821a0765cf

Git Objects

在方才的 Index File 中咱们会发现,其中只蕴含了咱们增加到 Staging Area 中的文件的门路、权限、文件的 SHA-1 值,然而并没有文件内容。而同时咱们也发现在 .git/objects 文件夹中多了一个文件,其实这个多进去的文件就是 blob object,其内容就是压缩后的数据。

当咱们执行 git add 命令时,如果此时有文件变动(增加或批改),都会在 .git/objects 中生成一个 blob object,Object 文件的生成有一些考究:

  • 该文件会被搁置在一个由两个十六进制字符命名的文件夹中;
  • 文件夹的名字和文件的名字拼接起来就是文件内容的 SHA-1 的值;

这里须要留神的是 blob object 只蕴含文件内容,并不蕴含文件门路,至于为何这样设计,能够本人思考一下。这里能够做一些尝试,比方,再建一个新文件,其内容也是“file_1 content”,执行 git add 后察看是否生成了新的 blob object。

查看 Git Objects

咱们能够通过一个十分外围的 git 底层命令来查看 Git Object 的内容:

  • git cat-file -t <object> 查看 Git Object 的类型;
  • git cat-file -p <object> 查看 Git Object 的内容;

这里,我应用一条组合命令间接查看 Git 我的项目中目前所有的 Git Objects:

$ find .git/objects -type f | cut -d '/' -f 3,4 | tr -d '/' | xargs -L 1 -I {} sh -c 'echo"--- SHA-1 ---";echo {};echo"--- type ---";git cat-file -t {};echo"--- content ---";git cat-file -p {};'
--- SHA-1 ---
2f1f63b009307da4ed7e965940a2eb843235b3a9
--- type ---
blob
--- content ---
file_2 content
--- SHA-1 ---
b176602ea4b9de59db75aeb527bcda8507bd68ea
--- type ---
blob
--- content ---
file_1 content

咱们持续。

执行 git commit 命令。

$ git commit -m "1st commit"
[master (root-commit) e558897] 1st commit
 2 files changed, 2 insertions(+)
 create mode 100644 dir_1/dir_1_1/file_1
 create mode 100644 dir_1/dir_1_1/file_2

而后看一下 .git 文件夹构造:

$ tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master
├── objects
│   ├── 2f
│   │   └── 1f63b009307da4ed7e965940a2eb843235b3a9
│   ├── 9b
│   │   └── 94e6dd70770e5d2427c5ac0173a15cc1368d60
│   ├── b1
│   │   └── 76602ea4b9de59db75aeb527bcda8507bd68ea
│   ├── b2
│   │   └── bff4ca9b8c7a200a3974c80ff36bc56633d339
│   ├── df
│   │   └── 31a2a5d12715d61bb07cb45e342220a410f3ef
│   ├── e5
│   │   └── 5889713caa8aa9f7dd91f1c9d3572b87229afa
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master
    └── tags

17 directories, 28 files

会发现又有了新的变动:

  • .git/objects 中多了四个新文件;
  • .git/refs/heads 中多了一个文件;

.git/refs/heads 咱们等会再说,先来看看 .git/objects 中的新文件:

$ find .git/objects -type f | cut -d '/' -f 3,4 | tr -d '/' | xargs -L 1 -I {} sh -c 'echo"--- SHA-1 ---";echo {};echo"--- type ---";git cat-file -t {};echo"--- content ---";git cat-file -p {};'
--- SHA-1 ---
9b94e6dd70770e5d2427c5ac0173a15cc1368d60
--- type ---
tree
--- content ---
100644 blob b176602ea4b9de59db75aeb527bcda8507bd68ea    file_1
100644 blob 2f1f63b009307da4ed7e965940a2eb843235b3a9    file_2
--- SHA-1 ---
b2bff4ca9b8c7a200a3974c80ff36bc56633d339
--- type ---
tree
--- content ---
040000 tree df31a2a5d12715d61bb07cb45e342220a410f3ef    dir_1
--- SHA-1 ---
df31a2a5d12715d61bb07cb45e342220a410f3ef
--- type ---
tree
--- content ---
040000 tree 9b94e6dd70770e5d2427c5ac0173a15cc1368d60    dir_1_1
--- SHA-1 ---
e55889713caa8aa9f7dd91f1c9d3572b87229afa
--- type ---
commit
--- content ---
tree b2bff4ca9b8c7a200a3974c80ff36bc56633d339
author Lin Zhang <zhanglin3@foxmail.com> 1613634462 +0800
committer Lin Zhang <zhanglin3@foxmail.com> 1613634462 +0800

1st commit
--- SHA-1 ---
2f1f63b009307da4ed7e965940a2eb843235b3a9
--- type ---
blob
--- content ---
file_2 content
--- SHA-1 ---
b176602ea4b9de59db75aeb527bcda8507bd68ea
--- type ---
blob
--- content ---
file_1 content

咱们会发现咱们刚刚介绍的 tree object 和 commit object 呈现了。仔细观察这两类 Git Object 的内容,会发现它们在援用其余的 Git Object,Git 正是依附这些援用来实现版本控制的。

Git References

当初让咱们回到刚刚被跳过的 .git/refs/heads 中多进去的文件 master。查看 master 文件的内容:

$ cat .git/refs/heads/master
e55889713caa8aa9f7dd91f1c9d3572b87229afa

其实当初 .git/HEAD 文件的内容也产生了变动:

$ cat .git/HEAD
ref: refs/heads/master

.git/refs 目录下有很多这类含有 SHA-1 值的文件。这就是 Git 分支 /Tag 的实质:一个指向某一系列提交之首的指针或援用。

  • .git/refs/heads 文件夹中的文件名与分支名一一对应;
  • .git/refs/tags 文件夹中的文件名与 tag 名一一对应;

而 .git/HEAD 文件是“HEAD 援用”的实现,“通常指向目前所在分支”的说法也在这里失去了验证。

咱们持续。

建设轻量标签:

$ git tag v0.1

查看 .git/refs 文件夹会发现多了一个文件。

$ tree .git/refs
.git/refs
├── heads
│   └── master
└── tags
    └── v0.1

其内容是最新的一次 commit object 的 SHA-1 值,轻量标签只是一个简略的援用。

$ cat .git/refs/tags/v0.1
e55889713caa8aa9f7dd91f1c9d3572b87229afa

再建设附注标签:

$ git tag -a v0.2 -m "version v0.2"

查看其内容,并不是最近一次 commit object 的 SHA-1 值,是一个新值。

$ cat .git/refs/tags/v0.2
21855c2aebdd3971d153b33738f0c78f903efd8a

查看 .git/objects 文件夹,会发现多了一个 Git Object:

$ tree .git/objects
.git/objects
├── 21
│   └── 855c2aebdd3971d153b33738f0c78f903efd8a
├── 2f
│   └── 1f63b009307da4ed7e965940a2eb843235b3a9
├── 9b
│   └── 94e6dd70770e5d2427c5ac0173a15cc1368d60
├── b1
│   └── 76602ea4b9de59db75aeb527bcda8507bd68ea
├── b2
│   └── bff4ca9b8c7a200a3974c80ff36bc56633d339
├── df
│   └── 31a2a5d12715d61bb07cb45e342220a410f3ef
├── e5
│   └── 5889713caa8aa9f7dd91f1c9d3572b87229afa
├── info
└── pack

查看新 Git Object,会发现它是一个 tag object(标签对象),这也是后面提到的最初一种类型的 git object。

$ echo 21855c2aebdd3971d153b33738f0c78f903efd8a | xargs -L 1 -I {} sh -c 'echo"--- SHA-1 ---";echo {};echo"--- type ---";git cat-file -t {};echo"--- content ---";git cat-file -p {};'
--- SHA-1 ---
21855c2aebdd3971d153b33738f0c78f903efd8a
--- type ---
tag
--- content ---
object e55889713caa8aa9f7dd91f1c9d3572b87229afa
type commit
tag v0.2
tagger Lin Zhang <zhanglin3@foxmail.com> 1613636382 +0800

version v0.2

咱们持续。

给仓库增加近程援用:

$ git remote add origin git@gitee.com:zhanglin5/my_project.git

这时候查看 .git/config 文件,会发现 remote 的配置多出了 [remote "origin"] 这一部分。

$ cat .git/config
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true
[remote "origin"]
    url = git@gitee.com:zhanglin5/my_project.git
    fetch = +refs/heads/*:refs/remotes/origin/*

将本地的 master 分支 push 到近程:

$ git push -u origin master

此时,查看 .git 文件夹的构造:

$ tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── TAG_EDITMSG
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│       ├── heads
│       │   └── master
│       └── remotes
│           └── origin
│               └── master
├── objects
│   ├── 21
│   │   └── 855c2aebdd3971d153b33738f0c78f903efd8a
│   ├── 2f
│   │   └── 1f63b009307da4ed7e965940a2eb843235b3a9
│   ├── 9b
│   │   └── 94e6dd70770e5d2427c5ac0173a15cc1368d60
│   ├── b1
│   │   └── 76602ea4b9de59db75aeb527bcda8507bd68ea
│   ├── b2
│   │   └── bff4ca9b8c7a200a3974c80ff36bc56633d339
│   ├── df
│   │   └── 31a2a5d12715d61bb07cb45e342220a410f3ef
│   ├── e5
│   │   └── 5889713caa8aa9f7dd91f1c9d3572b87229afa
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master
    ├── remotes
    │   └── origin
    │       └── master
    └── tags
        ├── v0.1
        └── v0.2

22 directories, 34 files

会发现 logs 文件夹和 refs 文件夹下都呈现了相干的文件,咱们先关注 refs 文件夹。

咱们看到的 .git/refs/remotes/origin/master 就是近程援用(remote reference)。如果增加了一个近程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保留在 refs/remotes 目录下。

近程援用和分支(位于 refs/heads 目录下的援用)之间最次要的区别在于,近程援用是只读的。尽管能够 git checkout 到某个近程援用,然而 Git 并不会将 HEAD 援用指向该近程援用。因而,你永远不能通过 commit 命令来更新近程援用。Git 将这些近程援用作为记录近程服务器上各分支最初已知地位状态的书签来治理。

git log 和 git reflog

git log 是显示以后的 HEAD 和它的先人的,递归是沿着以后指针的父亲,父亲的父亲,这样的准则。

git reflog 基本不遍历 HEAD 的先人。它是 HEAD 所指向的一个程序的提交列表:它的 undo 历史。reflog 并不是 repo(仓库)的一部分,它独自存储,而且不蕴含在 pushes、fetches 或者 clones 外面,它纯属是本地的。

git reflog 能够很好地帮忙你复原你误操作的数据,例如你谬误地 reset 了一个旧的提交,或者 rebase 等操作,这个时候你能够应用 git reflog 去查看在误操作之前的信息,并且应用 git reset --hard 去复原之前的状态。

What’s the difference between git reflog and log?:https://stackoverflow.com/questions/17857723/whats-the-difference-between-git-reflog-and-log

git reflog 存储在 .git/logs 文件夹中。

Packfiles

其实到这里,置信大家可能会有一些疑难,咱们每次增加、批改文件都会生成一个新的 blob object。如果咱们对一个十分大的文件做大量批改,那么新的 blob object 和老的 blob object 会有大量反复,岂不是节约了空间?

其实的确会是这样,Git 最后想磁盘中存储对象时所应用的格局被称为“涣散(loose)”对象格局,然而,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节俭空间和提高效率。当版本库中有太多的涣散对象,或者你手动执行 git gc 命令,或者你向近程服务器执行推送时,Git 都会这样做。

对于 pack 文件如何节约空间的,这里就不开展了,能够参照这篇文档:https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-%E5%8C%85%E6%96%87%E4%BB%B6

这里有一点须要留神一下,执行 git gc 命令后,会生成一个 .git/packed-refs 文件,外面蕴含了 .git/logs/refs 里的内容。对于这个文件能够参考:https://cloud.tencent.com/dev…

The Refspec

请移步:https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-%E5%BC%95%E7%94%A8%E8%A7%84%E8%8C%83

参考

  • 一文讲透 Git 底层数据结构和原理:https://zhuanlan.zhihu.com/p/142289703
  • Unpacking Git packfiles:https://codewords.recurse.com/issues/three/unpacking-git-packfiles
  • git pack-format:https://git-scm.com/docs/pack-format
  • Useful Git Commands:https://dev.to/lydiahallie/cs-visualized-useful-git-commands-37p1
  • 图解 Git:https://marklodato.github.io/visual-git-guide/index-zh-cn.html
  • Git Magic:http://www-cs-students.stanford.edu/~blynn/gitmagic/
正文完
 0