Git 实际是如何思考的(以及为什么大多数开发者理解错误)

发布: (2026年3月15日 GMT+8 16:25)
13 分钟阅读
原文: Dev.to

看起来您只提供了来源链接,而没有贴出需要翻译的正文内容。请把要翻译的文本粘贴进来,我会按照要求保留链接、格式和技术术语进行简体中文翻译。

Part 1 of the Git Mastery Series

以下是每个开发团队大约每月都会出现的一段对话:

  • 有人误用了 git reset --hard,本想执行别的操作。
  • 他们进行 rebase,结果历史完全乱了。
  • 他们合并分支,却弄不清为什么某些改动没有生效。

于是他们在聊天里打字:“我觉得我把 Git 弄坏了。”

你不可能把 Git 弄坏。但当你的思维模型与 Git 实际的工作方式不匹配时,确实会让自己感到困惑。

大多数 Git 教程只教命令,几乎不教 Git 的思考方式。这篇文章弥补了这个空白——因为一旦模型恰到好处,命令就不再是从 Stack Overflow 复制的咒语,而是你可以有意识做出的决策。

这是理解 Git 最重要的点,也是大多数教程要么跳过要么埋在第 10 章的内容。

当你运行 git commit 时,Git 不会 存储“自上次以来的改动”。它会在那个时刻 完整地快照 项目中每个受跟踪文件的内容。如果文件没有变化,Git 并不会复制它——只会指向前一次快照中相同的内容。概念上,每一次提交都是项目的完整图像,而不是改动列表。

为什么这很重要?

因为它解释了几乎所有让人困惑的现象。

  • 当你 cherry‑pick(挑拣)一次提交时,Git 并不是在“移动”改动——它是在把该提交与其父提交之间的差异应用到你当前的状态上。
  • 当你 rebase(变基)时,Git 并不是在“移动提交”——它是在新的基底之上重新播放一系列差异。
  • 当你 reset(重置)时,你只是把指针移动到另一个快照。真正被删除的只有在 Git 的垃圾回收器运行时才会真正消失。

这正是 Git 如此强大的原因——也是那些听起来具有破坏性的操作通常并不真的会破坏任何东西的原因。

Git的三个区域

区域它是什么它的作用
工作目录你的实际文件——在编辑器中看到的、可以运行和测试的文件。Git 知道这个区域,但不直接管理它。
暂存区(索引)你准备下一个提交的地方。git add 将更改移入等候室,表示“把它包含在下一个快照中”。
仓库.git 文件夹)提交永久保存的地方。git commit 将暂存区的内容包装成一个新快照。

为什么这很重要

大多数开发者会把 git add .git commit -m "…" 当成一次性操作,根本不考虑暂存区。这在一般情况下没问题,直到你需要:

  • 提交文件的部分内容
  • 只撤销部分更改
  • 查明为什么提交里出现了你不想要的内容

这时,这个模型就能帮助你。

一次性查看三个区域的区别

git diff           # 工作目录 vs 暂存区
git diff --staged  # 暂存区 vs 最近一次提交
git status         # 三个区域的概览

在做了一些更改后运行这些命令。仔细阅读输出。你会立刻看到哪些更改“悬而未决”,哪些已经提交。

分支只是指针

Git 分支听起来像是个复杂的东西——代码的平行宇宙,独立的开发轨道。实际上实现得几乎滑稽地简单:分支就是一个 包含 40 字符提交哈希的文本文件。仅此而已。

  • 分支是指向一次提交的指针。
  • 当你在分支上创建新提交时,指针会前移到新的提交。
  • 当你创建分支时,Git 会把该指针复制到一个新文件。没有代码的复制,没有平行宇宙——只有一个指针。

看看分支到底是什么

cat .git/refs/heads/main
# Output: a3f8c9d1e2b4f6a8c0d2e4f6a8b0c2d4e6f8a0b2

那个哈希 就是 你的整个分支。分支名称只是该提交的可读别名。

后果

  • 创建分支是免费的——因为你只是在写一个文件,所以几乎是瞬时完成。
  • 切换分支很快,因为你只是在移动指针并更新工作目录以匹配目标快照。

“我会为此创建一个分支” 永远不应该是一项沉重的决定。

HEAD: 你现在所在的位置

HEAD 是一个文件。它包含分支名或提交哈希。它回答一个问题:“我现在在哪里?”

当你在 main 分支并运行 git log 时,你会看到 main 的历史,因为 HEAD 指向 main,而 main 指向它的最新提交。当你提交时,HEAD 会自动向前移动。

cat .git/HEAD
# Output: ref: refs/heads/main

HEAD 包含分支名时,你处于 普通模式
HEAD 直接包含提交哈希——而不是分支名——时,你处于 分离的 HEAD 状态。

分离的 HEAD 听起来令人担忧,但它只是表示你正在查看历史中的特定提交,而不是某个分支。如果你在此状态下进行提交,它们最终会被垃圾回收,因为没有分支指针随之移动。要保留在分离的 HEAD 中所做的工作,创建一个分支:

git switch -c my-new-branch

提交及其内部结构

每个提交(除最初的那个)都指向它的父提交。这就是 Git 知道“之前是什么”的方式。一次提交是一个对象,包含:

  • 指向快照的指针(tree
  • 指向父提交的指针(可多个)
  • 提交信息
  • 作者和时间戳

查看任意提交的原始内容

git cat-file -p HEAD
# Output:
# tree 8a3f2d9c...
# parent 1b4e7a2f...
# author Shakil  1709123456 +0530
# committer Shakil  1709123456 +0530
#
# feat(auth): add OTP login

父指针链构成了你的历史。当你运行 git log 时,Git 从 HEAD 开始,向后遍历父链。当你运行 git merge 时,Git 通过向后遍历两条链来找到共同祖先。

这就是为什么看似神奇的 Git 操作——查找合并冲突、显示 blame、运行 bisect——实际上都是机械的。Git 只是在遍历一个链表。

可恢复性

大多数 Git 焦虑来源于不知道哪些内容是可恢复的。事实是:几乎所有内容都是可恢复的。当你执行 reset 时,提交并不会被删除——它们会变成未引用的对象。它们仍然存在于对象数据库中,直到 Git 的垃圾回收器将其清除。

(原文在此处意外中断;核心信息是 Git 会保留数据,直到显式清理为止。)

恢复已删除的提交

git reflog 会显示 HEAD 曾经所在的每一个位置,包括当前没有任何分支指向的提交。你大约有 90 天的窗口 可以在垃圾回收运行之前恢复任何内容。

# 查看 HEAD 曾指向的所有位置
git reflog

示例输出

a3f8c9d HEAD@{0}: commit: fix login bug
1b4e7a2 HEAD@{1}: reset: moving to HEAD~1
c5d2f8a HEAD@{2}: commit: add OTP login

你使用 git reset --hard “删除”的提交仍然存在于 HEAD@{1}。将其恢复:

git reset --hard HEAD@{1}

思维转变

一旦把 Git 看作 快照数据库,Git 操作就不再显得脆弱:

  • 提交(Commits) – 不可变的快照,记录着它们的父节点。
  • 分支与 HEAD – 只是指向该数据库的指针。
  • 工作目录 – 你对某个快照的视图。
  • 暂存区 – 你构建下一个快照的地方。

当你理解了:

  • 提交是永久的。
  • 分支是可移动的。
  • HEAD 仅仅是 “你现在所在的位置”。

…Git 就成了一个可以推理的工具,而不是一堆令人害怕的命令。

所有 Git 命令都在操作 快照指针,或三个区域(工作树、索引、仓库)。那些常让人困惑的命令——rebaseresetcherry-pickreflog——一旦你能够想象它们对图的影响,就会立刻变得清晰。

实践练习

打开一个新文件夹(或一个测试项目),并按照以下步骤操作,仔细阅读每个输出

# Initialise a repository
git init

# Create a file
echo "hello" > file.txt

# Show untracked file
git status                    # → untracked file

# Stage the file
git add file.txt

# Show staged file
git status                    # → staged file

# Commit it
git commit -m "first commit"

# View the commit
git log --oneline             # → see the commit hash and message

创建并检查分支

git branch feature            # create branch
cat .git/refs/heads/feature    # → just a hash
cat .git/HEAD                  # → shows current ref (e.g., ref: refs/heads/master)

git switch feature            # switch to the new branch
cat .git/HEAD                  # → now points to refs/heads/feature

做更改并探索差异

echo "world" >> file.txt
git diff                      # working directory vs. index (unstaged changes)

git add file.txt
git diff                      # nothing – changes are staged

git diff --staged             # index vs. last commit

在特性分支上提交

git commit -m "add world"
git log --oneline --all --graph  # visualize the branch split

有意识地这样做——阅读每个命令的输出并思考发生了什么变化以及为什么——能够比任何被动阅读更快地建立扎实的思维模型。

下一篇:第 2 部分 – 有意提交:优秀提交的艺术

如果你觉得这有帮助,我已将整套系列整理成 23 页 PDF 参考手册,其中包括:

  • 检查清单
  • Hook 模板
  • 超过 80 条实用命令
  • reflogbisect 的深入解析
  • 针对 12 种真实场景的恢复手册

Git Mastery Field Guide → (link placeholder)

0 浏览
Back to Blog

相关文章

阅读更多 »

无惧分支

Git Mastery 系列第 3 部 ← 第 2 部:Committing with Intention https://dev.to/itxshakil/committing-with-intention-the-art-of-a-good-commit-p90 | 第 4 部:C…

Magit 中的变基

markdown Rebasing in Magit !Rebasing in Magit https://entropicthoughts.com/image/banner/rebasing-in-magit.jpg 我阅读了Ian Whitlock的文章《Why He Can’t Quit...》。