本篇重点:Git 初始化、暂存区机制、对象模型(commit / tree / blob / refs)。
本文沿用书中
master作为默认分支名。Git 2.28+ 默认改为main,行为完全一致。
Git 初始化
全局配置:用户身份与别名
提交时记入 author / committer 信息,全局配置一次即可(写入 ~/.gitconfig):
git config --global user.name "Jiang Xin"
git config --global user.email jiangxin@ossxp.com
常用别名:
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.co checkout
git config --global alias.st status
创建仓库
git init demo # 等价于 mkdir demo && cd demo && git init
cd demo
ls -aF # → ./ ../ .git/
.git 是版本库目录,其所在目录称为 工作区。
子目录中执行 Git 命令时,Git 沿目录向上递归查找 .git。可用 strace 验证:
strace -e 'trace=file' git status
定位版本库与工作区根:
git rev-parse --git-dir # → /path/to/demo/.git
git rev-parse --show-toplevel # → /path/to/demo
三层配置文件
| 层级 | 路径 | 命令 | 优先级 |
|---|---|---|---|
| 仓库 | <repo>/.git/config | git config -e | 最高 |
| 全局 | ~/.gitconfig | git config -e --global | 中 |
| 系统 | /etc/gitconfig | git config -e --system | 最低 |
配置文件是 INI 格式,可读可写:
git config core.bare # 读
git config a.b something # 写 → [a] b = something
git config x.y.z others # 写 → [x "y"] z = others
git config 也能操作任意 INI 文件:
GIT_CONFIG=test.ini git config a.b.c.d "hello, world"
修补错误的提交者信息
漏配 user.name 时已经提交了?重置后 amend:
git config --global user.name "Jiang Xin"
git commit --amend --allow-empty --reset-author
--amend:修补最近一次提交,不产生新提交。--allow-empty:允许空白提交(修补后无文件变化时需要)。--reset-author:同时重置 Author 与 AuthorDate;不加只会改 Committer。
Git 不做提交者身份认证,“谁都能写任意 name/email” 是分布式特性。权限控制交给 GitLab / GitHub 等服务端。
Git 暂存区(stage / index)
工作区 → 暂存区 → 版本库
修改文件后必须先 add 再 commit:
git add welcome.txt
git diff # 工作区 vs 暂存区
git diff --cached # 暂存区 vs HEAD
git status -s # 简洁状态:?? 未跟踪 / A 新增 / M 暂存 / M 仅工作区改 / MM 都改
git commit -m "which version checked in?"
git log --pretty=oneline
git commit -a自动 add 所有已跟踪文件的改动后提交,会丢掉"挑选改动入提交"的能力,慎用。
index 文件的本质
.git/index 是一棵虚拟工作区目录树,记录每个跟踪文件的名字、时间戳、长度,以及指向 .git/objects/ 中实际内容的对象 ID。

关键操作的边界:
| 命令 | 暂存区 | 工作区 |
|---|---|---|
git reset HEAD | 重置为 HEAD | 不变 |
git rm --cached <file> | 删除 | 不变 |
git checkout -- <file> | 不变 | 用暂存区覆盖 ⚠ |
git checkout HEAD -- <file> | 用 HEAD 覆盖 | 用 HEAD 覆盖 ⚠ |
Git 2.23+ 推荐
git restore/git restore --staged替代checkout,语义更清晰:restore只动文件,switch只切分支。
查看暂存区与 HEAD 的目录树
git ls-tree -l HEAD # HEAD 的 tree
git ls-files -s # 暂存区的 tree

Git 对象模型
Git 对象库 .git/objects/ 中只有四种对象:blob(文件内容)、tree(目录)、commit(提交)、tag(附注标签)。每个对象用 40 位 SHA-1 唯一寻址。
三种对象的关系
git log -1 --pretty=raw
# commit e695606fc5e31b2ff9038a48a3d363f4c21a3d86 ← 本次提交
# tree f58da9a820e3fd9d84ab2ca2f1b467ac265038f9 ← 提交对应的目录快照
# parent a0c641e92b10d8bcca1ed1bf84ca80340fdefee6 ← 上一次提交
git cat-file 查类型与内容:
git cat-file -t e695606 # → commit
git cat-file -t f58d # → tree
git cat-file -t fd3c06 # → blob
git cat-file -p fd3c06 # → 文件内容
对象在磁盘上按 SHA-1 前 2 位分目录、后 38 位作文件名:
ls .git/objects/e6/95606fc5e31b2ff9038a48a3d363f4c21a3d86

HEAD 与 master 的关系
cat .git/HEAD # → ref: refs/heads/master
cat .git/refs/heads/master # → e695606fc5e31b2ff9038a48a3d363f4c21a3d86
HEAD是符号引用,指向当前分支。refs/heads/master是分支文件,内容为最新提交的 SHA-1。- 在 master 分支上时,
HEAD≡master≡refs/heads/master。
.git/refs/ 是引用命名空间,refs/heads/ 下是分支,refs/tags/ 下是标签。

SHA-1 是怎么算出来的
Git 对象 ID = SHA1("<type> <size>\0<content>")。
注意 Git 不是直接对内容做 SHA-1,而是先在前面拼一个 header:<type> 是 commit / tree / blob / tag 之一,<size> 是内容字节数,\0 是分隔符。因为 header 里带 type,同样的字节当 blob 和当 tree 算出的 ID 不同——这就是 Git 区分四类对象的方式。
手工复算 commit,验证公式:
git cat-file commit HEAD | wc -c # → 234 content 字节数
( printf "commit 234\000"; git cat-file commit HEAD ) | sha1sum
# → e695606fc5e31b2ff9038a48a3d363f4c21a3d86 手工算的
git rev-parse HEAD
# → e695606fc5e31b2ff9038a48a3d363f4c21a3d86 Git 自己给的,完全一致
小括号 ( ; ) 把 header 和 content 串成一段流喂给 sha1sum。把 commit 换成 blob / tree、size 换成对应字节数,同样成立。git hash-object 就是这套"拼 header + SHA-1"的官方封装。
由此可推出 Git 几个核心性质:
- 任何字节级改动都会产生新 ID;commit 里又包含 parent ID,所以改一处历史会让后续所有 commit ID 雪崩——这是 Git 不可篡改的根。
- 不同 type 的对象 ID 永不冲突,因为 header 不同。
- 部分 SHA-1 前缀(默认 7 位)就能寻址,是因为单仓库的对象数远小于 16⁷ ≈ 2.7 亿。
SHA-1 不是数学上唯一,但 160 位空间 + 抗碰撞设计使实际工程中可视为唯一。Git 2.29+ 已在向 SHA-256 演进。
提交的引用语法
集中式系统能用递增编号,是因为只有一个写入点;Git 是分布式的,必须用全球可重现的对象 ID 寻址。
| 写法 | 含义 |
|---|---|
master / refs/heads/master | 分支最新提交 |
HEAD | 当前分支最新提交 |
HEAD^ / HEAD~1 | 父提交 |
HEAD^^ / HEAD~2 | 祖父提交 |
HEAD^2 | 第二个父提交(merge 才有) |
a573106^{tree} | 该提交对应的 tree |
a573106:path/to/file | 该提交里某个文件的 blob |
:path/to/file | 暂存区里的 blob |
部分 SHA-1 前缀(不冲突即可)也能寻址:
git cat-file -p e695
git cat-file -p e695^
git rev-parse e695^{tree}