git log -S,找一段代码的生死记录
原文管这个叫 "git pickaxe"(镐子),命令是 git log -S"字符串"。作用:列出所有让这个字符串在代码库里"出现次数发生变化"的提交。说白了就是找一段代码的生与死——它是什么时候加进来的,又是什么时候被删掉的。
我正好有个现成的案例。去年我们网关里有个限流逻辑,方法叫 checkQuota,后来一次大重构之后整个没了。当时想查它是被哪个提交动掉的,用的是 git blame,但重构把代码挪来挪去,blame 追到的行早就不是原来那行了,查了个寂寞。 这次直接:
git <span>log</span> -S<span>"checkQuota"</span> <span>--oneline</span>
输出大概长这样:
<span>b7e2d41 perf:</span> <span>令牌桶替代计数器,网关层限流重写</span>
<span>8a3f19c fix:</span> <span>checkQuota</span> <span>并发场景下的计数越界</span>
<span>c01d9e2 feat:</span> <span>网关层接入限流</span>
三个提交,把这段代码的一生讲完了:c01d9e2 把它加进来,8a3f19c 修过一次 bug,b7e2d41 把它删了。注意中间那个 fix 提交——-S 能看到它,是因为那次修复改变了 checkQuota 这个字符串的出现次数(函数体里多了一次调用)。
这和 git blame 的区别值得单独说一句。blame 回答的是"这行代码现在是谁写的",一旦代码被移动或重构,blame 的锚点就断了;-S 回答的是"这个字符串的生与死",它不关心代码在哪一行,只关心出现次数。所以越老的、被反复重构的代码库,-S 越好用。 原文还提了一句 git log -G"正则" 和它类似,但行被移动时也会显示。这个区别我正好构造了个场景试了:把调用 checkQuota 的那几行从方法头部挪到尾部,不增不减。
<span>$ </span><span>git <span>log</span> -S<span>"checkQuota"</span> --oneline <span># 无输出:出现次数没变,镐子不看它</span></span>
<span>$ </span><span>git <span>log</span> -G<span>"checkQuota"</span> --oneline</span>
e4c1b99 refactor: 调整校验调用位置
结论很清楚:-S 数出现次数,纯移动它不管;-G 看 diff 里有没有匹配正则的增删行,挪动位置也算。想要"生死记录"用 -S,想要"所有碰过它的提交"用 -G。
原文一个有歧义的说法:git checkout -
原文里有句 git checkout -,说它类似 cd -,能"check out your previous HEAD"。我第一反应是这玩意我经常用啊,不就是回到上一个分支吗。但再一琢磨,"previous HEAD"这个说法容易被理解成"回到上一个提交",然后有人在自己分支上敲一下试试——
$ git branch
* feat-search
<span>main</span>
$ git checkout -
Switched <span>to</span> branch '<span>main</span>'
看,在 feat-search 上敲 git checkout -,它把你切回 main 了。"上一个 HEAD"指的是上一个所在的分支位置(等价于 git checkout @{-1}),不是上一个提交。如果你刚从 main 切到 feat-search 想安安静静干活,这一敲直接给你送回 main,工作区的东西还在,但分支换了,接下来的提交就跑到 main 上去了。 它真正好使的场景是来回横跳:改 feature 分支,切 main 看个东西,git checkout - 一步回来,再敲一下又过去,和 cd - 一个手感。至于"回到上一个提交",那是 git reset --hard HEAD~1 的事,别混。原文这句不算错,但确实容易让人想歪。
历史搜索:fzf 好使,atuin 惊艳
Ctrl+R 反向搜索历史,这个估计大多数人都知道。原文推荐的两个进阶,我都装了。 fzf 装好 shell 集成之后,Ctrl+R 变成模糊搜索,敲几个片段就能把那条命令捞出来,体验比原生好一截。这个属于"装了就回不去"系列,没什么好说的。 真正让我意外的是 atuin。它把 shell 历史整个搬进一个 SQLite 数据库:
curl --proto <span>'=https'</span> --tlsv1<span>.2</span> -LsSf https:<span>//setup.atuin.sh | sh</span>
atuin <span>import</span> <span>auto</span> # 把现有 shell 历史导进去
然后在 .zshrc 里加一行 eval "$(atuin init zsh)",重开终端,Ctrl+R 就变成它的全屏搜索界面了。按目录过滤、按退出码过滤、按时间范围过滤,都能做。命令行里也能直接查:
<span>$ </span><span>atuin search <span>"docker compose"</span> --<span>limit</span> 3</span>
我库里攒了小半年的历史,import 完两万多条,搜两个月前那条带一长串挂载参数的 docker 命令,两秒的事。以前这种时候我的操作是 history | grep,翻半天。 要说顾虑,就是它默认还想让你注册账号做跨机同步——同步是可选的,不注册纯本地用也没问题,但我能理解有人对"历史命令上云"过敏,我自己是只开本地。
其他几个,顺手验了下
剩下的就不挨个展开了,挑几个说印象:
SELECT 不带 FROM 这条,原文举的例子是 SELECT TRUE <> NULL,让我先猜返回什么。我猜 true,拿 sqlite3 一跑,返回的是空(NULL)——NULL 和任何值比较结果都是 UNKNOWN,SQL 三值逻辑这个坑,光看书真不如自己跑一遍记得牢。
EXPLAIN ANALYZE 是常识级的,PostgreSQL 和 MySQL 8.0.18+ 都支持,真执行一遍查询再给性能数据,比纯 EXPLAIN 靠谱,做慢查询优化的应该都在用,不细说。
globstar 那条我之前真没用过:bash 里 ** 递归匹配默认是关的,shopt -s globstar 打开后 ls **/*.md 能替代大部分 find,zsh 原生就支持。就顺势把 rg 的事也说了——rg 替代 grep 没什么可争的,没换的今天换。 JS 那几条里 Promise.withResolvers() 我之前真不知道,Node 22 起可用:
<span>const</span> { promise, resolve, reject } = Promise.<span>withResolvers</span>();
以前要自己在外面声明两个变量才能在 Promise 外面控制 resolve,现在一行。写任务队列的时候挺干净。
写在后面
一上午跑下来,我的结论和原文重合度挺高:这堆技巧单拎出来哪个都撑不起一篇文章,但它们是纯赚的——学的时候两分钟,用的时候每次都省一点,攒起来就是复利。
我自己的做法是在笔记里开了一个"终端小技巧"的页面,每次从别人文章或者 HN 评论里捡到一条就记一条。这次实测完又添了三条进去。至于原文作者那种每天往群里发一条的做法,我们组倒是可以试试,就是得做好被同事嫌烦的心理准备。 工具全集齐了,活还是得自己干,这个道理小技巧救不了。
这些终端技巧能显著提升日常开发与代码考古效率,尤其适合经常与Git、命令行打交道的开发者。花几分钟掌握,长期受益,建议收藏实践。