本篇重点: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/configgit config -e最高
全局~/.gitconfiggit config -e --global
系统/etc/gitconfiggit 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)

工作区 → 暂存区 → 版本库

修改文件后必须先 addcommit

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 stage

关键操作的边界:

命令暂存区工作区
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 diff

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

git objects

HEAD 与 master 的关系

cat .git/HEAD                          # → ref: refs/heads/master
cat .git/refs/heads/master             # → e695606fc5e31b2ff9038a48a3d363f4c21a3d86
  • HEAD 是符号引用,指向当前分支。
  • refs/heads/master 是分支文件,内容为最新提交的 SHA-1。
  • 在 master 分支上时,HEADmasterrefs/heads/master

.git/refs/ 是引用命名空间,refs/heads/ 下是分支,refs/tags/ 下是标签。

git refs

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 / treesize 换成对应字节数,同样成立。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}

References