Chapter 08

高级操作

stash、cherry-pick、reset、reflog、bisect——掌握这些命令,成为真正的 Git 高手

git stash — 临时保存工作

场景:你正在开发一个功能,写到一半时突然需要切换到另一个分支修复 bug,但当前工作还没完成,不想提交一个不完整的 commit。git stash 就是解决这个问题的工具。

stash 会将工作区和暂存区的变更"打包"保存,让工作区恢复到上次提交时的干净状态。

# 保存当前工作(包含工作区和暂存区)
git stash

# 附带描述信息(推荐!方便识别)
git stash push -m "WIP: user profile editing feature"

# 同时保存未追踪的新文件(-u / --include-untracked)
git stash push -u -m "WIP: include new files"

# 查看所有 stash 列表
git stash list
# 输出:stash@{0}: WIP: user profile editing feature
#       stash@{1}: On main: WIP: another thing

# 恢复最近一次 stash,并从列表中删除
git stash pop

# 恢复但不从列表删除(可多次 apply 到不同分支)
git stash apply stash@{0}

# 删除特定 stash
git stash drop stash@{0}

# 删除所有 stash(谨慎!)
git stash clear

# 查看某个 stash 的内容
git stash show -p stash@{0}

# 从 stash 创建新分支(适合 stash 内容已与当前分支冲突的情况)
git stash branch new-feature-branch stash@{0}

工作流示例:正在开发 feature,收到紧急 bug 报告 → git stash push -m "WIP: feature"git switch main → 修复 bug、提交、推送 → git switch featuregit stash pop → 继续开发。

git cherry-pick — 挑选特定提交

cherry-pick 允许你将某个分支上的特定一个或几个提交应用到当前分支,而不需要合并整个分支。

# 将指定 commit 应用到当前分支
git cherry-pick abc1234

# cherry-pick 多个 commit
git cherry-pick abc1234 def5678

# cherry-pick 一个范围(不含起始,含结束)
git cherry-pick abc1234..def5678

# cherry-pick 但不立即提交(先放入暂存区,让你可以修改)
git cherry-pick --no-commit abc1234

# 遇到冲突,解决后继续
git add .
git cherry-pick --continue

# 放弃 cherry-pick
git cherry-pick --abort

典型使用场景:hotfix 分支上修复了一个 bug(commit abc1234),想把这个修复也应用到正在开发的 feature 分支上,而不需要合并整个 hotfix 分支。或者开发分支上有一个独立的功能提交,想提前发布到 main 而不想发布其他未完成的提交。

git reset — 撤销提交(理解三种模式!)

git reset 移动 HEAD(和当前分支指针)到指定的 commit。三种模式的区别在于:如何处理被"撤回"的那些提交的内容。

当前:A ── B ── C ── D ◄── HEAD (main)
执行 git reset HEAD~2(回退到 B):
A ── B ◄── HEAD (main) C 和 D 的内容去哪了?取决于 mode
模式HEAD暂存区(Index)工作区适用场景
--soft移动到目标保留(C、D 的变更已暂存)保留合并多个提交(然后重新 commit)
--mixed(默认)移动到目标清空(变更退回工作区)保留撤销 add/commit,重新组织提交
--hard移动到目标清空丢弃(变更完全消失)彻底放弃某些提交(危险!)
# --soft:回退 HEAD,保留所有变更在暂存区(可重新提交)
git reset --soft HEAD~1
git reset --soft abc1234

# --mixed(默认):回退 HEAD,变更退回工作区(未暂存状态)
git reset HEAD~2        # 等同于 --mixed
git reset --mixed HEAD~2

# --hard:回退 HEAD,彻底丢弃所有变更(无法恢复,除非用 reflog)
git reset --hard HEAD~3
git reset --hard abc1234

# 常用:撤销最后一次 commit,保留变更在工作区
git reset HEAD~1  # --mixed 模式

# 常用:取消暂存某个文件(从暂存区退回工作区)
git reset HEAD src/app.js
# 或使用更新的语法:
git restore --staged src/app.js
!

--hard 之后变更真的消失了吗?不完全是。只要你知道被丢弃 commit 的哈希,就可以通过 git reflog 找回(只要没超过 Git 的垃圾回收周期,默认 90 天)。但如果是工作区的未提交变更被 --hard 丢弃,则真的无法恢复。

git revert — 安全撤销

git revert 不移动 HEAD,而是创建一个新的提交来"反向"应用指定提交的变更。原始提交依然保留在历史中。

# 创建一个新提交,撤销指定 commit 的变更
git revert abc1234

# 撤销但不立即提交(手动修改后再 commit)
git revert --no-commit abc1234

# 撤销最近一次 merge commit(需要指定 mainline)
git revert -m 1 merge-commit-hash
git revert abc1234:
A ── B ── C(abc1234) ── D ── E(revert of C) ◄── main
(历史完整保留,通过新 commit 抵消了 C 的效果)

revert vs reset 的选择原则:

git reflog — 后悔药

reflog(Reference Log)记录了本地仓库中所有 HEAD 的移动历史,包括:commit、reset、merge、rebase、cherry-pick 等所有操作。这是 Git 的"后悔药"。

# 查看 HEAD 的移动历史
git reflog

# 查看某个分支的 reflog
git reflog main

典型输出:

a3f2c1d HEAD@{0}: commit: feat: add profile page
8e4b9a2 HEAD@{1}: reset: moving to HEAD~2
c7d1e3f HEAD@{2}: commit: feat: add settings page
5a2b8c1 HEAD@{3}: commit: feat: add notifications
2f9d4e5 HEAD@{4}: checkout: moving from feature to main
# 找回被 reset --hard 丢弃的 commit
git reflog                    # 找到丢失 commit 的哈希
git reset --hard c7d1e3f    # 回到那个状态

# 或者在那个哈希上创建新分支
git branch recovered-work c7d1e3f

reflog 的保留期:默认保留 90 天(gc.reflogExpire 配置)。在此期间,即使是被 reset --hard 丢弃的提交,只要在 reflog 中有记录,就可以找回。定期运行 git gc 会清理过期记录。

git bisect — 二分法定位 Bug

当你知道 "某个功能在 commit A 时还好的,在最新的 HEAD 时已经坏了",但中间有几十上百个 commit,不知道是哪次引入了 bug,git bisect 用二分法帮你快速定位。

# 1. 开始二分查找
git bisect start

# 2. 标记当前版本为"坏的"(有 bug)
git bisect bad

# 3. 标记一个已知"好的"版本(比如上周的 tag)
git bisect good v1.0.0
# 或使用哈希
git bisect good abc1234

# Git 自动 checkout 到中间的某个 commit
# 4. 测试当前版本,然后标记
git bisect good   # 如果这个 commit 是好的
git bisect bad    # 如果这个 commit 是坏的

# 重复步骤 4,直到 Git 找到第一个"坏的"commit
# Git 会输出:"abc1234 is the first bad commit"

# 5. 查看那次提交做了什么
git show abc1234

# 6. 结束 bisect,回到原来的 HEAD
git bisect reset

如果有自动化测试,可以让 bisect 全自动运行:

# 自动 bisect(脚本返回 0 = good,非0 = bad)
git bisect start HEAD v1.0.0
git bisect run npm test

git blame — 查看代码作者

git blame 显示文件每一行最后一次被修改的提交哈希、作者和日期。常用于追查"这行代码是谁写的、为什么这样写"。

# 查看整个文件的 blame 信息
git blame src/auth.js

# 只看第 10 到 20 行
git blame -L 10,20 src/auth.js

# 忽略空白字符变更
git blame -w src/auth.js

# 显示每行被修改时的原始哈希(忽略文件重命名/复制)
git blame -C src/auth.js

典型输出:

a3f2c1d (张三 2024-03-10 14:23:11 +0800 42) function validateToken(token) {
8e4b9a2 (李四 2024-03-08 09:15:02 +0800 43)   if (!token) return false;
c7d1e3f (张三 2024-03-10 14:23:11 +0800 44)   return jwt.verify(token, SECRET_KEY);

git shortlog — 贡献者统计

# 按提交数量排序显示贡献者(-s=数字 -n=排序)
git shortlog -sn

# 包含邮箱
git shortlog -sne

# 统计特定时间范围
git shortlog -sn --since="2024-01-01"