卷 VI · 落地CH 23深度 23/24

那些你以为是玄学的现象

这一章把十条常见的「Git 怎么又抽风了」摆出来,逐条拆开。你会发现没有一条需要新知识——它们全都是前面二十二章那几件事的直接推论。这正是读完这本书真正的收获。

现象对照排障推理而非记忆

先把那几条原理收拢

整本书讲的东西,压成六句话:

◆ 你现在手里有的全部工具
  1. 提交是快照,不是差量——每个提交指向一整棵完整的树(第 1 章)
  2. 对象的名字是内容的哈希——所以只增不改、天然去重、改一处则一路改名(第 2、4 章)
  3. 分支只是一张 41 字节的贴纸——提交里没有任何字段记录「我属于哪个分支」(第 6 章)
  4. 索引是一张扁平的文件路径表——没有目录这个概念(第 11 章)
  5. 历史是一张 DAG,箭头只指向过去——遍历必须从贴纸出发(第 15 章)
  6. 「删除」= 失去了通往它的路径——对象还在,只是没名字了(第 5、9 章)

下面十条,每一条都能用这六句里的某几句推出来。试着先自己想一想,再看答案。

▶ 动手 · 现象对照

点开每一条看解释和处理办法。右边标着它对应书里的哪一章。

挑三条展开说

demo 里那十条已经够用了。这里挑三条最容易反复踩的,多说两句。

一、「我明明改了 .gitignore,为什么没用」

用原理 4 推:索引是一张已跟踪文件的表。一旦某个路径进了这张表,Git 每次都会检查它——.gitignore 是「别把新东西加进来」的规则,对已经在表里的东西根本不适用

$ git rm --cached debug.log      # 从表里摘掉,文件留在磁盘上

还有一个相关的坑值得单独记:整个目录被忽略时,没法用 ! 反忽略里面的东西。

# 不管用:
build/
!build/keep.txt

# 管用:
build/*
!build/keep.txt

因为 Git 发现 build/ 被忽略就整个跳过、根本不走进去(这是个性能优化),里面的规则自然没机会生效。

调试 ignore 规则有个专门的命令,能告诉你是哪一行规则命中的

$ git check-ignore -v 那个文件
.gitignore:12:build/	build/keep.txt

二、「为什么两个人改了不同文件也会冲突」

用原理 1 推:提交指向的是整棵树,而目录本身也是对象(第 3 章)。

如果你俩都动了同一个目录的结构——你把 utils/a.js 改名成 helper.js,同事在 utils/ 里加了个新文件——那么 utils 这棵 TREE 对象两边都改了

冲突发生在树这一层,报出来的是 rename/addrename/rename 这类你可能没见过的类型(第 17 章)。这类冲突没有冲突标记可以编辑,因为问题不在文件内部,而在「这个文件该不该存在、该叫什么」。

重命名类的尤其常见,根源在第 3 章那个事实:Git 不记录重命名,它是现场推断的。两边各自推断出的结果可能都不一样。

三、「rebase 之后 push 被拒绝」

用原理 2 推:rebase 重建了提交,哈希全变了(第 18 章)。你本地那串和服务器上那串谁也不是谁的祖先,不是快进关系(第 16 章)。

服务器拒绝,是在保护它上面那些你这边已经没有的提交——它们可能是别人推的(第 8 章)。

处理方式取决于一个问题,而这个问题是社交的,不是技术的

  • 这个分支只有你在用git push --force-with-lease,收工。
  • 别人也在用 → 别推。用 merge 或者 revert(第 19 章),别改写共享的历史。
✎ 几条「一次设好,长期受益」的配置

散落在各章的建议,集中在这里:

# 冲突时显示 base,判断依据一下就有了(第 17 章)
$ git config --global merge.conflictStyle zdiff3

# 记住冲突的解决方式,反复 rebase 时只用解一次(第 17 章)
$ git config --global rerere.enabled true

# 更好的 diff 算法,大块代码移动时结果好看得多(第 14 章)
$ git config --global diff.algorithm histogram

# 区分「移动的代码」和「新增的代码」(第 14 章)
$ git config --global diff.colorMoved zebra

# pull 时用 rebase,不产生无意义的合并提交(第 16 章)
$ git config --global pull.rebase true

# fetch 时自动清理已删除的远程分支便条(第 8 章)
$ git config --global fetch.prune true

# 自动把 fixup! 提交排好序(第 18 章)
$ git config --global rebase.autosquash true

# 强推的安全版本,设成别名免得手快打错(第 8、20 章)
$ git config --global alias.pushf 'push --force-with-lease'

这八行大概能消灭掉你未来一半的 Git 烦躁。成本是复制粘贴一次。

⌗ 掀开 .git 看一眼:一份自查清单

下次觉得「Git 又抽风了」,按这个顺序问自己:

1. 我现在在哪?

$ git status
$ cat .git/HEAD              # 是不是 detached?(第 7 章)

2. 我说的那个东西,Git 眼里是什么?

$ git rev-parse HEAD main origin/main    # 三张贴纸各指向哪儿(第 8 章)
$ git cat-file -p <哈希>                  # 那个对象到底是什么

3. 它们在图上是什么关系?

$ git log --graph --oneline --all -20
$ git merge-base --all main HEAD          # 有没有多个 base(第 16 章)
$ git rev-list --left-right --count main...HEAD

4. 我要找的东西还在吗?

$ git reflog                              # 90% 的情况这里就有答案(第 9 章)
$ git fsck --lost-found                    # 兜底

这四步能解决绝大多数「不知道发生了什么」的场面。共同点是:它们都在问「客观状态是什么」,而不是「我该敲哪条命令」。

↩ 回到你的仓库

「有没有什么习惯,能从源头减少这些破事?」

有四条,按收益排:

一、提交小而完整。git add -p 把改动切开(第 10 章)。收益不只是 review 好看——bisect(第 15 章)、revert(第 19 章)、blame(第 14 章)全都依赖这个。一个 500 行的巨型提交会让这三个工具同时失效。

二、频繁同步主干。冲突的规模正比于「分开的时间」(第 17 章)。每天 rebase 一次,是把一次大痛苦拆成很多次小痛苦——而小痛苦的总和远小于那次大的。

三、危险操作前先贴张贴纸。41 个字节(第 6 章),换一个确定的退路:

$ git branch backup-$(date +%m%d-%H%M)
$ git stash --include-untracked

四、大文件从一开始就别提交。这个操作在 Git 的模型里近乎不可逆(第 1、21 章)——事后清理要改写历史,让所有人重新 clone。五秒钟的犹豫,能省下未来的一场会议。

这一章的一句话

这十条现象没有一条需要新知识,它们全都是「提交是快照、名字由内容决定、分支是贴纸、索引是扁平表、历史是 DAG、删除只是失去路径」这六句话的直接推论。而读完这本书真正的收获,不是记住十条答案,是下次碰到第十一条的时候,你能自己推出来。

最后一章:把全书压成一张表——出事了先看它。而那张表其实只有一列是重要的。