Git 内部机制和历史编辑

最后更新: 2024-03-07 13:17:55 +0800 +0800
原文以英文撰写。您正在阅读AI翻译版本。

Git 存储不可变对象,并在这些对象之间移动小型引用。日常工作应使用 porcelain 命令,而存储模型可以解释为什么重写会创建新历史,以及为什么在一段时间内仍有可能恢复。

检查仓库数据库 #

.git 目录包含仓库本地状态:

路径用途
HEAD已检出分支的符号名称,或分离状态下的对象 ID
config仓库配置
index下一次提交的候选快照
objects/松散对象和 packfile
refs/ 和 packed-refs分支、标签和远程跟踪名称
logs/引用的本地 reflog
hooks/客户端钩子样例和已安装钩子
modules/已初始化子模块的 Git 目录

工作树保存已检出文件。索引记录候选树。提交将树与父 ID 和元数据关联起来。分支是指向提交的可移动引用,而不是可变的提交容器。

无需手动解码文件即可检查名称和对象类型:

sh
git rev-parse --git-dir
git show-ref --head
git cat-file -t HEAD
git cat-file -p HEAD
git count-objects -vH

对象模型和内容寻址 #

Git 通过对规范字节序列进行哈希来标识对象:

text
<type><space><decimal-size><NUL><content>

SHA-1 仓库使用 160 位对象 ID。新仓库可以使用 SHA-256。对象 ID 标识内容和结构; 它不编码时间或序列号。

Git 有四种核心对象类型:

  1. blob 存储文件字节,不包含文件名。
  2. tree 将路径名和模式映射到 blob 和子树。
  3. commit 命名根树、零个或多个父级、作者和提交者身份及时间,以及消息。
  4. 带注释的 tag 存储标签元数据,并指向另一个对象。

树条目模式 100644 表示普通的不可执行文件,100755 表示可执行文件,120000 表示符号链接,160000 表示子模块 gitlink。gitlink 指向另一个仓库中的提交,而不是嵌入该仓库的文件。

在临时仓库中创建并检查对象:

sh
printf 'hello\n' | git hash-object -w --stdin
git cat-file -p <object-id>
git ls-tree HEAD
git ls-tree -r HEAD

松散对象位于 .git/objects/aa/bb... 下,其中 aa 是十六进制 ID 的第一个字节。 Git 会对它们的规范表示进行 zlib 压缩。git cat-file 比手动添加压缩头或使用临时解压管道更安全。

了解 Pack、增量和可达性 #

许多松散对象会产生文件系统元数据操作。git gc 可以将可达对象收集到 .git/objects/pack/ 中的 packfile。.pack 文件保存对象表示,.idx 文件将对象 ID 映射到 pack 偏移量,.rev 文件可以加速需要 pack 顺序的操作。

Packfile 可以相对于其他对象对对象进行增量压缩。增量是存储优化,而不是 Git 的历史模型: 每次提交仍然命名完整的根树,Git 可以通过对象图重建任何被引用的 blob。提交不会只存储相对于父级的文本差异。

可达性从分支、标签、远程跟踪分支和 reflog 等引用开始。垃圾回收会保留可达对象,并可以在配置的过期时间后清理不可达对象。git fsck 报告连通性,并可以定位不可达对象:

sh
git fsck --full --no-reflogs --unreachable
git cat-file -p <unreachable-object-id>

读取引用和索引 #

HEAD 通常包含 ref: refs/heads/main。分离检出状态会直接包含对象 ID。松散分支名称是 refs/heads/ 下的文件,而许多引用可以合并到 packed-refs 中。

索引不是变更文件列表。它存储下一棵树的路径、模式、对象 ID 和 stat 缓存。合并冲突期间, 索引可以为一个路径携带多个阶段:

阶段含义
1合并基
2当前分支版本
3其他分支版本

使用以下命令检查索引条目:

sh
git ls-files --stage
git diff --cached

解决冲突并运行 git add 后,会将阶段 1–3 替换为一个阶段 0 条目。

解释重写为何会改变 ID #

提交包含其父对象 ID。更改消息、树、作者、提交者、时间戳或父级都会产生不同的提交 ID。 因为记录的父级发生变化,所以每个后代都必须重新创建。

如果能改善历史,可以自由重写本地未发布工作。重写共享分支前应先协调。强制更新可能丢弃协作者新的远程提交;必须强制推送时使用 --force-with-lease:

sh
git push --force-with-lease origin feature/topic

修改最新提交 #

git commit --amend 会为 HEAD 创建替代提交并移动当前分支:

sh
git add README.md
git commit --amend --no-edit

不带 --no-edit 运行 git commit --amend 会替换消息。原始提交在过期和清理之前仍可通过 reflog 恢复。

交互式编辑较早提交 #

交互式 rebase 会重写所选终点之后的提交:

sh
git rebase -i HEAD~3

命令会显示最近三个提交。其 todo 命令包括:

命令作用
pick保留提交
reword替换消息
edit应用后停止
squash与前一提交合并并编辑消息
fixup合并并丢弃其消息
drop忽略提交

要更改较早提交的内容,将其标记为 edit,rebase 停止时进行 amend,然后继续:

sh
git add path/to/file
git commit --amend --no-edit
git rebase --continue

解决冲突,暂存已解决的路径,然后继续。git rebase --abort 会将分支恢复到 rebase 之前的状态。rebase 暂停时,git rebase --edit-todo 可以更改剩余指令。

使用 Reflog 恢复 #

每次本地引用移动都可以有一条 reflog 条目。在有风险的重写前创建明确的备份名称:

sh
git branch backup/before-rewrite

然后定位并命名较早的状态:

sh
git reflog show --date=iso HEAD
git switch -c recovered-work HEAD@{1}

Reflog 是本地实现历史,而不是分布式备份。发布重要工作,保留远程备份,并避免在恢复完成前运行破坏性清理。

评论

在 GitHub 上阅读或评论(发表评论需要登录)