.git 目录的结构和每个部分的作用[译]


想必每个程序员都对 git 非常熟悉,但是你是否知道 .git 目录的结构和每个部分的作用呢?今天我翻译的文章是《Inside .git》,作者是 Julia Evans。文章详细介绍了 Git 版本控制系统中的 .git 目录的结构和每个部分的作用。以下是对文章的全文翻译,为便于理解,部分内容可能稍作修改:

你好!这周我在 Mastodon 上发布了一个关于 .git 目录里有什么内容的漫画,有人想要文字版,于是诞生了这篇文章。我还添加了一些额外的注释。下面是漫画图片。它用大约 15 个词解释了你的 .git 目录的每个部分。

如果你想自己运行这些例子,可以使用 git clone https://github.com/jvns/inside-git

这里有一个目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HEAD: .git/head
branch: .git/refs/heads/main
commit: .git/objects/10/93da429…
tree: .git/objects/9f/83ee7550…
blobs: .git/objects/5a/475762c…
reflog: .git/logs/refs/heads/main
remote-tracking branches: .git/refs/remotes/origin/main
tags: .git/refs/tags/v1.0
the stash: .git/refs/stash
.git/config
hooks: .git/hooks/pre-commit
the staging area: .git/index
this isn’t exhaustive
this isn’t meant to completely explain git

前五个部分(HEAD,分支,提交,树,快照)是 git 的核心。

HEAD: .git/head

HEAD 是一个非常小的文件,只包含你当前分支的名称。

示例内容:

1
2
$ cat .git/HEAD
ref: refs/heads/main

HEAD 也可以是一个提交 ID,这被称为 “分离 HEAD 状态(detached HEAD state)”。

分支(branch): .git/refs/heads/main

分支也被存储到一个小文件中,只包含一个提交 ID。它被存储在名为 refs/heads 的文件夹中。

示例内容:

1
2
$ cat .git/refs/heads/main
1093da429f08e0e54cdc2b31526159e745d98ce0

提交(commit): .git/objects/10/93da429…

提交是一个小文件,包含它的父项、提交信息、和作者。

示例内容:

1
2
3
4
5
6
7
$ git cat-file -p 1093da429f08e0e54cdc2b31526159e745d98ce0
tree 9f83ee7550919867e9219a75c23624c92ab5bd83
parent 33a0481b440426f0268c613d036b820bc064cdea
author Julia Evans <julia@example.com> 1706120622 -0500
committer Julia Evans <julia@example.com> 1706120622 -0500

add hello.py

这些文件是压缩的,查看对象的最佳方式是使用 git cat-file -p HASH

树(tree): .git/objects/9f/83ee7550…

是包含目录列表的小文件。其中的文件称为 blob[1]

示例内容:

1
2
3
4
$ git cat-file -p 9f83ee7550919867e9219a75c23624c92ab5bd83
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 .gitignore
100644 blob 665c637a360874ce43bf74018768a96d2d4d219a hello.py
040000 tree 24420a1530b1f4ec20ddb14c76df8c78c48f76a6 lib

这里的权限看起来像是 Unix 权限,但实际上非常受限,只允许 644 和 755。

blob: .git/objects/5a/475762c…

blob 是包含你实际代码的那些文件。

示例内容:

1
2
$ git cat-file -p 665c637a360874ce43bf74018768a96d2d4d219a
print("hello world!")

每次更改都存储一个新的 blob 可能会使其变得很大,所以 git gc 会定期在 .git/objects/pack 中打包它们以提高效率。

引用日志(reflog): .git/logs/refs/heads/main

引用日志存储每个分支、标签和 HEAD 的历史。对于 .git/refs 中的每个文件,通常在 .git/logs/refs 中都有一个对应的日志文件,但不是绝对的,可能存在一些例外情况。

示例内容,对于 main 分支来说:

1
2
3
4
5
6
$ tail -n 1 .git/logs/refs/heads/main
33a0481b440426f0268c613d036b820bc064cdea
1093da429f08e0e54cdc2b31526159e745d98ce0
Julia Evans <julia@example.com>
1706119866 -0500
commit: add hello.py

译者注:tail -n 1 这个命令的意思是显示文件的最后 1 行。

引用日志的每一行包含:

1
2
3
4
提交前后的 ID
用户
时间戳
日志消息

通常它都是一行,我只是为了方便阅读而在这里换行。

远程跟踪分支: .git/refs/remotes/origin/main

远程跟踪分支(remote-tracking branches)保存了你最后一次从远程仓库获取(fetch)或拉取(pull)时,远程分支上的提交 ID(commit ID)。

示例内容:

1
2
$ cat .git/refs/remotes/origin/main
fcdeb177797e8ad8ad4c5381b97fc26bc8ddd5a2

当你运行 git status 命令时,如果 Git 提示 “你与 origin/main 同步” ,Git 只是检查了这个文件。它通常已经过时了,你可以使用 git fetch origin main 来更新该文件。

标签(tags): .git/refs/tags/v1.0

标签是 .git/refs/tags 中包含提交 ID 的一个小文件。

示例内容:

1
2
$ cat .git/refs/tags/v1.0
1093da429f08e0e54cdc2b31526159e745d98ce0

与分支不同,当你进行新的提交时,标签不会更新。

stash: .git/refs/stash

stash 是一个名为 .git/refs/stash 的小文件。它包含当你运行 git stash 时创建的提交 ID。

1
2
cat .git/refs/stash
62caf3d918112d54bcfa24f3c78a94c224283a78

stash 是一个栈,以前的值存储在 .git/logs/refs/stash(stash 的引用日志)中。

1
2
3
cat .git/logs/refs/stash
62caf3d9 e85c950f Julia Evans <julia@example.com> 1706290652 -0500 WIP on main: 1093da4 add hello.py
00000000 62caf3d9 Julia Evans <julia@example.com> 1706290668 -0500 WIP on main: 1093da4 add hello.py

与分支和标签不同,如果你从 stash 中 git stash pop 一个提交,它会从引用日志中删除,所以几乎不可能再次找到它。stash 是 git 中唯一一个被添加后很快就会被删除的引用日志。(在 Git 中,默认情况下,引用日志的条目大约保留 90 天后会被自动清除。)

关于引用的说明

到目前为止,你可能已经注意到很多东西(例如分支、远程跟踪分支、标签和 stash)都是 .git/refs 中的提交 ID。它们被称为 “引用(references)” 或 “refs”。每个引用都是一个提交 ID,但是 git 以非常不同的方式处理不同类型的引用,即使它们都使用相同的文件格式,我发现将它们分开考虑很有用。例如,git 会从 stash 引用日志中删除日志,但不会删除分支或标签引用日志。

.git/config

.git/config 是存储库的配置文件。这是你配置远程仓库的地方。

示例内容:

1
2
3
4
5
6
[remote "origin"]
url = git@github.com: jvns/int-exposed
fetch = +refs/heads/_: refs/remotes/origin/_
[branch "main"]
remote = origin
merge refs/heads/main

该配置分为本地和全局设置,本地设置在这里,全局设置在 ~/.gitconfig 目录中。

钩子: .git/hooks/pre-commit

钩子是可选的脚本,意味着你可以根据需要(例如提交前)选择是否使用它们。

示例内容:

1
2
#!/bin/bash
any-commands-you-want

(这显然不是一个真正的 pre-commit 钩子)

the staging area: .git/index

the staging area 存储你准备提交的文件。与 git 中大多数纯文本文件不同的是,它是一个二进制文件。

据我所知,查看索引内容的最佳方式是使用 git ls-files --stage

1
2
3
4
$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 .gitignore
100644 665c637a360874ce43bf74018768a96d2d4d219a 0 hello.py
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 lib/empty.py

这不是目录中的全部内容

.git 中还有其他一些东西,比如 FETCH_HEAD、worktrees 和 info。我只包含了那些我认为有助于理解的内容。

这并不意味着完全解释了 git

我听到的关于 git 的最常见的建议之一是“只要学会 .git 目录的结构,然后你就会理解一切!”

我比任何人都更喜欢理解事物的内部,但是“.git 目录的结构”并不能解释很多东西,比如:

  • 合并(merges)和变基(rebases)是如何工作的,以及它们可能出错的地方(例如这个关于变基可能出错的列表)
  • 你的同事是如何使用 git 的,以及你应该遵循什么指导方针才能成功地与他们合作
  • 从其他存储库推送 / 拉取代码是如何工作的
  • 如何处理合并冲突

虽然提供的信息可能不全面,不过,希望这些信息对于那些需要它的人是有帮助的。

一些其他参考资料:

译者说

在翻译此文过程中,使用 hexo 的 reference 的脚注插件发现,脚注内容不能包含 "" 双引号,否则会渲染异常。如下图:

Stash 和 Stage 的区别

Stash(暂存)

  • 暂存(Stash) 是一个用于临时保存工作进度的特性。当你在开发过程中需要切换到另一个分支,但你不想立即提交当前的工作(可能是因为它不完整或者你还在工作进行中),你可以使用 git stash 将当前的工作进度保存起来。
  • 被 stash 的更改会从工作目录和暂存区移除,这样你的工作目录就会变得干净,可以进行其他操作,如切换分支或拉取最新的更改。
    你可以有多个 stash,每个 stash 都保存了工作进度的不同快照。
  • 通过 git stash list 可以查看所有的 stash,使用 git stash apply 可以重新应用 stash 的更改,或者使用 git stash pop 应用并从列表中移除这个 stash。

Stage(暂存区)

  • 暂存区 是 Git 工作流程中的一个中间步骤,用于准备下一次提交。当你对文件做了更改后,你需要使用 git add 命令将这些更改添加到暂存区。
  • 暂存区是一个文件(.git/index),它记录了你准备在下一次提交中包含的文件列表和这些文件的当前状态。
  • 暂存区的状态可以通过 git status 查看,它会显示哪些文件已经被暂存,哪些文件还在工作目录中等待暂存。
  • 一旦你使用 git commit 提交了更改,暂存区的内容就会被写入一个新的提交,并且暂存区会被清空。

区别总结

  • 目的不同:Stash 用于临时保存和恢复工作进度,而 Stage 用于准备和审查即将提交的更改。
  • 工作方式不同:Stash 会将更改保存在栈中,可以随时应用或丢弃;Stage 是一个准备区,用于在提交前组织和检查更改。
  • 状态影响不同:使用 Stash 后,工作目录会被清空,而 Stage 后,更改会被标记为待提交,但仍然保留在工作目录中。

参考文档

原文链接


  1. 1.在 Git 版本控制系统中,blob 是 Binary Large Object 的缩写,但在 Git 的上下文中,它通常指的是存储在仓库中的文件内容。在 Git 的对象存储中,blob 代表文件数据,即文件的具体内容。在 Git 的术语中,当你提交(commit)一个文件到仓库时,Git 会将文件的内容存储为一个 blob 对象。这个 blob 对象包含了文件的数据,但不包含文件的元数据(如文件名或目录结构)。然后,这些 blob 对象被引用在一个 tree 对象中,tree 对象相当于一个目录的快照,它记录了文件的名称、类型(是文件还是子目录)以及对应的 blob 或子 tree 的引用。