6篇文章讲清楚Git:Git 实用技巧:暂存现场、挑选提交与定位 Bug (5/6)

前面几篇讲的都是"把代码从一个地方搬到另一个地方",这一篇换一批更偏实战的命令,都是平时解决问题时会用到的。

stash:临时存一下现场

场景很常见:正在改一个功能,改到一半,突然要切分支去修一个紧急 bug。这时候直接切会报错(有未提交的改动),直接提交又不合适(半成品)。

git stash 就是用来把当前的工作区和暂存区打包存起来,让工作区回到干净状态:

shell 复制代码
git stash            # 存起来(默认不含未跟踪的新文件)
git stash -u         # -u 连同未跟踪的新文件一起存
git stash -a         # -a 连 .gitignore 忽略的文件也存

我们实际看一下效果:

shell 复制代码
git status -s
text 复制代码
 M f.txt
?? wip.txt
shell 复制代码
git stash -u
git status -s
text 复制代码
(干净,什么都没了)

工作区干干净净,可以随便切分支了。等处理完急事回来:

shell 复制代码
git stash pop        # 恢复并从列表里删掉这条记录
git stash apply      # 只恢复,记录还留着
shell 复制代码
git status -s
text 复制代码
 M f.txt
?? wip.txt

改动原样回来了,包括那个还没被跟踪的 wip.txt。

常用的几个操作

shell 复制代码
git stash list                 # 看存了哪些
git stash pop                  # 恢复最近一条并删除记录
git stash pop stash@{1}        # 恢复指定的那一条
git stash apply                # 只恢复不删除
git stash drop stash@{0}       # 删掉某一条
git stash clear                # 全删
git stash show -p stash@{0}    # 看某一条里具体存了什么
git stash branch newbranch     # 以某条 stash 为基础建分支
shell 复制代码
git stash list
text 复制代码
stash@{0}: WIP on main: 6d0bf61 初始提交

stash@{0} 是最新的一条。这里需要注意的是它不是按分支隔离的 ,所有 stash 都在一个全局列表里,切了分支也能 pop 出来,这会带来麻烦,所以恢复前最好先 git stash list 确认一下存的是哪一条。

stash 的本质

stash 其实不是"藏起来"这么玄乎,它是一个真实的提交对象 (一个有两个父提交的特殊提交,一个父是当前 HEAD,另一个父是暂存区的内容),存在 .git/refs/stash 里。

理解这一点有实际好处:它进过 reflog,所以误 drop 掉的 stash 也能找回来 ,翻 git reflog 或者用 git fsck --unreachable 找到那个提交就行;另外这些改动是真在磁盘上的,不是"内存里存着",重启电脑不会丢。

cherry-pick:只要那一个提交

场景:我们在 feature 分支上顺手修了一个 bug,这个修复其实现在就该上 main,但 feature 其它改动还没做完,不能整个合过去。

cherry-pick 可以把指定的一个(或几个)提交复制到当前分支:

shell 复制代码
git switch main
git cherry-pick c12d1c9

跑完看历史:

shell 复制代码
git log --oneline
text 复制代码
b76fa44 B: 修复 bug          ← 新生成的
e06f675 C: main 上的其他工作
3a6e07c A: 基础提交

修复已经到 main 上了。但需要注意原提交的哈希是 c12d1c9,新的变成了 b76fa44,和 rebase 一样是"重新应用",不是移动。

shell 复制代码
git cherry-pick c12d1c9                 # 单个
git cherry-pick c12d1c9 d34e5f6         # 多个,按顺序应用
git cherry-pick c12d1c9..d34e5f6        # 一个区间(左开右闭)
git cherry-pick -n c12d1c9              # 只应用改动不自动提交,方便再改改
git cherry-pick c12d1c9^..d34e5f6       # 区间包含起点

冲突的时候和处理 merge 一样:解决 → git add → git cherry-pick --continue;不想要了 git cherry-pick --abort。

频繁用 cherry-pick 其实是个信号,说明分支划分得不太好,或者有些东西本该更早合进主干。偶尔救急没问题,当成常规流程就会留下一堆内容相同但哈希不同的提交,以后合并容易冲突。

定位问题:log、blame、bisect

用 log 精确过滤

git log 的过滤能力比想象中强,找问题的时候很有用:

shell 复制代码
git log --oneline -- a.txt           # 只看改过 a.txt 的提交
git log -S "具体某段代码"             # 找哪次提交新增或删除了这段内容
git log -G "正则"                    # 类似 -S,但用正则匹配内容变化
git log --author="张三"              # 按作者
git log --since="2 weeks ago"        # 按时间
git log --grep="订单"                # 按提交说明
git log -L 10,20:a.txt               # 看 a.txt 第 10~20 行的完整演变历史
git log --merges                     # 只看合并提交

-S 是最常用的一个,俗称 "pickaxe"。想知道某行代码是哪次提交加进来的、当时为什么加 ,直接搜内容就行,比一层层翻快得多。git log -L 更狠,直接追踪指定行的演变,能看到这一行从创建到现在被改过几次、每次改成什么样。

blame:看某一行是谁写的

shell 复制代码
git blame a.txt
text 复制代码
a1b2c3d4 (张三 2026-09-01 10:00:00 +0800 1) 第一行
d4e5f6a7 (李四 2026-09-10 15:30:00 +0800 2) 第二行

每一行前面是"最后一次改这行的提交 + 作者 + 时间"。加参数可以看更细:

shell 复制代码
git blame -L 10,20 a.txt       # 只看 10~20 行
git blame -w a.txt             # 忽略空白改动
git blame -C a.txt             # 检测跨文件复制的代码(重命名、搬家场景有用)
git blame a.txt -L 10,20 -- file2.txt   # 追踪这行的来源

需要注意的是 blame 只告诉我们"最后改这行的是谁",而真正的 bug 可能是更早引入的,只是被一次格式化改动刷新了 blame 信息。所以 blame 经常要配合 -w(忽略空格改动)来用,或者用 git log -S 交叉验证一下。

bisect:二分查找是哪次提交引入的 bug

这是排查历史遗留问题最强的工具,思路就是二分查找:在一段提交区间里不停对半切,每次判断"这个提交有没有 bug",几轮就能定位到出问题的那一个。

手动的用法:

shell 复制代码
git bisect start
git bisect bad                  # 当前版本是有问题的
git bisect good a1b2c3d         # 这个老版本是好的

Git 会自动切到中间那个提交:

text 复制代码
HEAD detached at db00064
You are currently bisecting, started from branch 'main'.

这时候跑我们的测试或者手动验证这个版本有没有问题,然后告诉 Git:

shell 复制代码
git bisect good         # 这个提交是好的,bug 在后面
# 或者
git bisect bad          # 这个提交是坏的,bug 在前面

Git 再切到新的中间点,重复这个循环就行。100 个提交大概 7 次就能定位到。找到之后:

shell 复制代码
git bisect reset        # 结束,回到原来的分支

也可以全自动,前提是有一个能判断好坏的脚本(返回 0 表示好):

shell 复制代码
git bisect start HEAD a1b2c3d
git bisect run ./test.sh

Git 会把整个二分过程跑完,直接告诉我们哪个提交有问题。

⚠️ bisect 过程处于游离 HEAD 状态(就是第 2 篇里讲的游离 HEAD),结束时一定要 git bisect reset,否则会一直留在那个临时提交上。

交互式 rebase:整理提交

本地开发过程中难免留下一堆 fix typo、再改一下、真的最后一次 这样的提交,推之前把它们整理干净会清爽很多。

shell 复制代码
git rebase -i HEAD~4        # 整理最近 4 个提交

会打开编辑器,把这些提交列出来(注意顺序是从老到新,和 git log 是反的):

text 复制代码
pick a1b2c3d 添加用户表
pick b2c3d4e fix typo
pick c3d4e5f 补充注释
pick d4e5f6a 修复用户名校验

把每行开头的 pick 改成别的动作:

命令 效果
pick 保留这个提交
reword 保留改动,改提交信息
edit 停下来,让我们修改这个提交的内容
squash 合并进上一个提交,并把两条提交信息合起来让我们编辑
fixup 合并进上一个提交,丢弃这条的提交信息
drop 删掉这个提交
exec 执行一条命令

常用的是 fixup 和 squash:

text 复制代码
pick a1b2c3d 添加用户表
fixup b2c3d4e fix typo
fixup c3d4e5f 补充注释
reword d4e5f6a 修复用户名校验

保存退出之后,四个提交就变成了两个,中间那些琐碎的提交信息被吃掉了。

调整顺序就是直接改行的顺序 ,删除提交就把那一行删掉或者改成 drop。

交互式 rebase 是改写历史 的操作,效果和 rebase 一样会重新生成所有涉及的提交(哈希全变)。所以规则同样适用:只在没推送给别人的分支上做 。整理完推送时可能需要 --force-with-lease,这个在第 4 篇的强推边界里说过。

worktree:一个仓库多个工作目录

场景:正在 feature 分支写代码,突然要切到 hotfix 修个急事。普通做法是 stash → 切分支 → 修 → 切回来 → stash pop,来回折腾好几步。

worktree 可以给同一个仓库挂多个工作目录,每个目录各自在一个分支上,同时开工互不干扰:

shell 复制代码
git worktree add ../hotfix-dir hotfix          # 在另一个目录检出 hotfix 分支
git worktree add -b new-feature ../feat-dir    # 新建分支并检出
git worktree list                              # 看有哪些工作目录
git worktree remove ../hotfix-dir              # 删掉

../hotfix-dir 是一个完整可用的工作目录,有它自己的工作区和暂存区,但它们共享同一个 .git 仓库,所以不占额外空间,提交历史、远程配置都是同一份。

这比 stash 舒服的地方在于不用来回搬状态,两个目录可以同时开着,IDE 各开各的。适合维护版本分支、同时看两个分支的代码、跑对比测试这类场景。

相关推荐
MacroZheng1 小时前
装上这款全能增强插件,DeepSeek Harness瞬间高大上了!
java·人工智能·后端
小卿噢1 小时前
那个时间不存在——时区代码里六个会真的出事的坑
后端
小园子的小菜1 小时前
深度图解 Python 进程、线程与 GIL:彻底吃透并发核心原理
后端
用户7713970207061 小时前
Trae 深度使用指南:从配置到实战,一个 AI 原生 IDE 的完整工作流
后端
创新技术阁1 小时前
FastapiAdmin代码生成详细解释
前端·后端·fastapi
杨利杰YJlio1 小时前
ITSK 万能驱动 26V5:新驱动批量更新
前端·javascript·后端
imDwAaY1 小时前
6篇文章讲清楚Git:Git 团队开发实践:从 Pull Request 到版本发布 (6/6)
git·后端
DevUp1 小时前
老站往哪搬:织梦、帝国、PHPCMS 的出路
后端·php·cms
王中阳Go1 小时前
甲骨文裁3万、DeepSeek却招150人:后端程序员往哪走,我把这批JD拆了一遍
后端