前面几篇讲的都是"把代码从一个地方搬到另一个地方",这一篇换一批更偏实战的命令,都是平时解决问题时会用到的。
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 各开各的。适合维护版本分支、同时看两个分支的代码、跑对比测试这类场景。