24 — 高级实验:把十五到二十三章练成真能力

0 阅读17分钟

写在前面:这一章要解决什么

摘要: 本文是一篇 Git 高级操作动手实验教程,围绕 reflogrebasecherry-pickrevertstashbisect 与冲突解决七组实验展开。读者在可丢弃目录中,按「场景 → 步骤 → 预期结果 → 通过标准 → 容易卡的地方」完整走一遍,把高级操作练成肌肉记忆。同时强调安全边界:已推送用 revert、未推送用 reset、共享分支不改历史。 高级操作看书看懂了,上手还是懵。这一章把第 15–23 章的知识点 拧成七组动手实验,在 可丢弃目录 里走完整一遍。做完你才敢说「高级操作我用过了」。

学完后,你应该能 不看资料

  1. reflog 找回被 reset --hard 吃掉的提交
  2. 用交互式 rebase 合并、重排提交
  3. cherry-pick 从别的分支挑选提交
  4. revert 安全地撤回已推送的提交
  5. stash 救急切分支修 bug 再切回来
  6. bisect 二分查找第一个坏提交
  7. 亲手解决一次冲突并理解标记含义

读者设定: 大一同学,读完 15–23 章,知道这些命令是什么,但还没真练过手。


1. 定位:为什么要单开一章高级实验

1.1 一句话先记住

高级操作没练过手,等于没学会。

这一章不强加新知识,只把分散的高级操作 串成可复现的练习,逼你把动作练成肌肉记忆。

1.2 只读不练会怎样

只读不练真做了实验
以为懂 rebase,一用就冲突rebase 冲突解决过一次就不怕
知道 reflog 能救命,但没真救过找回过一次丢掉的提交,心里有底
cherry-pick 听过没用过挑过一次提交,手法清楚了
revert 和 reset 分不清实际用过,知道一个加提交一个改指针

1.3 实验规则

规矩原因
用可丢弃目录不怕点错
虚构身份练手不泄露隐私
每步后 git statusgit log知道自己在哪
卡住时回查对应章节别瞎试
不在生产仓库练这些命令威力大,别碰真作业
每个实验做完自检通过标准不是敲完就行,要确认结果对

2. 实验前准备

找一个可以随便删的目录,比如 ~/lab-advanced/

mkdir -p ~/lab-advanced
cd ~/lab-advanced
git --version
git version 2.43.0

本系列样例验证环境。你的版本接近即可。

设个虚构身份,省得真信息混进练习历史:

git config --global user.name "Ada Example"
git config --global user.email "ada@example.com"

你也可以只在每个实验仓库里设 local 配置,避免改全局。

准备就绪,下面开始七组实验。


3. 实验组 A:reflog 恢复实验

目标: 故意用 reset --hard 丢掉提交,再用 reflog 找回来。

场景

你手贱敲了 git reset --hard HEAD~2,两个提交「消失」了。别慌,reflog 记着呢。

步骤

  1. 建仓并做三次提交:
cd ~/lab-advanced
mkdir lab-a && cd lab-a
git init -b main
printf '第一行\n' > file.txt
git add file.txt
git commit -m "feat: 第一版"
printf '第二行\n' >> file.txt
git add file.txt
git commit -m "feat: 加第二行"
printf '第三行\n' >> file.txt
git add file.txt
git commit -m "feat: 加第三行"
  1. 看一下现在的历史,记住最新的那个哈希:
git log --oneline
  1. 模拟手贱,硬重置回两个提交前:
git reset --hard HEAD~2
git log --oneline

只剩一条了。file.txt 也只剩第一行。

  1. 用 reflog 看你走过的路:
git reflog

你会看到每一项都有哈希和操作说明。找到刚才「加第三行」那条记录,记下它的哈希(比如 a1b2c3d)。

  1. 恢复到那个提交:
git reset --hard a1b2c3d
git log --oneline
cat file.txt

三个提交全都回来了,file.txt 也恢复了三行。

预期结果

  • reflog 里能看到 reset: moving to HEAD~2 这条记录
  • 恢复后 git log --oneline 是三条
  • file.txt 内容是三行

通过标准

  • 能指出 reflog 里哪一行是 reset 前的提交
  • 不看资料能独立用 reflog 找回丢失的提交
  • 理解 reflog 是「操作日志」,不是提交历史

容易卡的地方

卡点回查
不知道找哪个哈希reflog 输出里找 feat: 加第三行 那行
恢复后还是旧的检查是不是用了 --hard,不是 --soft
reflog 里没有记录确认你确实做过 reset 操作

4. 实验组 B:rebase 整理实验

目标: 用交互式 rebase 把多个碎提交合并成一个干净的提交。

场景

你在功能分支上做了五个碎提交:「加文件」「改错字」「又改」「再改」「终于对了」。合并前想把它们捏成一个体面的提交。

步骤

  1. 建仓并做五个碎提交:
cd ~/lab-advanced
mkdir lab-b && cd lab-b
git init -b main
printf 'base\n' > app.py
git add app.py
git commit -m "init: 初始化"
git switch -c feature/nice
  1. 连续五个碎提交:
printf 'def hello():\n    pass\n' > app.py
git add app.py
git commit -m "wip: 加函数框架"
printf 'def hello():\n    print("hi")\n' > app.py
git add app.py
git commit -m "wip: 加了打印"
printf 'def hello():\n    print("hello")\n' > app.py
git add app.py
git commit -m "wip: 改成 hello"
printf 'def hello():\n    print("hello")\n\nhello()\n' > app.py
git add app.py
git commit -m "wip: 加调用"
printf 'def hello():\n    print("hello world")\n\nhello()\n' > app.py
git add app.py
git commit -m "wip: 最终版"
  1. 看一下现在的历史:
git log --oneline

五个 wip 开头的碎提交。

  1. 开始交互式 rebase,把最近五个提交合并:
git rebase -i HEAD~5

编辑器会弹出来,长这样:

pick abc1234 wip: 加函数框架
pick def5678 wip: 加了打印
pick ghi9012 wip: 改成 hello
pick jkl3456 wip: 加调用
pick mno7890 wip: 最终版
  1. 把后面四个 pick 改成 squash(或简写 s),意思是压进前一个提交:
pick abc1234 wip: 加函数框架
squash def5678 wip: 加了打印
squash ghi9012 wip: 改成 hello
squash jkl3456 wip: 加调用
squash mno7890 wip: 最终版

保存关闭编辑器。

  1. 接着弹出第二个编辑器让你写提交说明,改成:
feat: 加 hello 函数并调用

保存关闭。

  1. 看结果:
git log --oneline

五个碎提交变成了一个干净的提交。

预期结果

  • git log --oneline 只有一条 feat: 加 hello 函数并调用
  • 代码内容跟 rebase 前一模一样
  • 历史变干净了

通过标准

  • 独立完成交互式 rebase 合并提交
  • 理解 picksquash 的区别
  • 知道 rebase 会改写历史(哈希会变)

容易卡的地方

卡点回查
编辑器不会操作vi 里按 i 进入编辑,改完按 Esc,输入 :wq 保存退出
rebase 中途改错了git rebase --abort 放弃重来
不知道选几个从第一个 pick 往后数,都要合进去就都改成 squash
冲突了怎么办先解决冲突,再 git add,再 git rebase --continue

5. 实验组 C:cherry-pick 选菜实验

目标: 从另一个分支挑选一个特定的提交,移植到当前分支。

场景

队友在 feature/ui 分支上修了一个 bug,你也需要这个修复合到你的 main 上,但不需要他分支上的其他东西。

步骤

  1. 建仓,主线上做两步:
cd ~/lab-advanced
mkdir lab-c && cd lab-c
git init -b main
printf 'v1\n' > core.py
git add core.py
git commit -m "feat: 核心功能 v1"
printf 'v2\n' > core.py
git add core.py
git commit -m "feat: 核心功能 v2"
  1. 开一个功能分支,做三个提交(其中一个是我们想要的):
git switch -c feature/ui
printf '样式1\n' > style.css
git add style.css
git commit -m "feat: 加样式文件"
printf '紧急修复\n' > fix.txt
git add fix.txt
git commit -m "fix: 关键补丁"
printf '样式2\n' >> style.css
git add style.css
git commit -m "feat: 更多样式"
  1. 找到「关键补丁」的哈希:
git log --oneline

记下 fix: 关键补丁 那行的哈希(比如 x1y2z3a)。

  1. 切回主线,只挑这个提交:
git switch main
git cherry-pick x1y2z3a
  1. 看结果:
git log --oneline
cat fix.txt

主线历史里出现了 fix: 关键补丁,但 style.css 不在。你只挑了你想要的。

预期结果

  • 主线历史里多了一条 fix: 关键补丁
  • fix.txt 出现了,但 style.css 没有
  • cherry-pick 的提交哈希跟原始的不同(因为父提交变了)

通过标准

  • 独立用 cherry-pick 挑选指定提交
  • 理解 cherry-pick 是「复制内容,生成新提交」而不是「移动提交」
  • 知道挑过来的提交哈希会变

容易卡的地方

卡点回查
挑过来有冲突跟合并冲突一样解决:改文件、add、继续
找不到哈希回原分支 git log --oneline
挑错了一个git cherry-pick --abort 放弃,或 git reset --hard HEAD~1 退回

6. 实验组 D:revert 公开撤回实验

目标:revert 安全地撤销一个已经推送的提交,不加掩饰地留痕。

场景

你昨天推了一个有问题的提交到 main,同事已经拉了。你不能改历史,只能加一个「反提交」来撤回。

步骤

  1. 建仓并做三次提交:
cd ~/lab-advanced
mkdir lab-d && cd lab-d
git init -b main
printf '功能A\n' > a.txt
git add a.txt
git commit -m "feat: 功能A"
printf '功能B(有bug)\n' > b.txt
git add b.txt
git commit -m "feat: 功能B(有bug)"
printf '功能C\n' > c.txt
git add c.txt
git commit -m "feat: 功能C"
  1. 看一下当前历史:
git log --oneline
  1. 发现「功能B」有 bug,用 revert 撤回:
git revert HEAD~1

编辑器弹出提交说明,默认是 Revert "feat: 功能B(有bug)",直接保存退出即可。

  1. 看结果:
git log --oneline
ls
cat b.txt 2>/dev/null || echo "b.txt 内容被撤回了"

历史里多了一条 Revert 提交,b.txt 的内容被还原了。原来的 feat: 功能B 提交还在历史里,没被删掉。

  1. 对比 resetrevert 的区别:
操作改不改历史适合场景
reset改历史,提交没了只在本地、没推送过
revert不改历史,加反提交已推送、别人拉过了

预期结果

  • git log --oneline 里能看到原始提交和 revert 提交
  • 被撤回的提交内容被还原
  • b.txt 不再包含原来的内容

通过标准

  • 独立用 revert 撤回一个提交
  • 能说清 revertreset --hard 的区别
  • 理解为什么已推送的提交要用 revert 而不是 reset

容易卡的地方

卡点回查
revert 有冲突说明要撤回的提交跟后面的提交改了同一块,先解决冲突再 add + git revert --continue
不知道该用 revert 还是 reset已推送的用 revert,还没推的可以用 reset
撤回的是哪个提交HEAD~1 表示当前提交的上一条,也可以用哈希

7. 实验组 E:stash 救急实验

目标: 代码写到一半,突然要切分支修 bug。先把改动藏起来,修完再取回来。

场景

你在 feature/new-ui 分支写功能,改到一半产品经理喊你修线上 bug。代码还不能提交,但切分支又被挡住。

步骤

  1. 建仓,在功能分支上写一半:
cd ~/lab-advanced
mkdir lab-e && cd lab-e
git init -b main
printf '主程序\n' > main.py
git add main.py
git commit -m "feat: 主程序"
git switch -c feature/new-ui
printf '新界面(还没写完)\n' > ui.py
git add ui.py
printf '更多内容\n' >> main.py
  1. 此时你既没提交,又有未暂存的改动:
git status
  1. 来活了,需要切到 main 修 bug。先把改动藏起来:
git stash push -m "新界面写了一半"
git status

工作区干干净净。

  1. 切到 main,修 bug:
git switch main
printf '主程序(修了bug)\n' > main.py
git add main.py
git commit -m "fix: 紧急修bug"
  1. 修完了,切回功能分支,把改动取回来:
git switch feature/new-ui
git stash list
git stash pop
git status

刚才写到一半的改动全回来了。

  1. 继续写完并提交:
git add ui.py main.py
git commit -m "feat: 完成新界面"

预期结果

  • stash 后工作区干净
  • stash pop 后改动回来了
  • 修 bug 的提交在 main 上,功能开发的提交在分支上

通过标准

  • 独立用 stash 保存和恢复半成品
  • 理解 stash pushstash pop 是一对
  • 知道 stash list 能看多个贮藏

容易卡的地方

卡点回查
pop 有冲突跟之前一样解决冲突
stash 里好多条git stash list 看编号,git stash pop stash@{2} 取指定条
忘了 pop 就继续写先 pop 再继续,别在 stash 还没取时又攒新改动

8. 实验组 F:bisect 查案实验

目标:git bisect 二分查找,定位第一个引入 bug 的提交。

场景

你知道项目上周还没 bug,今天有了。中间有十几个提交,不知道是哪个搞坏的。一个个试太慢,二分查找最快。

步骤

  1. 建仓,做一系列提交(其中一个是「坏提交」):
cd ~/lab-advanced
mkdir lab-f && cd lab-f
git init -b main

# 提交 1:好的
printf 'ok\n' > check.py
git add check.py
git commit -m "第1次提交:正常"

# 提交 2:好的
printf 'ok\nok\n' >> check.py
git add check.py
git commit -m "第2次提交:正常"

# 提交 3:坏的(偷偷改了状态)
printf 'ok\nok\nbad\n' > check.py
git add check.py
git commit -m "第3次提交:引入bug"

# 提交 4:坏的状态延续
printf 'ok\nok\nbad\nmore\n' > check.py
git add check.py
git commit -m "第4次提交:还在坏"

# 提交 5:坏的状态延续
printf 'ok\nok\nbad\nmore\nstuff\n' > check.py
git add check.py
git commit -m "第5次提交:还是坏"
  1. 写一个判断脚本:如果 check.py 里有 bad 就是坏提交,否则是好提交:
printf '#!/bin/bash\nif grep -q bad check.py; then\n  exit 1\nelse\n  exit 0\nfi\n' > test.sh
chmod +x test.sh
  1. 开始二分查找:
git bisect start
git bisect bad HEAD
git bisect good HEAD~4
  1. 让 bisect 自动跑:
git bisect run ./test.sh
  1. 它会自动切提交、跑脚本、二分缩小范围,直到找到:
第3次提交:引入bug is the first bad commit
  1. 结束 bisect,回到原来的状态:
git bisect reset

预期结果

  • bisect 自动定位到 第3次提交:引入bug
  • 不用逐个手动测试
  • 结束后回到正常工作状态

通过标准

  • 独立用 bisect 找到第一个坏提交
  • 理解二分查找的思路:好/坏标记让范围减半
  • 知道用完要 git bisect reset

容易卡的地方

卡点回查
不知道哪个是好哪个是坏你确定的好提交用 git bisect good,坏的用 git bisect bad
脚本报错确认脚本有执行权限,且在仓库根目录运行
中途想放弃git bisect reset 回到开始前的状态
手动标记太慢git bisect run 自动化

9. 实验组 G:冲突解决实验

目标: 亲手制造一次冲突,读明白冲突标记,手动解决它。

场景

两个人同一段代码改了不同的东西,合并时必冲突。这不是灾难,这是 Git 在保护你。

步骤

  1. 建仓,做一个基础版本:
cd ~/lab-advanced
mkdir lab-g && cd lab-g
git init -b main
printf '第一行\n第二行\n第三行\n' > data.txt
git add data.txt
git commit -m "init: 基础版本"
  1. 开分支 A,改第二行:
git switch -c branch-a
printf '第一行\nA改的行\n第三行\n' > data.txt
git add data.txt
git commit -m "feat: 分支A改第二行"
  1. 切回主线,也改第二行(不同内容):
git switch main
printf '第一行\nB改的行\n第三行\n' > data.txt
git add data.txt
git commit -m "feat: 主线也改第二行"
  1. 合并,必然冲突:
git merge branch-a -m "合并 branch-a"
  1. 看 Git 标记的冲突:
cat data.txt

你会看到:

第一行
<<<<<<< HEAD
B改的行
=======
A改的行
>>>>>>> branch-a
第三行

三段标记的含义:

标记内容
<<<<<<< HEAD=======你当前分支的版本
=======>>>>>>> branch-a要合并进来的版本
其他行双方没分歧的行
  1. 手动解决:两边都留着:
printf '第一行\nB改的行\nA改的行\n第三行\n' > data.txt
  1. 确认没有残留标记:
grep '<<<<<<' data.txt || echo "没有残留标记"
grep '>>>>>>' data.txt || echo "没有残留标记"
  1. 提交解决结果:
git add data.txt
git commit -m "合并 branch-a(已解决冲突)"
  1. 看最终历史:
git log --oneline --graph --all

预期结果

  • 合并时报冲突
  • 打开文件能看到 <<<<<<<=======>>>>>>> 三段标记
  • 手动改完后提交成功
  • 图上有 |\ 合并形状

通过标准

  • 能解释三段标记各是谁的版本
  • 提交前搜过 <<<<<<< 确保没残留
  • 独立解决冲突并提交
  • 不害怕冲突提示

容易卡的地方

卡点回查
改完忘了 add解决冲突后必须 git add 再 commit
标记没删干净提交前务必 grep 检查
想放弃合并git merge --abort 回到合并前
不知道该留哪边看场景决定,也可以两边都留

10. 自评表

做完七组实验后,对照自评:

能力通过标准
reflog不看资料能找回 reset --hard 丢掉的提交
rebase用交互式 rebase 合并过碎提交
cherry-pick从别的分支挑选过指定提交
revert安全撤回过已推送的提交,说清与 reset 区别
stash写到一半切分支修过 bug,改完取回来
bisect用二分法定位过第一个坏提交
冲突解决独立解决过一次冲突,没残留标记
安全意识没在真仓库练破坏性命令

11. 卡住时怎么办

症状去哪查
提交丢了第 16 章 reflog
rebase 中途想放弃git rebase --abort
cherry-pick 有冲突跟合并冲突一样解决
revert 有冲突同上
stash pop 冲突同上
bisect 找不准确认好/坏标记对了没
冲突标记没删grep '<<<<<<' . -r 全局搜

12. 学完之后

做完这七组实验,你已经把高级操作 练过手了。接下来:

  • 回到你的真项目,遇到问题不慌了
  • 需要深挖原理,回顾 09–14 章
  • 遇到没见过的场景,先 git status 看清楚状态

记住:可丢弃目录是你最好的老师。 任何操作不确定,先在玩具仓库试一遍。


13. 常见问题

问 1:实验能跳过吗? 能跳,但高级操作不练手永远停留在「知道但不会」的阶段。建议至少做 A、B、G 三组。

问 2:实验做坏了怎么办? 删掉重来。本来就是可丢弃目录。

问 3:rebase 和 merge 到底用哪个? 个人分支整理用 rebase,多人协作的公共分支用 merge。原则:别改别人已经拉走的历史。

问 4:revert 以后还能再 revert 回来吗? 能。revert 那个 revert 提交就行,或者手动把代码改回去再提交。

问 5:bisect 没有测试脚本怎么办? 手动标记也行:git bisect start,然后每到一步你手动看代码,好就 git bisect good,坏就 git bisect bad

问 6:stash 里存了多个,怎么取指定的? git stash list 看编号,git stash apply stash@{1} 取指定的但不删,git stash pop stash@{1} 取并删。

问 7:cherry-pick 跟 merge 有什么区别? cherry-pick 只拿一个提交,merge 把整个分支合进来。一个是点菜,一个是自助餐。

问 8:实验里哈希和书里不一样正常吗? 正常。哈希每次都不同,看结构和说明。


14. 总结、学习路线与思维升华

14.1 这一章请记住的

记住什么
实验高级操作不动手等于没学
七组实验reflog / rebase / cherry-pick / revert / stash / bisect / 冲突解决
自评对照表打勾
心态不怕改坏,可丢弃目录随便折腾
安全已推送用 revert,未推的用 reset,共享分支不改历史

14.2 在整个系列中的位置

01–07 基础
08 新手实验          ← 第一轮动手
09–14 原理
15–23 进阶操作
24 高级实验          ← 当前(进阶动手)

14.3 思维升华

把高级操作练成肌肉记忆,遇到真问题才不慌。 丢提交想 reflog,整理历史想 rebase,挑提交想 cherry-pick,公开撤回想 revert,写一半想 stash,找 bug 想 bisect,冲突不慌手动删标记。 每个操作你都做过一次,第二次就不会手生。

14.4 参考资料

Git 2.43.0 验证。演示身份:Ada Example <ada@example.com>

14.5 本章检查清单

  • 实验组 A:用 reflog 找回过丢掉的提交
  • 实验组 B:交互式 rebase 合并过碎提交
  • 实验组 C:cherry-pick 挑过指定提交
  • 实验组 D:revert 撤回过提交,说清与 reset 区别
  • 实验组 E:stash 保存并取回过半成品
  • 实验组 F:bisect 定位过第一个坏提交
  • 实验组 G:独立解决过一次冲突,无残留标记
  • 自评表至少六项打勾