本节摘要:log 是在提交图上做遍历,diff 是在任意两个快照间做比对。本节把两组命令的常用形态整理成考古工具箱,并讲清
^、~、范围语法与../...的差别——这些符号是侦探读案卷的页码系统。
^ 与 ~ 及双点三点范围语法-S/-G/blame 完成一次代码溯源第 1 章说过历史就是 parent 指针连成的图,log 本质是图遍历加过滤器。日常最值钱的几个形态:
git log --oneline --graph --all # 全分支拓扑一屏尽收 git log -3 # 只要最近三条 git log --stat # 附每提交文件级增删行数 git log -p f.txt # 只看某文件的演变史 含每步补丁 git log --author=detective # 按作者过滤 git log --grep="登录" # 按说明关键字过滤 git log --since="2 weeks ago" # 按时间过滤
其中 -p 文件路径 是考古利器:不改文件名的前提下,一份文件的所有前世今生按时间倒序排列,每一步是谁改的、为什么改(看提交说明)一目了然。配合 --follow 还能穿透重命名继续追踪——再次呼应上节"rename 是推断"的机制。
读 log 常见 HEAD~2、v1.0^2 这类表达式,规则其实极简:
~ 沿第一个 parent 往前走:HEAD~2 是"当前提交的爸爸的爸爸"。^ 指 parent 序号:HEAD^ 即 HEAD~1;merge commit 有双 parent,HEAD^2 取第二个(被合入那一方)。HEAD~2^2 先走两步再取第二父。范围语法是另一套页码:A..B 表示"在 B 而不在 A 的提交"(B 独有);A...B 表示"双方各自独有的提交"(对称差),配合 merge-base 理解最稳:A...B = 从公共祖先分别到 A、到 B 的两段。用在 diff 上含义又不同(见下),同一符号在 log 与 diff 里的差别是面试经典坑:
git log main..dev # dev 有 main 没有的提交 推送前的自查清单 git log dev..main # 反过来 main 独有的 git diff main...dev # 从公共祖先到 dev 的总变化 才是你功能分支的净工作量
main..dev 是推送前最重要的自查命令:它列出的正是将要被 push 出去的提交,逐条过目再推,比任何事后补救都便宜。
diff 的比对对象有四个层级,分别回答不同问题:
git diff # 工作区 vs index 还没入袋的改动 git diff --staged # index vs HEAD 已入袋待归档的改动 git diff HEAD # 工作区 vs HEAD 全部未提交改动 git diff a1b2c3d e5f6a7b # 任意两个提交的快照比对 git diff main dev -- src/ # 限目录路径比对
值得强调 --staged(旧写法 --cached):只显示已入袋部分,是 commit 前最后确认"袋里到底装了什么"的正规动作。字级差异用 --word-diff,行内小改(改个变量名)时比行级 diff 可读得多。
diff 的底层机理呼应第 1 章:两棵 tree 逐层比对哈希,同哈希即跳过整棵子树,所以比对大项目也飞快——差异是"算出来的"不是"存着的",对象库里任何提交都不存 diff,只存快照。
找"这行代码什么时候出现的":
git log -S "OAuthToken" --oneline # 检索内容增删含该串的提交 pickaxe git log -G "token.*expire" --oneline# 正则版本 git blame f.txt -L 20,30 # 第20到30行逐行标注最后一次修改的提交与作者
-S 的内部机制很"侦探":它逐提交比对相邻快照中该字符串的出现次数,次数变化即命中。重构排查"某配置项是谁引入又何时被改坏的",-S 加 blame 组合几乎无往不利——blame 给你"最后一手",log -p 给你"全部前史"。
统计与归档:git diff --stat main...dev 给出功能分支净变更的文件行数摘要,附在合并请求描述里是低成本高收益的团队习惯;git shortlog -sn 按作者统计提交数,写周报时省事。
| 问题 | 命令 | 比对/遍历对象 |
|---|---|---|
| 这文件怎么演变过来的 | git log -p 文件 |
提交链 |
| 袋里装了什么 | git diff --staged |
index 对 HEAD |
| 分支净工作量 | git diff main...dev |
公共祖先对 dev |
| 即将推送什么 | git log main..dev |
dev 独有提交 |
| 这行谁最后改的 | git blame -L 起点,终点 文件 |
单文件逐行 |
| 某字符串何时引入 | git log -S 字符串 |
相邻快照计数 |
⚠️ 常见坑:把
git diff main...dev与git diff main..dev当同义词。后者(两 commit 间)等价于直接git diff main dev,比的是两个端点快照;前者比的是"公共祖先到 dev",才是功能分支的实际增量。差一个点,含义天差地别。
💡 关键直觉:diff 的三个点两个点记不住时,回到对象模型——
A..B用于 log(集合差:B 有 A 没有),A...B用于 diff(以 merge-base 为基线的净变更)。需要"哪段范围"就想想你到底要"哪两个快照"比对。
--follow 穿透重命名。~ 走第一父链、^n 取第 n 父,merge 提交的分叉口靠 ^2 进入。git log main..dev 逐条过目,是最便宜的质量闸门。-S/-G 找引入点,log -p 展开前史。看懂历史之后,下一节处理每个人都遇到过的问题——做错了怎么撤。