2282 字
约 7 分钟
6
S01-J03 · Git 手术与效率:reset、revert、reflog、bisect、alias

适用读者:提交错了、推错了、合错了、找不到是哪次改动引入 bug 的人。
前置:S01-J01(对象与引用模型)、S01-J02(分支与 CR 流程)。
实测环境:Ubuntu 24.04.1 / Git 2.43.0;bisect、revert、cherry-pick、worktree、reflog 输出均为 2026-11-05 实测。
覆盖旧文:A-346(Git Alias 详解,48 点)。
新增扩展:E-S01-015(worktree / partial clone / sparse checkout,本篇讲 worktree)。
上一篇:S01-J02 · 分支策略与 Code Review / CI 流程
下一篇:S01-J04 · IDEA 调试与源码阅读。


1. 一句话结论

判断标准只有一条:这段历史有没有别人在用?

情况 用什么 为什么
只有我本地用(未推送) reset 改写 改写不影响任何人
已经推送、别人可能基于它工作 revert 追加反向提交 不改变历史,别人不用重新同步
想把某个提交搬到别的分支 cherry-pick 复制提交,不动原分支
想找出哪次改动引入 bug bisect 二分,O(log n)
想同时开两个工作目录 worktree 同一个仓库、两个目录、互不干扰
已经闯祸了 reflog + fsck 对象还在,只是没引用

reset 与 revert 的选择题,本质是"历史是私有的还是共享的"。


2. reset 三档:改的是哪个位置

git reset [--soft|--mixed|--hard] <commit>
                     │        │       │
             只动 HEAD   动 HEAD+暂存区  动 HEAD+暂存区+工作区
模式 HEAD 暂存区 工作区 典型场景
--soft ✅ 移 ❌ ❌ 撤提交但保留改动在暂存区(想重新组织提交)
--mixed(默认) ✅ ✅ 清 ❌ 撤提交与暂存,改动留在工作区(重新 add)
--hard ✅ ✅ ✅ 覆盖 彻底回退(未提交的改动会丢)
git reset --soft HEAD~1      # 把上一个提交退回暂存区
git reset HEAD~1             # 退回工作区(默认 mixed)
git reset --hard HEAD~1      # 连工作区一起回退

--hard 不是"删除历史":它只是把引用挪走,对象还在(S01-J01 §5)。所以下一条命令往往能救命。


3. revert:共享分支上唯一正确的"撤销"

$ git log --oneline | head -3
18c61e8 Revert "c10"          ← revert 产生的反向提交
8f8c49f c10
ca561d3 c9
git revert <commit>            # 生成一个"反向"提交
git revert --no-edit <commit>  # 不打开编辑器
git revert -m 1 <merge-commit> # 撤销合并(1 = 保留第一父,即主干)

为什么共享分支必须用 revert:它追加新提交,历史不变。别人 git pull 就能拿到;如果用 reset + push -f,所有人的本地历史会与远端分叉,每个人都要手动修——这是团队级事故。

revert 场景 注意
撤销普通提交 直接 revert
撤销 merge 提交 必须 -m 1;之后想"再合一次"要 revert 那个 revert,否则 Git 认为变更已存在
连续撤销多个 从新到旧依次 revert(或 git revert A..B)

4. reflog:本地"时光机"(实测)

$ git reset --hard HEAD~1
$ git reflog --oneline
a547b41 HEAD@{0}: reset: moving to HEAD~1
2f56673 HEAD@{1}: commit: c2        ← 丢掉的提交
a547b41 HEAD@{2}: commit (initial): c1

$ git reset --hard HEAD@{1}         # 原地复活
$ cat f.txt
v2

三条边界:

情况 能否找回
刚 reset / 刚删分支 / 刚 rebase 出错 ✅ reflog 里有
reflog 过期(默认 90 天) ⚠️ reflog 没了,git fsck --lost-found 可能还有悬空对象
已 git gc --prune=now ❌ 对象物理删除
从未 git add 的新文件被覆盖 ❌ Git 没存过(靠 IDE Local History,S01-B09)

5. bisect:把"哪次引入的 bug"变成二分查找(实测)

5.1 场景

10 个提交,第 5 个引入了 BUG,现在 HEAD 是坏的。

5.2 手工流程

git bisect start
git bisect bad HEAD
git bisect good $(git rev-list --max-parents=0 HEAD)   # 第一个提交
# 每次 Git 检出中间提交,你验证后告诉它 good/bad
git bisect good     # 或 git bisect bad
# …
git bisect reset    # 结束,回到原分支

5.3 自动化:git bisect run(实测)

cat > /tmp/check.sh <<'EOF'
#!/bin/sh
if grep -q BUG f.txt; then exit 1; else exit 0; fi
EOF
chmod +x /tmp/check.sh

git bisect start
git bisect bad HEAD
git bisect good $(git rev-list --max-parents=0 HEAD)
git bisect run /tmp/check.sh

实测输出:

Bisecting: 2 revisions left to test after this (roughly 2 steps)
...
    c5
f.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
bisect found first bad commit

它准确指出了 c5。 10 个提交手工排查要试 5 次以上,二分只要 ⌈log₂10⌉ = 4 步;100 个提交也只要 7 步。

5.4 前提:你需要一个"能自动判断好坏"的命令

bisect run 的价值全在这个脚本上。它可以是:

#!/bin/sh
mvn -q -pl order-service test -Dtest=OrderConcurrencyTest   # 测试通过=good

这也是"每个修复都要有回归测试"的真正理由:有测试,bisect 才能自动化(S16 会讲测试体系)。


6. cherry-pick:把某个提交搬到另一个分支(实测)

$ git cherry-pick --no-commit 8f8c49f
$ git status --short
M  f.txt
用法 说明
git cherry-pick <sha> 复制该提交到当前分支
git cherry-pick A..B 复制一段(不含 A)
git cherry-pick -x <sha> 在消息里记录来源 sha(跨分支追踪时强烈建议)
--no-commit 只应用改动,不自动提交(想再改一改时用)

风险:cherry-pick 会生成新的 hash(父提交不同,见 S01-J01 §2)。同一个改动在两处存在,后续合并容易冲突——能用 merge 就别用 cherry-pick。


7. worktree:一个仓库,两个工作目录(实测)

$ git worktree add /tmp/bd-wt HEAD~1
$ git worktree list
/tmp/bd     18c61e8 [main]
/tmp/bd-wt  8f8c49f (detached HEAD)

用途(这是旧文完全没提的高频场景):

场景 好处
正在改功能,线上突然要 hotfix 不用 stash 当前工作,直接开一个 worktree 修
要对比两个版本的行为 两个目录并行运行,互不干扰
跑长时间构建/测试 一个目录跑测试,另一个继续写代码

清理:git worktree remove <path>;git worktree prune 清失效记录。


8. alias:把高频命令变短(A-346)

git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.lg "log --oneline --graph --decorate -15"
git config --global alias.last "log -1 --stat"
git config --global alias.unstage "reset HEAD --"

shell 型别名(以 ! 开头,能跑任意命令):

git config --global alias.undo '!git reset --soft HEAD~1'
git config --global alias.cleanbr '!git branch --merged | grep -v "\*" | xargs -r git branch -d'
风险 说明
! 别名会执行 shell,可被用于注入 别从不可信来源复制 alias,尤其 ! 开头的
覆盖内建命令 alias.co=checkout 这类覆盖是安全的;但不要覆盖 git status 这类带 --porcelain 语义的命令(脚本会坏)
别名不进仓库 只在本机 ~/.gitconfig;团队共享要走 dotfiles(S01-B09 §5.4)

A-346 的补充:旧文讲了"什么是 alias、为什么需要、如何配置",但没提 ! 型别名的执行风险,也没提"别覆盖带脚本语义的内建命令"。这两条是实践里真正会踩的。


9. 五种事故处置手册

事故 立刻做 不要做
误提交到 main(未推) git reset --soft HEAD~1 或 git branch tmp && git reset --hard HEAD~1 别 push -f 到 main
误提交并已推送 git revert <sha>;若在个人分支可 reset + push -f 别在共享分支 push -f
误合并且已推送 git revert -m 1 <merge> 别 reset 共享分支
误删分支 git reflog 找到 sha → git branch <name> <sha> 别先 gc
找不到引入 bug 的提交 git bisect run + 可自动化判定的脚本 别手工逐个 checkout 试

10. 自测

  1. reset --soft、--mixed、--hard 分别动了哪几个位置?
  2. 已推送的提交要撤销,为什么必须 revert 而不是 reset?
  3. revert -m 1 <merge> 里的 1 是什么?撤销 merge 后再想合并回去要做什么?
  4. bisect run 的脚本要满足什么条件?为什么说这解释了"回归测试为什么必须写"?
  5. worktree 与 git stash 解决的场景有何不同?

11. 下一篇

S01-J04 · IDEA 调试与源码阅读:断点、求值、远程调试

Git 层面能定位"哪次改动",代码层面还要定位"哪一行、哪个线程、哪个变量"。下一篇从旧文的 4 个调试技巧出发,补上条件断点、日志断点、Stream Debugger 与容器内远程调试(JDWP),并给出"不改代码就能观察变量"的三条路径。


本篇属于系列《S01 · 开发环境与工具链》。旧文来源:A-346;扩展点 E-S01-015。所有输出为 2026-11-05 实测。

S01-J03 · Git 手术与效率:reset、revert、reflog、bisect、alias
https://www.clxhxhhr.top/posts/5168/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。