Git 作为分布式版本管理工具的核心作用:支撑多人协同开发云端托管项目。开发者可克隆仓库至本地完成开发,完成改动后推送至远端主仓库,团队成员同步拉取更新;Git 完整记录代码迭代历史,支持任意版本回溯。 我亦掌握git add、git push等基础 Git 命令操作。 日常开发中 95% 的场景仅需复用六七条基础指令。其中两项核心规范:
- 其一,启动新需求开发时,务必基于开发分支新建特性分支,并在该分支内完成全部开发工作;
- 其二,严禁直接向生产分支提交推送任何代码变更,这条准则须严格恪守。" 对于我来说,日常高频使用的指令:git pull、git checkout、git add、git commit、git push 与 git merge。 在实操过程中,逐步吃透了commit的核心含义:其等同于版本迭代历程里的独立快照节点;同时也厘清了分支(branch)的底层逻辑 ------ 分支是从指定提交节点衍生出的独立开发链路。
在长期开发实践中,开发者会接触到各类进阶指令,例如 git reset、git revert,同时掌握 git stash、git cherry-pick 等高阶操作技巧。
多数从业者可熟练运用 Git 完成多人员协同开发工作,但对工具后台运行机制缺乏认知。为此,有必要深入学习 Git 底层数据模型,厘清工具内部运行原理。 充分掌握 Git 底层运行机制后,各类操作逻辑将形成完整闭环。本文旨在跳出机械套用指令的浅层使用模式,完整拆解 Git 内部底层运行原理。
Git 概述
首先参考 Git 官方文档给出的标准定义。 补充说明:使用 Git 工具前需完成本地环境安装,未部署该工具的使用者可前往 Git 官方网站获取安装程序。 下文将援引官方文档内容,梳理 Git 的标准释义。 官方文档首页,Git 的自我描述:
bash
NAME
git - the stupid content tracker
依据 Git 官方标准定义:Git 是一款高效、可拓展的分布式版本控制系统,具备完备丰富的指令体系,既支持上层封装操作,亦开放完整底层接口供使用者调取内部运行机制。 剥离上层功能封装,Git 的核心本质可概括为内容追踪工具( A tool to track content)。这是理解该工具的关键认知要点。 大众普遍存在固有认知,即 Git 仅适用于软件开发项目,该认知存在局限性。Git 的核心能力为追踪任意类型文件内容,适用范围不受软件代码限制。 该基础认知看似浅显,却能够直观反映使用者普遍存在的知识盲区:多数使用者仅掌握表层操作,并未真正理解 Git 底层核心逻辑。
Git 的内容追踪原理
可将 Git 的底层存储机制抽象为一套键值映射体系:体系中以唯一键匹配对应数据,映射中的值为各类文件转化后的二进制字节流。 当一段数据存入 Git 时,系统会对其完成持久化存储,并借助 SHA-1 哈希算法生成专属哈希键。哈希键是一段固定长度的摘要字符串,可唯一标识大块数据,Git 依靠该索引区分仓库内所有已存储内容。 哈希值的生成结果不受设备、操作系统影响:只要两段数据内容完全一致,无论在任何环境下计算,最终产出的哈希键必然相同。该特性是掌握 Git 底层逻辑的关键认知。
通过实际操作来理解
我准备创建一个项目,用来管理我家人的任务。 让我们初始化一个 Git 仓库(repository):
bash
// Initialize an empty Git repository inside tech-lab folder
$ git init tech-lab
// Open tech-lab folder
$ cd tech-lab
// Examine .git folder
$ ls .git
HEAD config description hooks/ info/ objects/ refs/
执行仓库初始化操作后,Git 将自动生成隐藏目录 .git,该目录内含多组配套文件与子目录。 本节重点说明 objects 目录,此目录为 Git 的对象数据库。 项目所有版本快照对应的存储对象,均统一生成并存放于该目录中。
bash
$ ls .git/objects
info/ pack/
objects 目录下内置 info 与 pack 两个子目录,二者服务于 Git 底层存储优化逻辑,现阶段无需深究。 未存入任何内容时,objects 目录默认处于空目录状态。 接下来,向当前项目中新增首个文件:
bash
# 创建 Plants.txt 文件,写入内容 Rose
$ printf "Rose" >> Plants.txt
#使用cat查看文件内容
cat Plants.txt
Rose
#查看新增文件后的仓库当前状态
$ git status
On branch master
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
Plants.txt
nothing added to commit but untracked files present (use "git add" to track)
Git 在这里告诉我们的是,我们有一些文件处于未跟踪(untracked)状态。这些文件存在于 Git 所称的 工作目录(Working Directory) 中。 如果我们希望将这些更改添加到下一次 commit(提交) 中,就必须先将它们添加到一个中间区域(intermediary area)。 将新增或修改的文件添加到暂存区:
bash
$ git add .
执行该指令并不会生成提交记录,亦不会将变更写入版本历史。 该命令的本质意图可表述为:
告知 Git 对目标文件开启跟踪,持续记录其后续改动。
这个用于存放已跟踪文件的第二个区域称为 Staging Area(暂存区),它的技术名称是 index。 若确认仅以当前全部变更生成commit,并且不包含其他任何更改,则暂存区内所有已登记变更,均会作为新commit的组成内容。
创建一个commit,信息(message)为"create plants file"的新commit
bash
$ git commit -m "create Plants file"
1 file changed, 1 insertion(+)
create mode 100644 Plants.txt
此时产生了何种底层变化? 其一,本次操作生成了第一条 commit。 除此之外,系统还将在 objects repository 的 objects/ 目录中生成若干内部存储对象。 可进入目录进行核查:
shell
$ ls .git/objects
01/ ab/ cf/ info/ pack/
除原有 info/、pack/ 子目录外,objects 目录下新增了三类对象文件。 在此之前,需明确 Git 包含三种基础对象类型:blob、tree 与 commit,下文将结合当前示例依次阐释各类对象的定义与作用。 接下来逐层解析新增对象的内部结构,首先从本次生成的 commit 对象入手 显示commit log:
bash
$ git log
commit 016e263a1b9b430cf0b1044b59d8e66d57ccc58c (HEAD -> master)
Author: plants-lab <plants-lab@lab.com>
Date: Sat Aug 15 21:54:56 2026 +0700
create Plants file
本次生成的首个 commit 哈希值为 :
bash
016e263a1b9b430cf0b1044b59d8e66d57ccc58c
观察 objects/ 目录下新增的子目录能够发现,存在名为 f6/ 的文件夹,命名取自该哈希值的前两位字符。 进入该目录,查看内部存储内容:
bash
$ ls .git/objects/01
6e263a1b9b430cf0b1044b59d8e66d57ccc58c
01/ 文件夹中的文件名,正好等于构成该 commit 的 hash key 的其余字符。
bash
01/6e263a1b9b430cf0b1044b59d8e66d57ccc58c= 016e263a1b9b430cf0b1044b59d8e66d57ccc58c
需留意对象数据库的存储命名规则:objects 目录下新建子目录名称取自哈希值前两位字符,子目录内文件名称则为哈希键剩余字符。该套命名约定(naming convention)是 Git 内置的数据存储规范,以此结构存储可大幅提升对象(objects)检索效率。
该文件即为对象数据库中与本次 commit 相对应的 object。
第一种 object 类型:commit
该 object 的内部结构与存储内容如何查看? 可借助底层 Git 命令(low-level Git command)cat-file,该指令能够输出仓库(repository)内任意 object 的类型与原始内容。 查看object类型:
bash
$ git cat-file 016e263a1b9b430cf0b1044b59d8e66d57ccc58c -t
commit
查看object类容:
bash
$ git cat-file 016e263a1b9b430cf0b1044b59d8e66d57ccc58c -p
tree cf88f5f0c0482b8948fec42bc85bcf23875e12fd
author plants-lab <plants-lab@lab.com> 1786805696 +0700
committer plants-lab <plants-lab@lab.com> 1786805696 +0700
create Plants file
这便是 commit 的内部结构,对应前文刚生成的commit记录。 commit 本质为纯文本数据。与 Git 内所有存储内容一致,该文本会先完成压缩处理,再生成专属 hash,最终持久化存入 objects repository。 commit object 内部存储各类 metadata 元数据,涵盖 author、committer、commit date 以及 commit message。 除此之外,该 commit 中还记录了一条 tree 及其对应的 hash key。 沿用检索 commit hash key 的方式查询此 tree 的 hash key,能够证实仓库(repository)内存在匹配该哈希标识的 object。
bash
$ ls .git/objects/
01/ ab/ cf/ info/ pack/
$ ls .git/objects/cf/
88f5f0c0482b8948fec42bc85bcf23875e12fd
bash
cf/88f5f0c0482b8948fec42bc85bcf23875e12fd = cf88f5f0c0482b8948fec42bc85bcf23875e12fd
这个 object 就是 object database 中对应于这个 tree 的 object
第二种 object 类型:tree
tree 是用于存储项目 directories(目录)信息的 object。 单个 tree 可引用其他 tree,以此 build 出完整的文件与子目录 hierarchy(层级结构),同时也能够指向 blob。 每一条 commit 都会关联一个 tree object,该 tree object 会以完整 snapshot(快照)的形式,capture 当前 commit 执行瞬间整个 repository 的全部状态。这份 snapshot 便是存入 Git history(Git 历史记录)的项目版本。 接下来查看 tree 的内部结构:
bash
$ git cat-file cf88f5f0c0482b8948fec42bc85bcf23875e12fd -t
tree
$ git cat-file cf88f5f0c0482b8948fec42bc85bcf23875e12fd -p
100644 blob ab0d218b097a7cb260b99336411779a703a672e1 Plants.txt
输出内容解释:
| 字段 | 值 | 含义 |
|---|---|---|
| 模式 | 100644 | 文件权限模式,100644 表示普通文件(非可执行),即 -rw-r--r-- |
| 类型 | blob | 说明这个条目是一个文件内容对象(blob),不是子目录(tree) |
| 哈希 | ab0d218b... | 这个文件内容对应的 SHA-1,可以用 git cat-file -p ab0d218b... 继续查看 Plants.txt 的实际内容 |
| 文件名 | Plants.txt | 该 blob 在这个目录(tree)里的名字 |
tree object 内部以单行记录单个文件或子目录信息,每条记录会依次存储 permissions(权限)、object type(对象类型)、object hash 以及 filename(文件名)。 文件名由 tree object 统一管理,而非文件自身的 blob 内容决定。下文将对该特性的原理展开说明。 当前 tree 中存在一条指向 blob object 的 引用,前往 objects 数据库检索即可确认,就会发现这个第三个 object 确实存在于其中
bash
$ ls .git/objects
01/ ab/ cf/ info/ pack/
$ ls .git/objects/ab/
0d218b097a7cb260b99336411779a703a672e1
最后,我们来查看第三种 object 类型:blob 的内部结构:
bash
$ git cat-file ab0d218b097a7cb260b99336411779a703a672e1 -t
blob
$ git cat-file ab0d218b097a7cb260b99336411779a703a672e1 -p
Rose
第三种 object 类型:blob
Blob 是 binary large object(二进制大对象)的缩写,用于存储文件原始数据,不携带任何与文件相关的 metadata,其中也不含 filename。文件的每一个版本,都由独立 blob 承载。 总结
- Git 包含三类基础 object:blobs、trees、commits。Git 会为每一类 object 计算专属 SHA-1 hash 作为唯一 key,随后将对象内容压缩并持久化存入 repository。
- 一条 commit 会关联一个 tree object,该 tree 以 snapshot(快照)形式 捕获repository 在对应时间点的完整状态;
- tree 可引用 blob,也可嵌套指向其他 tree,以此搭建完整 层级结构;
- blob object 仅单纯存储文件字节内容,无额外附属信息。
开发流程中生成的每一条 commit 均遵循上述存储逻辑,对应 object 会统一存入 Git 的 object database,这便是 Git 存储、管理项目内容的底层实现机制。
本次首个 commit 对应的对象关系图:
bash
+------------------------------+ +--------------------------------------+ +----------------------------------+
| commit 016e263... | | tree cf88f5... | | blob ab0d21... |
+------------------------------+ +--------------------------------------+ +----------------------------------+
| tree cf88f5... | | mode type object name | | content (Plants.txt): |
| author plants-lab |<------>|--------------------------------------|<------>| |
| committer plants-lab | | 100644 blob ab0d21... Plants.txt| | Rose |
| date 2026‑08‑12 15:30:00| | | | |
| | | | | ... |
| Initial commit | | ... ... ... ... | | (文件内容的二进制数据) |
+------------------------------+ +--------------------------------------+ +-----------------------------------+
commit(提交对象) tree(树对象) blob(对象)
记录一次提交的信息,包含作者、提交者、 记录目录结构。每一行代表一个文件或子目录, 保存文件的实际内容(二进制数据),
时间、提交说明等,并指向一个 tree 对象。 包含权限、类型、对象哈希和文件名。 不包含文件名或任何元数据。
可以指向 blob(文件)或其他 tree(子目录)。
+---------------------------------------------------------------------------------------------------+
| 字段说明: mode=权限 type=类型(blob=文件, tree=目录) object=对象的 SHA‑1 哈希 name=文件名/目录名 |
+---------------------------------------------------------------------------------------------------+
| 关系: commit 指向 tree -> tree 指向 blob 或其他 tree -> blob 保存文件内容 |
+---------------------------------------------------------------------------------------------------+
我们先前还留有一个悬而未决的问题,在探讨 trees 与 blobs 的机制时,这个问题至关重要: 为什么文件名是存放在 tree 之中,而不是存放在 blob 里? 这个问题的答案,可以说是理解 Git 过程中,最重要、最让人恍然大悟的关键之一。
第二次 commit
首先,我们在项目的根目录下新增一个名为 tasks/ 的文件夹。在其中,为每个任务创建一个文件,并以负责该任务的人员姓名作为文件名。
第一个任务是plants trees,而负责这项任务的人是我。
bash
$ mkdir tasks
$ printf "Rose" >> ./tasks/plant_trees.txt
$ git add .
$ git commit -m "Create plant the trees task"
[master ff1080d] Create plant the trees task
1 file changed, 1 insertion(+)
create mode 100644 tasks/plant_trees.txt
我们刚刚新增了一条 commit,按照前面学到的知识,objects 数据库目录里理应生成了一批新对象。接下来,我们就来看看具体新增了哪些内容。 和第一次提交时一样,我们先查看这个全新的 commit。这次我们使用 Git 的 --oneline 参数,将每一条 commit 的信息做精简展示。
bash
$ git log --oneline
ff1080d (HEAD -> master) Create plant the trees task
016e263 create Plants file
$ git cat-file ff1080dc44bc51d20bde8432ed5f4630b3c2fe5e -t
commit
$ git cat-file ff1080dc44bc51d20bde8432ed5f4630b3c2fe5e -p
tree c652f10626f677c9452fdfeb4579fec8afced0a0
parent 016e263a1b9b430cf0b1044b59d8e66d57ccc58c
author plants-lab <plants-lab@lab.com> 1786887576 +0700
committer plants-lab <plants-lab@lab.com> 1786887576 +0700
Create plant the trees task
我们得到了第二个 commit 预期的内容,但同时也发现了一个新的要素。这个 commit 里面包含了一个 parent 对象。那么这个 parent 对象到底是什么呢? 它其实就是我们的第一个 commit。你可以对比两者的 ID,以此来验证这一点。 除了第一个 commit 以外,其余所有 commit 都至少拥有一个 parent commit。 现在,我们再来看看 tree 容器:
bash
$ git cat-file c652f10626f677c9452fdfeb4579fec8afced0a0 -t
tree
$ git cat-file c652f10626f677c9452fdfeb4579fec8afced0a0 -p
100644 blob ab0d218b097a7cb260b99336411779a703a672e1 Plants.txt
040000 tree a59dc4bb5f187af5549111d0fee48578bee3ecd7 tasks
tree 内部包含两组对象引用:
- 和上一个 commit 完全相同的 blob 引用,对应文件 Plants.txt
- 一个全新的 tree 引用,对应目录 Tasks 前面我们提到过,tree 的职责就是搭建项目的目录层级结构。本次我们向项目新增了一个文件夹,出现这个新 tree 对象也就不难理解。 指向 Plants.txt 的 blob 引用没有任何改动,也就代表该文件对应的实际内容完全没有发生变化。
bash
$ git cat-file ab0d218b097a7cb260b99336411779a703a672e1 -t
blob
$ git cat-file ab0d218b097a7cb260b99336411779a703a672e1 -p
Rose
让我们一起来查看这个全新的 tree 对象:
bash
$ git cat-file a59dc4bb5f187af5549111d0fee48578bee3ecd7 -t
tree
$ git cat-file a59dc4bb5f187af5549111d0fee48578bee3ecd7 -p
100644 blob ab0d218b097a7cb260b99336411779a703a672e1 plant_trees.txt
正如我们预料的那样,这个 tree 中保存着一个 blob 对象的 ID,该条记录的引用名称为Plant_trees.txt。 最后,我们再来查看这个 blob 对象内部的实际内容。
bash
$ git cat-file ab0d218b097a7cb260b99336411779a703a672e1 -t
blob
$ git cat-file ab0d218b097a7cb260b99336411779a703a672e1 -p
Rose
这里出现了一个很有意思的现象: plant_trees.txt对应的 blob 对象 ID,和 Plants.txt 的 blob ID 是一模一样的。 两个完全独立的文件,对象 ID 按理来说不应当是唯一的吗? 事实上这并不是简单的重复,只是 Git 在对已有数据做高效复用。 Git 检测到这两份文件的内容完全一致,于是它并不会在数据库中生成两份一模一样的对象,而是只创建一个 blob 对象,让两处引用同时指向这同一个 blob。 Git 的内部逻辑大致是这样:
"假如对象库中已经存在这份压缩好的内容,没必要再保存一份一模一样的对象。直接复用这个对象,让不同 tree 里的条目都指向同一个 blob 就可以。"
因此,只要内容完全一致,Git 就会生成完全相同的哈希 key。眼前这个场景正是该规则的实际体现。
Git 有一条底层命令 hash‑object,接收一段内容,返回对应的 hash key。我们可以通过管道传递内容,加上 --stdin 参数,让命令从标准输入读取字符串。
我们示例中的这两个文件,内容都是 Rose。
现在,我们把字符串 Rose 交给 Git,看看会得到什么结果:
bash
$ printf "Rose" | git hash-object --stdin
ab0d218b097a7cb260b99336411779a703a672e1
我们得到了完全一样的 hash key。 意味着,如果我们再新建一个内容为 Rose 的 txt 文件,新的 commit 同样会指向这同一个 blob。 Git 对对象数据库里的对象做了极高效率的管理:只要内容一致,就会直接复用已有的 blob,文件处在哪个目录下并不会对此产生任何影响。 这也解释了为什么文件名要存放在 tree 对象当中。正是这样的设计,才允许不同文件名共同引用同一个 blob。Git 远比我最初想象的要巧妙。 下面就是第二个 commit 的对象关系图:
bash
┌─────────────────────────────┐ ┌─────────────────────────────┐ ┌─────────────────────────────┐
│ commit ff1080.. │ │ tree c652f.. │ │ blob ab0d21.. │
├─────────────────────────────┤ ├─────────────────────────────┤ ├─────────────────────────────┤
│ tree c652f.. │------->│ blob ab0d21.. Plants.txt │------> │ content "Rose" │
│ parent 016e26.. │ │ tree a59dc4.. Tasks │ │ │
│ author plants_lab .. │ └─────────────────────────────┘ └─────────────────────────────┘
│ committer plants_lab .. │ ↓ ↑
└─────────────────────────────┘ └──────────────────> ┌───────────────────────────────┐
│ tree a59dc4.. │
├───────────────────────────────┤
│ blob ab0d21.. Plant_trees.txt │
└───────────────────────────────┘
最后,这就是两个 commit 的 对象关系图: 它们在 objects database 中共享同一个 blob。
bash
┌─────────────────────────────┐ ┌─────────────────────────────┐ ┌─────────────────────────────┐
│ commit ff1080.. │ │ tree c652f1.. │ │ tree a59dc4.. │
├─────────────────────────────┤ ├─────────────────────────────┤ ├──────────────────────────────┤
│ tree c652f1.. │------->│ blob ab0d21.. Plants.txt │ ------>│ blob ab0d21..Plant_trees.txt │
│ parent 016e26.. │ │ tree a59dc4.. tasks │ └──────────────────────────────┘
│ author plants_lab.. │ └─────────────────────────────┘ ↓
│ committer plants_lab.. │ ↓ ↓
└─────────────────────────────┘ └──────────────────────>┌───────────────────────────┐
↓ │ blob ab0d21.. │
↓ ├───────────────────────────┤
┌─────────────────────────────┐ ┌──────────────────────────┐ │ content "Rose" │
│ commit 54725f.. │ │ tree cf88f5..│--->└───────────────────────────┘
├─────────────────────────────┤ ├──────────────────────────┤
│ tree cf88f5.. │------->│ blob ab0d21.. Plants.txt │
│ author plants_lab.. │ └──────────────────────────┘
│ committer plants_lab.. │
└─────────────────────────────┘
2 个 commit、3 个 tree,但只有 1 个 blob。
用 reference 追踪 commit
我们再回到第二个 commit。 可以看到,这里出现了一个名为 parent 的全新对象,它是指向父commit的引用。 正是依靠这个引用,Git 才得以维护提交的先后历史顺序。Git 通过各个 commit 串联形成一条提交链。只要掌握每个 commit 的hash key,我们就可以随时回到项目过去任意一个历史状态。 但哈希是一个 40 位的十六进制数字,对人来说很难记忆。相比一串随机字母与数字组成的字符串,我们更容易记住带有实际语义的单词。 为此 Git 提供了可读性更好的引用(references),帮助我们摆脱冗长哈希的记忆负担。 我们打开仓库根目录下的 refs/ 文件夹,看看示例环境中这里存放了什么内容。
bash
$ ls .git/refs/
heads/ tags/
在 refs/ 文件夹内部,包含 heads/ 和 tags/ 两个子目录。 我们先来打开 heads/ 文件夹,查看其中的内容:
bash
$ ls .git/refs/heads/
master
这就是我们的 master 分支(branch)。Git 在我们初始化 repository 的那一刻就创建了这个分支。
让我们创建一个新的分支:
bash
$ git checkout -b branchA
Switched to a new branch 'branchA'
$ ls .git/refs/heads/
branchA master
分支(branches)本质上就是引用(references),所有分支都存放在 refs/ 目录下的 heads/ 文件夹中。 一个分支在磁盘上的物理形态究竟是什么?
bash
$ cat .git/refs/heads/master
ff1080dc44bc51d20bde8432ed5f4630b3c2fe5e
一个 branch 本质上就是一个 hash。它就是第二个 commit 的 hash。 一个 branch 就是一个指向 commit 的指针(pointer),只是它拥有一个对人类更友好的名称。 说得更简单一点: 一个 branch 本质上就是一个文件,文件里面存放着一个字符串,而这个字符串就是 commit 的 hash key。 借助 Git 的 branch,我们可以在 commit 历史中非常快速地前后切换,也就是在项目的不同历史状态之间来回移动。
bash
$ cat .git/refs/heads/branchA
ff1080dc44bc51d20bde8432ed5f4630b3c2fe5e
如果我们读取 branchA 的内容,会发现它的哈希 key 和 master 完全一致。这是因为该分支从 master 分支创建出来之后,还没有产生过任何新的 commit。 这两个指针,此刻同时指向同一个 commit。 那么it 又是如何判断我们当前正处于哪一个分支呢? Git 会把这个信息保存在仓库根目录下的 HEAD 文件当中。
bash
$ cat .git/HEAD
ref: refs/heads/branchA
这个文件里面存放的并不是哈希值 正如前面所说,只有 object database 中的 objects 才拥有 hash。分支本身并不是对象,它们属于引用;而 HEAD 指向的恰恰是一个分支。 所以,HEAD 存储的是对另一个 reference 的引用。 简单做个总结:
HEAD 是一个指向其他指针的指针。
我们也可以这样来理解这层关系:
bash
┌──────┐ ┌────────┐ ┌─────────────────────────────┐ ┌────────┐
│ HEAD │----> │branchA │------> │ commit ff1080.. │ ←------│ master │
└──────┘ └────────┘ ├─────────────────────────────┤ └────────┘
│ tree c652f1.. │
│ parent 016e26.. │
│ author plants_lab.. │
│ committer plants_lab .. │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ commit 016e26.. │
├─────────────────────────────┤
│ tree cf88f5.. │
│ author plans_lab.. │
│ committer plants_lab .. │
└─────────────────────────────┘
让我们创建一个新的commit,看看会发生什么:
bash
$ git branch
* branchA
master
$ printf "Rose" >> tasks/plant_shop.txt
$ git add .
$ git commit -m "create plant_shop task"
[branchA 7f1598b] create plant_shop task
1 file changed, 1 insertion(+)
create mode 100644 tasks/plant_shop.txt
让我们检查一下引用的当前值:
bash
$ cat .git/refs/heads/master
ff1080dc44bc51d20bde8432ed5f4630b3c2fe5e
$ cat .git/refs/heads/branchA
7f1598bf66023ef3e20caa9bcbca473edacfc48d
$ cat .git/HEAD
ref: refs/heads/branchA
整个过程可以概括为:
- 新的 commit 创建后,branchA 的值会随之更新,因为它现在指向了这个新的 commit。此时,branchA 是当前分支(current branch)。
- HEAD 文件不会发生变化,它仍然指向 branchA。
- master branch 不会发生变化,仍然保持原来的 hash。
bash
┌──────┐ ┌────────┐ ┌─────────────────────────────┐
│ HEAD │----> │branchA │------------------->│ commit 7f1598.. │
└──────┘ └────────┘ ├─────────────────────────────┤
│ tree b544ff.. │
│ parent ff1080.. │
│ author plants_lab.. │
│ committer plants_lab.. │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐ ┌────────┐
│ commit ff1080.. │ ←----- │ master │
├─────────────────────────────┤ └────────┘
│ tree c652f1.. │
│ parent 016e26.. │
│ author plants_lab.. │
│ committer plants_lab.. │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ commit 016e26.. │
├─────────────────────────────┤
│ tree cf88f5.. │
│ author plants_lab.. │
│ committer plants_lab.. │
└─────────────────────────────┘
Tag
我们最后一种 reference 是 tag。 tag 是一种特殊的 reference(引用),用于在 commit 历史中标记某个 commit。 它和 branch 有些相似,但两者有一个关键区别: tag 和 branch 的主要区别在于:commit 创建之后,tag 不会改变它的值。tag 是一个指向特定 commit 的不可变引用(immutable reference)。
注意:tag 主要用于标记代码的发布版本(release version)。这样,无论什么时候通过这个 tag 获取代码,都能得到与该版本发布时完全相同的 snapshot。
Git Tag vs Branch 对比:
| 特性 | Branch(分支) | Tag(标签) |
|---|---|---|
| 本质 | 会自动移动的指针 | 固定不变的引用 |
| 新增 commit 后会怎样 | 自动往前移动,指向最新 commit | 不会移动,永远指向打标签那一刻的 commit |
| 代表的含义 | "现在进行到哪了"(动态进度) | "某个特定的历史节点"(固定时刻) |
| 典型用途 | 日常开发、功能迭代(如 main、dev、feature/xxx) | 标记版本发布点(如 v1.0.0) |
常用命令: git branch 、git checkout git tag 、git tag |
简单示例:
bash
# 分支会跟着新的 commit 移动
main → commitA
git commit -m "新功能"
main → commitB # main 自动更新指向最新的
# tag 一旦打上,永远固定
git tag v1.0 # v1.0 → commitA
git commit -m "新功能"
v1.0 → commitA # tag 依然指着原来那个,不会变
main → commitB # 只有 main 分支往前移动了
让我们创建一个 tag。它会被保存在 refs/ 目录下的 tags/ 子目录中。
bash
$ git branch
* branchA
master
$ git tag v1.0
$ cat .git/refs/tags/v1.0
7f1598bf66023ef3e20caa9bcbca473edacfc48d
如同我们看到的,tag v1.0 本质上只是一个指向某个 commit 的指针------也就是当前这个 commit。 即使以后项目不断发展,branchA 已经沿着 commit 历史向前推进了很多,我们仍然可以通过 tag v1.0 快速回到这个 commit 所对应的项目状态。
总结
让我们总结一下本文最重要的 5 个要点:
- Git 在 object database 中存储三种类型的 object:commits、trees 和 blobs。Git database 中的每个 object 都有一个与之关联的 hash key。
- Git 利用 commits、trees 和 blobs,并使用它们的 hash key 作为指针,高效地构建项目的数据层级结构,同时避免重复存储相同的内容。
- Hash key 很难被人类记忆,因此 Git 提供了一种更友好的方式来引用这些 hash:references(引用)。我们有 branches、HEAD 和 tags。
- branch 是一个指向 commit 的指针。Git 默认的 branch 名称是 master,但你可以创建任意数量的 branch。HEAD reference 告诉我们当前正在使用哪个 branch。每当创建一个新的 commit 时,branch 指针都会自动向前移动。
- 最后,tag reference 是不可变的(immutable),它始终指向同一个 commit,因此可以用来永久标记某个时间点的项目状态。