那些你以为是玄学的现象
这一章把十条常见的「Git 怎么又抽风了」摆出来,逐条拆开。你会发现没有一条需要新知识——它们全都是前面二十二章那几件事的直接推论。这正是读完这本书真正的收获。
先把那几条原理收拢
整本书讲的东西,压成六句话:
- 提交是快照,不是差量——每个提交指向一整棵完整的树(第 1 章)
- 对象的名字是内容的哈希——所以只增不改、天然去重、改一处则一路改名(第 2、4 章)
- 分支只是一张 41 字节的贴纸——提交里没有任何字段记录「我属于哪个分支」(第 6 章)
- 索引是一张扁平的文件路径表——没有目录这个概念(第 11 章)
- 历史是一张 DAG,箭头只指向过去——遍历必须从贴纸出发(第 15 章)
- 「删除」= 失去了通往它的路径——对象还在,只是没名字了(第 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/add、rename/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 又抽风了」,按这个顺序问自己:
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、删除只是失去路径」这六句话的直接推论。而读完这本书真正的收获,不是记住十条答案,是下次碰到第十一条的时候,你能自己推出来。
最后一章:把全书压成一张表——出事了先看它。而那张表其实只有一列是重要的。