Git 核心概念
理解 Git 的底层模型是熟练使用 Git 的基础。本章从数据结构角度解释 Git 是如何存储和管理版本的。
三个工作区域
工作目录 暂存区 本地仓库 远程仓库
(Working Dir) (Staging/Index) (Local Repo) (Remote)
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ 文件 │ ─add→ │ 快照 │ ─commit→│ 版本 │ ─push→ │ 共享 │
│ 修改 │ │ │ │ 历史 │ │ 仓库 │
└──────┘ └──────┘ └──────┘ └──────┘
↑ │ │
└───────── checkout / restore ─────┘ │
│
└────────────────── pull / fetch ──────────────────┘| 区域 | 存储位置 | 操作 |
|---|---|---|
| 工作目录 (Working Directory) | 磁盘上的实际文件 | 编辑、创建、删除文件 |
| 暂存区 (Staging Area / Index) | .git/index |
git add 添加 / git reset 移除 |
| 本地仓库 (Repository) | .git/objects/ |
git commit 提交 / git checkout 检出 |
💡 理解暂存区:Git 与其他 VCS 的最大区别——你提交的不是工作目录的当前状态,而是暂存区中的快照。这意味着你可以精确控制每次 commit 包含哪些修改。
Git 对象模型
🔬 深入原理:Git 本质上是一个内容寻址的文件系统,核心数据结构是四种对象:
.git/objects/
├── xx/ ← 用 SHA-1 哈希的前两位作为目录名
│ └── xxxxxxxxxx ← 剩余 38 位作为文件名四种对象类型
| 对象 | 存储内容 | 用途 |
|---|---|---|
| blob | 文件内容(不含文件名) | 每个文件版本存储为一个 blob |
| tree | 目录结构(文件名 + blob/tree 引用) | 对应一个目录的快照 |
| commit | 一次提交(tree + parent + 作者 + 信息) | 版本历史的节点 |
| tag | 带注释的标签引用 | 标记里程碑 |
commit (C3)
├── tree: 24b9f...
├── parent: C2
├── author: xxx
└── message: "fix bug"
└──→ tree (24b9f)
├── blob a906c... README.md
├── blob 8b137... main.py
└── tree 0c1f4... (src/)
├── blob 91b1d... utils.py
└── blob 3c4e5... app.py文件名和目录结构存储在 tree 对象中,文件内容存储在 blob 对象中。如果两个文件内容相同,它们共享同一个 blob(节省空间)。
手动查看对象
# 查看对象类型
git cat-file -t <hash>
# 查看对象内容
git cat-file -p <hash>
# 查看 commit 对象示例
git cat-file -p HEAD
# 输出:tree abc123...
# parent def456...
# author xxx
# committer xxx
# 提交信息HEAD 与引用
HEAD
HEAD 是一个符号引用,指向当前检出的分支或提交:
# HEAD 指向分支
HEAD → main → C3
# 分离 HEAD(Detached HEAD)
HEAD → C2 (直接指向提交而非分支)# 查看 HEAD 内容
cat .git/HEAD
# ref: refs/heads/main ← 指向分支
# 或
# ca82a6dff817... ← 分离 HEAD,直接是 hash
# 查看 HEAD 实际指向
git symbolic-ref HEAD # 如果指向分支
git rev-parse HEAD # 无论如何都返回 commit hash分支(Branch)
分支只是一个指向 commit 的轻量级指针:
# 查看分支文件内容
cat .git/refs/heads/main
# ca82a6dff817ec66f44342007202690a93763949 ← 一个 commit hash
# 创建分支 = 创建一个新指针文件
git branch feature
# 在 .git/refs/heads/feature 中写入当前 HEAD 的 hash💡 这就是为什么 Git 分支如此轻量——创建分支只是写入一个 40 字符的文件。
标签(Tag)
# 轻量标签(lightweight):只是一个引用
cat .git/refs/tags/v1.0
# ca82a6dff817...
# 注释标签(annotated):是一个 tag 对象,包含元数据
git cat-file -p v1.0
# object ca82a6d...
# type commit
# tag v1.0
# tagger xxx引用语法(Relative Refs)
在分支名后加操作符来相对定位:
# ^:向上移动 1 个提交(遇到 merge commit 时选第 1 个 parent)
HEAD^ # HEAD 的 parent
main^^ # main 的 parent 的 parent
main^2 # main 的第 2 个 parent(merge commit 时用)
# ~:向上移动 N 个提交(始终跟随第一个 parent)
HEAD~1 # 等价于 HEAD^
HEAD~3 # 向上 3 个提交
main~5
# 组合使用
HEAD~2^2 # 先向上 2 个,再选第 2 个 parent| 操作符 | 含义 | 示例 |
|---|---|---|
^ |
向上 1 个,可选 parent 编号 | HEAD^2 |
~n |
向上 n 个(沿第一个 parent) | HEAD~3 |
提交历史
有向无环图(DAG)
Git 的提交历史是一个 DAG(Directed Acyclic Graph):
C0 ← C1 ← C2 ← C3 (main)
↘
C4 ← C5 (feature)- 每个 commit 有一个或多个 parent
- merge commit 有 2 个(或更多)parent
- 历史不可变——修改历史其实是创建新的 commit 链
Git 如何标识 commit
git log --oneline
# a1b2c3d (HEAD -> main) Fix login bug
# e4f5g6h Add user model
# i7j8k9l Initial commit每个 commit 的 SHA-1 哈希由以下内容计算得出:
commit 对象的内容(tree + parent + author + committer + message + timestamp)
│
▼
SHA-1 哈希 → a1b2c3d...🚨 哈希的确定性:相同的内容会产生相同的哈希。这意味着你不能"修改"一个 commit——你只能创建一个新的 commit,它有不同的内容,因此有不同的哈希。
文件状态流转
Untracked (未跟踪)
│ git add
▼
Staged (已暂存)
│ git commit
▼
Unmodified (未修改)
│ 编辑文件
▼
Modified (已修改)
│ git add
└──→ Staged
# 查看文件状态
git status
git status -s # 简短格式| 状态标记 | 含义 |
|---|---|
?? |
Untracked(新文件) |
A |
已添加到暂存区 |
M |
暂存区中有修改 |
M |
工作目录中有修改(未暂存) |
D |
已删除并暂存 |
D |
工作目录中已删除(未暂存) |
.git 目录结构
.git/
├── HEAD # 当前检出分支/commit
├── config # 仓库级配置
├── description # 仓库描述(GitWeb 用)
├── index # 暂存区(二进制文件)
├── hooks/ # 客户端钩子脚本
├── objects/ # 所有对象(blob/tree/commit/tag)
│ ├── pack/ # 打包压缩后的对象
│ └── ...
├── refs/ # 引用
│ ├── heads/ # 分支引用
│ ├── tags/ # 标签引用
│ └── remotes/ # 远程引用
├── logs/ # 引用日志(reflog)
└── info/
└── exclude # 本地排除规则(类似 .gitignore,但不提交)