目录

git-03 Git原理

1. Git 的本质:内容寻址文件系统

Git 本质上是一个以内容寻址的文件系统,在此之上构建了版本控制功能。

所有数据以对象形式存储在 .git/objects/ 目录下,每个对象用其内容的 SHA-1 哈希值(40位十六进制)命名。

.git/
├── HEAD          # 当前所在分支/提交
├── config        # 仓库配置
├── index         # 暂存区
├── objects/      # 所有 Git 对象
│   ├── info/
│   └── pack/
└── refs/         # 引用(分支、标签)
    ├── heads/    # 本地分支
    ├── remotes/  # 远程分支
    └── tags/     # 标签

2. 四种对象类型

2.1 Blob(文件内容)

存储文件内容,不包含文件名。相同内容的文件只存储一份。

echo "hello git" | git hash-object -w --stdin

git cat-file -p 8d0e41

git cat-file -t 8d0e41

对象存储路径:.git/objects/8d/0e41234f24b6da...(前2位为目录名)

2.2 Tree(目录结构)

存储目录结构,包含文件名、权限及指向 blob/tree 的引用。

git cat-file -p HEAD^{tree}

格式:<mode> <type> <SHA-1>\t<name>

mode 说明
100644 普通文件
100755 可执行文件
120000 符号链接
040000 目录(tree)
160000 Submodule(gitlink)

2.3 Commit(提交)

存储一次提交的元数据:作者、时间戳、提交消息,以及指向 tree 和父提交的引用。

git cat-file -p HEAD
# tree 9b1a2f3...          ← 指向根 tree
# parent a1b2c3...         ← 父提交(merge 提交有两个 parent)
# author  Liu <l@x.com> 1710000000 +0800
# committer Liu <l@x.com> 1710000000 +0800
#
# feat: add login feature

提交链:

commit C  →  commit B  →  commit A (初始提交,无 parent)
  |              |              |
tree T3       tree T2        tree T1

2.4 Tag(标签对象)

附注标签(annotated tag)会创建 tag 对象,包含标签者、时间、消息及指向被标记对象的引用。

git cat-file -p v1.0.0
# object a1b2c3...
# type commit
# tag v1.0.0
# tagger Liu <l@x.com> 1710000000 +0800
#
# Release version 1.0.0

轻量标签只是一个引用(ref),不创建 tag 对象。


3. 对象存储结构

Git 存储对象的格式:

内容 = "<type> <size>\0<data>"
SHA1 = sha1(内容)
存储 = zlib压缩(内容)
路径 = .git/objects/<SHA1前2位>/<SHA1后38位>
python3 -c "
import zlib, sys
data = open('.git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f', 'rb').read()
print(zlib.decompress(data))
"

4. 引用(References)

引用是指向 SHA-1 的具名指针,存储在 .git/refs/ 下。

cat .git/refs/heads/main

cat .git/HEAD

HEAD 的两种状态:

cat .git/HEAD

cat .git/HEAD

4.1 引用更新过程

git commit 执行时:

1. 将暂存区生成 tree 对象
2. 创建 commit 对象(包含 tree SHA + parent SHA + 元数据)
3. 更新 HEAD 指向的分支引用到新 commit SHA

5. 暂存区(Index)

.git/index 是一个二进制文件,记录了暂存区的状态:

  • 文件路径
  • 文件权限
  • 对应 blob 的 SHA-1
  • stat 信息(mtime、ctime、size 等,用于快速检测文件是否变更)
git ls-files --stage

git status 的工作原理:

  1. 比较工作区文件的 stat 与 index 中记录的 stat → 判断是否有未暂存修改
  2. 比较 index 中的 SHA-1 与 HEAD commit 中 tree 的 SHA-1 → 判断是否有已暂存修改

6. 分支的本质

分支只是一个文件,内容是 commit 的 SHA-1。

cat .git/refs/heads/main

echo "a1b2c3d4e5f6..." > .git/refs/heads/new-branch

这就是为什么 Git 分支创建如此廉价——只需写入40字节。

6.1 merge 的本质

Fast-forward merge: 直接移动分支指针,不创建新对象。

Three-way merge: 找到两分支的公共祖先,生成合并提交。

       A──B──C  (main)
      /
base─
      \
       D──E──F  (feature)

合并时:
- 找公共祖先 base
- 对比 base→C 的变更和 base→F 的变更
- 合并变更,冲突需手动解决
- 创建新提交 M(有两个 parent:C 和 F)

6.2 rebase 的本质

rebase 不是"移动提交",而是重新应用变更

原始:
main:    A──B──C
              \
feature:       D──E

rebase feature onto main:
1. 找到 feature 和 main 的公共祖先(B)
2. 把 feature 上 B 之后的提交(D、E)生成 patch
3. 在 main 的 C 上依次应用这些 patch,生成新提交 D'、E'
4. 移动 feature 分支指针到 E'

result:
main:    A──B──C
                \
feature:         D'──E'

D’ 和 D 的内容相同,但 SHA-1 不同(parent 不同)。


7. Pack 文件

随着提交增多,松散对象(loose objects)数量会很多,Git 会自动将它们打包为 pack 文件以节省空间。

git gc

ls .git/objects/pack/

Pack 的压缩策略:

Git 不是存储每个版本的完整文件,而是存储差量(delta)

版本1(完整): blob 完整内容(base)
版本2:        delta = 版本2 相对版本1 的差异
版本3:        delta = 版本3 相对版本2 的差异

查找对象时通过 .idx 索引快速定位,再根据 delta 链重建完整内容。

git count-objects -vH

8. 底层命令(Plumbing Commands)

Git 分为两类命令:

  • Porcelain(瓷器):用户友好的高级命令(commitmergepush
  • Plumbing(管道):底层命令,供脚本调用
git hash-object -w file.txt      # 写入 blob 对象,返回 SHA-1
git cat-file -t <sha>            # 查看对象类型
git cat-file -p <sha>            # 查看对象内容
git cat-file -s <sha>            # 查看对象大小

git ls-tree HEAD                 # 查看 HEAD 的 tree
git write-tree                   # 将暂存区写成 tree 对象

git commit-tree <tree> -p <parent> -m "msg"  # 手动创建 commit

git update-ref refs/heads/main <sha>  # 更新引用
git symbolic-ref HEAD                  # 查看/设置符号引用

git ls-files --stage             # 查看暂存区
git update-index --add file.txt  # 手动添加到暂存区

git diff-tree -p HEAD            # HEAD 的 tree diff
git diff-index HEAD              # 暂存区与 HEAD 的 diff

手动创建一次提交(不用 git commit):

blob=$(echo "hello" | git hash-object -w --stdin)

git update-index --add --cacheinfo 100644,$blob,hello.txt
tree=$(git write-tree)

commit=$(git commit-tree $tree -m "manual commit")

git update-ref refs/heads/main $commit

9. 数据完整性

Git 用 SHA-1 保证数据完整性:

  • 每个对象的名称就是其内容的哈希值
  • 修改任何内容会产生不同的 SHA-1
  • Commit 包含 tree 的 SHA-1,tree 包含 blob 的 SHA-1,形成哈希链
  • 任何历史记录的篡改都会导致 SHA-1 不匹配,立即被检测到
git fsck

git fsck --full

10. 垃圾回收

某些操作会产生"悬挂对象"(dangling objects)——没有任何引用指向它们:

  • git reset --hard:旧提交变为悬挂
  • git commit --amend:旧提交变为悬挂
  • 删除分支:该分支上未合并的提交变为悬挂

这些对象默认保留 90 天(通过 reflog 可恢复),之后被 git gc 清理。

git fsck --lost-found

git show a1b2c3

git reflog expire --expire=now --all
git gc --prune=now

11. 总结:一次 git commit 发生了什么

git commit -m "feat: add login"

1. 读取暂存区(.git/index)
   ↓
2. 为每个暂存的文件创建/复用 blob 对象
   ↓
3. 递归创建目录的 tree 对象
   ↓
4. 创建 commit 对象
   - 包含根 tree 的 SHA-1
   - 包含父提交的 SHA-1
   - 包含作者/提交者信息
   - 包含提交消息
   ↓
5. 更新 HEAD 指向的分支引用
   .git/refs/heads/main → 新 commit SHA-1

整个过程:写对象(不可变) + 移动指针,这正是 Git 快速、安全、可回溯的根本原因。