Skip to content
Git
Git 核心概念

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,但不提交)