编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 质量保障

  • 产品思考

  • 软实力

  • 开发流程

  • Git应用

    • README
    • 版本控制的诞生:从一场灾难说起
    • 单人工作流:在 Git 的时光机里自由穿梭
    • 分支:Git 的灵魂——像开平行宇宙一样开发
    • 远程协作:把你的代码推到全世界
    • Git特种作战:stash、cherry-pick、bisect三件套
      • 一、中断开发
      • 二、暂停手艺
        • 2.1 stash 做了什么?
        • 2.2 实战:中断开发 → 修紧急 bug → 恢复现场
        • 2.3 stash 的高频操作
        • 2.4 stash 的进阶用法:部分保存
      • 三、精准移植
        • 3.1 cherry-pick 解决什么问题?
        • 3.2 动手:把 hotfix 的修复移植到 develop
        • 3.3 cherry-pick 的进阶操作
        • 3.4 cherry-pick vs merge:什么时候用谁?
      • 四、二分定位
        • 4.1 bisect 解决什么问题?
        • 4.2 实战:二分法找出谁搞坏了 getTotalPrice
        • 4.3 退出 bisect 模式
        • 4.4 bisect 的自动化(有测试用例时更强大)
      • 五、追溯责任
        • 5.1 追责不是目的,定位上下文才是
        • 5.2 常用选项
        • 5.3 真实应用场景
      • 六、终极救生艇
        • 6.1 找回误删的分支
        • 6.2 找回 rebase 之前的原始 commit
      • 七、综合实战
        • 场景设定
        • 实战开始
        • 🎯 时间线回顾
      • 八、本章回顾
        • 📎 本章涉及的命令速查
    • 团队工作流实战:从一个人能打到一队人能战
    • Git故障排除:遇到报错不再慌的急诊手册
    • Git 场景速查地图:遇到问题对号入座
    • 常见操作实践:从理论到实战的最后一步
  • 技术模版

  • 技术规范

  • markdown

  • mermaid

  • license

  • 博客部署

  • 技术招聘

  • 测试经验

  • 技术
  • Git应用
杨充
2025-06-06
目录

Git特种作战:stash、cherry-pick、bisect三件套

# 第5章 · 特种作战

# 一、中断开发

下午 4 点 20 分,小李正在实现一个复杂的「数据导出功能」。他改了 5 个文件、新增了一个 export.js、重构了 data.js 里一半的函数。代码现在完全跑不起来——因为导出功能还差最后两步。

突然 Leader 的消息弹出来:

"线上支付回调接口挂了,所有订单都无法支付。紧急!马上修!"

小李看着终端里的 git status:

Changes not staged for commit:
  modified:   data.js
  modified:   user.js
  modified:   report.js
  modified:   config.js
  modified:   utils.js

Untracked files:
  export.js
1
2
3
4
5
6
7
8
9

怎么办?

  • ❌ 直接 commit?不行,代码根本跑不起来,commit 一个 broken commit 是团队耻辱。
  • ❌ 直接切分支?不行,Git 会阻止——工作区有未保存的改动,切分支会覆盖它们。
  • ❌ 全部丢掉?不行,写了两个小时的心血。
  • ❌ 把整个目录复制一份?第 3 章已经证明这是低效灾难。

这就是本章要解决的问题:当你的工作区处于「不能 commit 又不能丢」的尴尬状态时,Git 给了你三件特种作战武器。


# 二、暂停手艺

# 2.1 stash 做了什么?

 你的工作区(杂乱)                   stash 栈
┌────────────────────┐          ┌─────────────┐
│ data.js   (已修改) │   stash  │  存起来 ✅   │
│ export.js (新文件) │ ───────→ │             │
│ config.js (已修改) │          ├─────────────┤
└────────────────────┘          │             │
   git status: clean 🎉         └─────────────┘
1
2
3
4
5
6
7

git stash 的底层逻辑:把你工作区和暂存区的所有改动,打包压缩成一个临时 commit,存到 stash 栈里。然后工作区恢复到上一次 commit 的干净状态。

# 2.2 实战:中断开发 → 修紧急 bug → 恢复现场

mkdir stash-lab && cd stash-lab && git init

# 模拟已有的项目基础
echo "function pay() { /* 支付逻辑 */ }" > payment.js
echo "function login() { /* 登录逻辑 */ }" > user.js
git add . && git commit -m "init: 基础功能"

# ==================== 小李正在开发导出功能 ====================

# 改了一大堆文件
echo "function exportData() { /* 导出数据 - 未完成 */ }" > export.js
echo "// 重构数据模块" >> payment.js
echo "function exportUser() {}" >> user.js
echo "const EXPORT_CONFIG = { debug: true };" > config.js

git add config.js   # config.js 已经 add 了
# export.js 是不跟踪的新文件

git status
#输出:
# Changes to be committed:
#   new file: config.js
# Changes not staged for commit:
#   modified: payment.js
#   modified: user.js
# Untracked files:
#   export.js

# ==================== 🚨 紧急 bug 来了! ====================

# Step 1:连不跟踪的文件一起保存
git stash --include-untracked    # -u 缩写,保存新文件
# 或:git stash -u -m "导出功能开发到一半"

git status
# 输出:nothing to commit, working tree clean
# 🎉 工作区干净了!导出的代码全部安全保存

# Step 2:查看 stash 列表
git stash list
# 输出:stash@{0}: On master: 导出功能开发到一半

# Step 3:切分支,修 bug
git switch -c hotfix/支付回调修复

# 修复线上支付回调
cat > payment.js << 'EOF'
function pay(orderId) {
  // 修复:回调地址编码问题
  const callback = encodeURIComponent('/api/callback');
  console.log('支付成功,回调地址:', callback);
  return { success: true };
}
EOF

git add . && git commit -m "hotfix: 修复支付回调地址编码问题"

# Step 4:合并回 master
git switch master
git merge --no-ff hotfix/支付回调修复 -m "merge: 合并支付回调 hotfix"

# Step 5:回到 feature 分支,恢复现场!
git stash pop
# 输出类似:
# On branch master
# Changes not staged for commit:
#   ...
# Dropped refs/stash@{0} (abc123...)  ← stash 被清掉了

git status
# 🎉 导出功能的代码全回来了!和 stash 前一模一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

# 2.3 stash 的高频操作

# 查看 stash 列表
git stash list

# 只保存已跟踪的文件(不保存新文件)
git stash                     # 快捷但不安全,新文件会丢

# 保存所有(含新文件和忽略文件)
git stash --include-untracked  # 推荐!-u 缩写
git stash --all                # 连 .gitignore 里的也存

# 带备注
git stash push -m "导出功能开发到一半,先修支付回调"

# 恢复最新的 stash 并删除
git stash pop

# 恢复最新的 stash 但不删除
git stash apply

# 恢复指定的 stash
git stash apply stash@{2}

# 删除某个 stash
git stash drop stash@{1}

# 清空所有 stash
git stash clear

# 查看 stash 里存了什么
git stash show -p stash@{0}
# -p 显示详细 diff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 2.4 stash 的进阶用法:部分保存

# 只 stash 某个文件
git stash push -m "只存 config.js" -- config.js

# 只 stash 已暂存的文件(保留工作区未暂存的)
git stash --keep-index
1
2
3
4
5

💡 场景映射:stash 不仅用于「中断开发去修 bug」,还适用于:

  • 你改着改着发现方向错了,想先把当前改动存起来,试试别的方案
  • 你 pull 之前本地有改动,不想 commit,先 stash → pull → pop
  • 你改出了 bug 不确定是哪次改动引起的,stash 一部分 → 测试 → 二分定位

# 三、精准移植

# 3.1 cherry-pick 解决什么问题?

在第 3 章,我们学了 merge——把一个分支的所有改动合过来。在第 4 章,我们学了 rebase——把你分支的所有 commit 搬到别人后面。

但如果我只想要一个 commit,不要其他的呢?

  • 你在 hotfix 分支上修了一个 bug,这个修复需要同步到 main 和 develop 两个分支。但 hotfix 上有 3 个 commit,只有一个跟这个 bug 相关。
  • 同事在他的实验分支上写了一个非常棒的工具函数,你想拿到你的 feature 分支用。但那个实验分支上还有一堆你不需要的东西。

这就是 cherry-pick 的场景:从一堆樱桃里,只摘你想要的这一颗。

# 3.2 动手:把 hotfix 的修复移植到 develop

mkdir cherry-lab && cd cherry-lab && git init

# 模拟主线开发
echo "const app = { version: '1.0' };" > app.js
git add . && git commit -m "init: v1.0"

# 线上报了个 bug,hotfix 分支修复
git switch -c hotfix/紧急修复

echo "function safeJSONParse(str) { try { return JSON.parse(str); } catch { return null; } }" >> app.js
git add . && git commit -m "fix: 添加安全 JSON 解析,防止崩溃"

# hotfix 上还做了一些其他改动(跟 bug 无关)
echo "// 日志临时调试" >> app.js
git add . && git commit -m "tmp: 加一些调试日志后面删"  # 这个不要移植

echo "const DEBUG = process.env.NODE_ENV !== 'production';" >> app.js
git add . && git commit -m "chore: 配置环境变量"  # 这个也不要

# 现在 hotfix 上有 3 个 commit
git log --oneline
# 输出:
# c3c3c3c chore: 配置环境变量
# b2b2b2b tmp: 加一些调试日志后面删
# a1a1a1a fix: 添加安全 JSON 解析,防止崩溃

# ==================== 回到 develop 分支 ====================
git switch -c develop

# develop 上也有一些自己的改动
echo "function fetchData() { /* 获取数据 */ }" >> app.js
git add . && git commit -m "feat: 添加数据获取函数"

git log --oneline --graph --all
# 输出:
# * d4d4d4d (develop) feat: 添加数据获取函数
# | * c3c3c3c (hotfix/紧急修复) chore: 配置环境变量
# | * b2b2b2b tmp: 加一些调试日志后面删
# | * a1a1a1a fix: 添加安全 JSON 解析,防止崩溃
# |/
# * 0000000 (master) init: v1.0

# ==================== 🎯 cherry-pick! ====================
# 我只要 a1a1a1a 那个「安全 JSON 解析」的修复
git cherry-pick a1a1a1a

# 输出:
# [develop e5e5e5e] fix: 添加安全 JSON 解析,防止崩溃
#  Date: ...
#  1 file changed, 1 insertion(+)

git log --oneline --graph --all
# 输出:
# * e5e5e5e (develop) fix: 添加安全 JSON 解析,防止崩溃  ← 移植过来了!
# * d4d4d4d feat: 添加数据获取函数
# | * c3c3c3c (hotfix/紧急修复) chore: 配置环境变量
# | * b2b2b2b tmp: 加一些调试日志后面删
# | * a1a1a1a fix: 添加安全 JSON 解析,防止崩溃           ← 原始 commit
# |/
# * 0000000 (master) init: v1.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

注意:cherry-pick 过来的 commit 有新的 hash(e5e5e5e ≠ a1a1a1a),但改动内容一样。

# 3.3 cherry-pick 的进阶操作

# 移植多个不连续的 commit
git cherry-pick aabbcc ddeeff

# 移植一个连续范围的 commit(不含起点,含终点)
git cherry-pick a1a1a1a..c3c3c3c
# 移植 b2b2b2b 和 c3c3c3c,不含 a1a1a1a

# 移植一个连续范围(含起点)
git cherry-pick a1a1a1a^..c3c3c3c

# 只取改动不自动提交(取过来先放着,改完再 commit)
git cherry-pick -n a1a1a1a
# -n = --no-commit

# 移植时顺便修改 commit message
git cherry-pick -e a1a1a1a
# -e = --edit

# 如果冲突了
# → 手动解决冲突文件
# → git add <冲突文件>
# → git cherry-pick --continue

# 如果不想继续了
git cherry-pick --abort

# 跳过当前冲突的 commit,继续下一个
git cherry-pick --skip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 3.4 cherry-pick vs merge:什么时候用谁?

场景 用什么 原因
整个功能分支合入主分支 merge 保留完整上下文
单个 bug 修复需要同步到多个分支 cherry-pick 只要这一个修复,不要别的
把同事某个实验分支的工具函数拿过来 cherry-pick 只要那个函数,不要他实验的垃圾
发布分支上打补丁 cherry-pick main 上修了,需要同步到 release 分支

# 四、二分定位

# 4.1 bisect 解决什么问题?

你的项目有 200 个 commit。v2.0 跑得好好的,v2.1 出了个 bug——某个接口偶尔返回 500。你看了半天代码,200 个 commit 每个都可能有问题。

逐个人工检查 200 个 commit? 今晚不用回家了。

二分查找来自动定位! 你告诉 Git「commit #50 是好的」「commit #200 是坏的」,Git 自动帮你跳到第 125 个——测试一下,好的?那 bug 在 125 后面。坏的?那 bug 在 125 前面。最多 log₂(150) ≈ 8 次测试就能定位。

# 4.2 实战:二分法找出谁搞坏了 getTotalPrice

mkdir bisect-lab && cd bisect-lab && git init

# 构建 8 个 commit 的模拟项目
echo 'const app = "Shop";' > app.js
git add . && git commit -m "v1: 项目初始化"                # commit 1

echo 'function login() { return true; }' >> app.js
git add . && git commit -m "v2: 登录"                      # commit 2

echo 'function logout() {}' >> app.js
git add . && git commit -m "v3: 登出"                      # commit 3

echo 'let total = 0;' >> app.js
echo 'function addToTotal(n) { total += n; }' >> app.js
git add . && git commit -m "v4: 购物车总计"                # commit 4 ✅

echo 'function discount(rate) { total *= rate; }' >> app.js
git add . && git commit -m "v5: 折扣"                      # commit 5 ✅

echo '// 重构总计逻辑' >> app.js
# 🔴 这里引入了一只隐晦 bug:把 total 的计算方式改了
sed -i '' 's/total += n/total = n/' app.js   # 把 += 改成 =,导致 total 变成最后一个值
git add . && git commit -m "v6: 重构计数逻辑"             # commit 6 💀 bug 在这!

echo 'function clearTotal() { total = 0; }' >> app.js
git add . && git commit -m "v7: 清空购物车"               # commit 7 💀

echo 'function getTotal() { return total; }' >> app.js
git add . && git commit -m "v8: 获取总计"                 # commit 8 💀

# 现在 v4、v5 是好的,v6、v7、v8 是坏的
# 但你不知道 bug 在哪次引入的!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

开始二分定位:

# Step 1:开始二分查找
git bisect start

# Step 2:标记当前版本是坏的(v8)
git bisect bad HEAD
# 或:git bisect bad v8

# Step 3:标记 v5 是好的(你确认过 v5 没问题)
git log --oneline
# 假设 v5 的 hash 是 5555555
git bisect good 5555555

# Step 4:Git 自动跳到中间位置(v6 和 v7 之间)
# Git 输出:
# Bisecting: 1 revision left to test after this (roughly 1 step)
# [6666666] v6: 重构计数逻辑

# 现在你在 commit 6(v6: 重构计数逻辑)
# 测试一下:v6 有没有 bug?
# 你可以手动测试:
node -e "
let total = 0;
function addToTotal(n) { total = n; }   // 从 app.js 拷贝
addToTotal(100);
addToTotal(200);
console.log(total);  // 输出 200 ← 应该是 300!bug 在!
"

# 确认 v6 是坏的
git bisect bad

# Step 5:Git 继续缩小范围
# 只剩下 v5(好)和 v6(坏)之间的一个版本了
# Bisecting: 0 revisions left to test after this
# [5555555] v6: 重构计数逻辑

# 🎯 定位完成!
# Git 输出:
# 6666666 is the first bad commit
# commit 6666666
# Author: 杨充 <yangchong@example.com>
# Date:   ...
#
#     v6: 重构计数逻辑
#
#  app.js | 2 +-
#  1 file changed, 1 insertion(+), 1 deletion(-)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

Git 精准找到了那个把 += 改成 = 的 commit!

# 4.3 退出 bisect 模式

git bisect reset
# 回到 bisect 之前的状态
1
2

# 4.4 bisect 的自动化(有测试用例时更强大)

# 如果你有单元测试,一条命令自动跑完所有二分步骤:
git bisect start HEAD v5
git bisect run npm test
# Git 会自动在每个 commit 上执行 npm test
# test 通过 → 标记 good
# test 失败 → 标记 bad
# 最终输出第一个坏的 commit

# 完成!
git bisect reset
1
2
3
4
5
6
7
8
9
10

💡 bisect 的强大之处:不管项目有多少个 commit,定位时间都是 O(log N)。1000 个 commit 只需要约 10 次测试。这个算法思想(二分查找)被用在几乎所有调试场景中。


# 五、追溯责任

# 5.1 追责不是目的,定位上下文才是

git blame app.js
# 输出:
# 0000000 (杨充 2025-06-06 10:00:00 +0800  1) const app = "Shop";
# 2222222 (杨充 2025-06-06 10:01:00 +0800  2) function login() { return true; }
# 3333333 (杨充 2025-06-06 10:02:00 +0800  3) function logout() {}
# ...
# 6666666 (杨充 2025-06-06 10:05:00 +0800  6) function addToTotal(n) { total = n; }
1
2
3
4
5
6
7

输出解读:每一行前面都标注了「谁 + 什么时候 + 哪个 commit」最后改了它。

# 5.2 常用选项

# 只看某个范围的行
git blame -L 5,10 app.js

# 看特定 commit 版本的 blame
git blame <commit-hash> app.js

# 忽略空白改动(只追踪实质修改)
git blame -w app.js

# 追踪代码移动(某行是从哪个文件拷过来的)
git blame -M app.js

# 更激进的移动追踪
git blame -C app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 5.3 真实应用场景

# 场景1:代码里有个奇怪的写法,你想知道当时为什么这么写
git blame app.js -L 6,6
# 找到 commit hash → 看 commit message → 理解历史背景

# 场景2:线上 bug 排查——这行报错代码是谁改的
git blame -L 15,15 src/api/user.js
# 找到最近一次修改 → git show 那个 commit → 确认是功能改动还是错误引入
1
2
3
4
5
6
7

💡 blame 的正确心态:不是用来「追责批斗」,而是用来「理解上下文」。看到一行代码看不懂为什么这么写,blame 找到 commit → 看 commit message → 可能还关联一个 PR → 里面有人讨论过为什么选这个方案。这比删了重写安全得多。


# 六、终极救生艇

第 2 章我们体验了 reflog 的最基本用法(reset --hard 后找回)。这里补充两个高阶场景。

# 6.1 找回误删的分支

# 模拟:手滑删了一个还在开发中的分支
git branch
#   develop
# * feature/新支付

git switch develop
git branch -D feature/新支付   # 💀 没合并就删了!

# 找回!
git reflog --date=iso
# 找到 feature/新支付 被删之前指向的 commit
# 输出类似:
# abcdef1 HEAD@{3}: checkout: moving from feature/新支付 to develop
#                               ↑ 看到它了!

git switch -c feature/新支付 abcdef1
# 🎉 分支回来了,代码全在!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 6.2 找回 rebase 之前的原始 commit

# 你做了 rebase,觉得不满意,想回到 rebase 之前
git reflog
# 找到 rebase 之前的 ORIG_HEAD 或 HEAD@{n}

git reset --hard HEAD@{5}
# 🎉 回到 rebase 之前,一切照旧
1
2
3
4
5
6

# 七、综合实战

# 场景设定

你是小李,上午有三件事:

  1. 正在开发「导出报告」功能(还没写完)
  2. 线上报了个 bug——用户搜索时输入特殊字符会崩溃(需要 cherry-pick 到 develop 和 release 两个分支)
  3. 一个历史遗留 bug:某个接口偶尔超时,不知道是哪次 commit 引入的

用今天学的三件套,逐个击破。

# 实战开始

# ==================== Phase 1:环境准备 ====================
mkdir spec-ops && cd spec-ops && git init

echo 'function search(keyword) {
  return db.query("SELECT * FROM products WHERE name LIKE '%" + keyword + "%'");
}' > search.js

echo '// 购物车模块
const cart = [];' > cart.js

echo 'module.exports = { search, cart };' > index.js

git add . && git commit -m "init: 项目初始化"

# 创建 develop 分支和 release 分支
git branch develop
git branch release

# develop 上开发了一些新功能
git switch develop
echo 'function exportReport() { /* TODO */ }' > report.js
git add . && git commit -m "feat: 导出报告占位"

# 模拟线上分支的演进
git switch master
echo '// 修复上一版 typo' >> search.js
git add . && git commit -m "fix: typo"

git switch release
echo '// release 分支补丁' >> search.js
git add . && git commit -m "chore: release 配置"


# ==================== Phase 2:事件一——开发中被叫去修 bug ====================
git switch develop

# 小李开始写导出报告的真正逻辑
cat > report.js << 'EOF'
function exportReport(format) {
  // 支持导出为 CSV 和 PDF
  if (format === 'csv') {
    // 转换数据为 CSV 格式
    let csv = '';
    // 未完成:还需要把 JSON 转成 CSV 行
  }
  // TODO: PDF 格式
}
EOF

echo 'const REPORT_CONFIG = { maxRows: 10000 };' > report-config.js

git add report-config.js  # 这个已经 add 了

git status
# 输出:
# Changes to be committed:
#   new file: report-config.js
# Changes not staged for commit:
#   modified: report.js


# 🚨 线上 bug:搜索特殊字符导致 SQL 注入!
# Step 1:保存现场
git stash --include-untracked -m "导出报告开发到一半,先去修搜索 SQL 注入"

# Step 2:创建 hotfix 分支
git switch -c hotfix/sql注入 master
# 注意:hotfix 应该从 master(线上分支)开出

# Step 3:修复
cat > search.js << 'EOF'
function search(keyword) {
  // 修复:使用参数化查询,防止 SQL 注入
  const sanitized = keyword.replace(/['";\\]/g, '');
  return db.query("SELECT * FROM products WHERE name LIKE ?", [`%${sanitized}%`]);
}
EOF

git add . && git commit -m "hotfix: 修复搜索 SQL 注入漏洞"


# ==================== Phase 3:事件二——cherry-pick 到多个分支 ====================
# 这个修复需要同步到 develop 和 release

# 切到 develop,cherry-pick 过来
git switch develop
git cherry-pick $(git log hotfix/sql注入 --oneline | head -1 | awk '{print $1}')

# 如果冲突了(develop 的 search.js 可能跟 master 不同),手动解决
# 假设没冲突,继续

# 切到 release,同样 cherry-pick
git switch release
git cherry-pick $(git log hotfix/sql注入 --oneline | head -1 | awk '{print $1}')

# 现在三个分支都有了修复

# 合并 hotfix 到 master
git switch master
git merge --no-ff hotfix/sql注入 -m "merge: 合并搜索 SQL 注入修复"


# ==================== Phase 4:恢复导出报告开发 ====================
git switch develop
git stash pop

# 🎉 导出报告的代码全回来了!
git status
# 确认:report.js 已修改,report-config.js 在暂存区


# ==================== Phase 5:事件三——用 bisect 定位历史 bug ====================
# 产品经理反馈:某个 API 偶尔超时,大概是最近两周引入的
# 已知两周前(约 10 个 commit 之前)没问题

# 模拟:在历史里插入一个「超时 bug」...
# 先看看最近的提交
git log --oneline
# 假设输出:
# xxxxxxx hotfix 合并
# yyyyyyy feat: 导出报告占位
# zzzzzzz init: 项目初始化

# (这里用 bisect 的模拟场景已经足够说明用法,不再重复构建历史)

echo '
# 手动二分查找示范(概念):  
#
# git bisect start
# git bisect bad HEAD
# git bisect good <两周前的commit>
#
# Git 自动跳到中间,你测试 → good/bad → Git 再跳到中间...
# 最多 log₂(提交数) 次测试就能定位
#
# git bisect reset   # 退出二分模式
'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

# 🎯 时间线回顾

  9:00 AM          10:00 AM        10:15 AM          10:30 AM         11:00 AM
──────┼────────────────┼───────────────┼─────────────────┼────────────────┼──→
      │                │               │                 │                │
 开始写导出报告    🚨 线上报 bug    stash 保存现场    cherry-pick 到   stash pop
 写到一半          搜索 SQL 注入   创建 hotfix 修复   develop+release   继续开发
1
2
3
4
5

三件套的协作模式:stash 让你随时中断、随时恢复;cherry-pick 让你精准同步单个修复到多个分支;bisect 让你在几百个 commit 里快速找到元凶。一个上午三个危机,全靠这三件套从容化解。


# 八、本章回顾

我学会了什么 一句话总结
stash 把你的半成品打包暂存,还你一个干净的工作区
stash pop vs apply pop 用完即删;apply 保留以便复用
stash -u 务必加 -u,否则新创建的文件会被忽略
cherry-pick 精准摘取一个 commit 到另一个分支
cherry-pick 的冲突 同 merge:手动解决 → add → --continue
cherry-pick vs merge 单 commit 用 cherry-pick;完整功能用 merge
bisect 二分法自动定位第一个引入 bug 的 commit
bisect run 有测试用例时,全自动二分定位
blame 看每一行代码的最后修改人和时间
reflog 30 天内的任何操作都有记录——删分支、rebase 都能救

🎯 核心习惯:

  1. 工作区不干净又不想 commit → 立刻 git stash -u -m "备注",不要带着半成品切分支。
  2. 只同步一个修复到另一个分支 → cherry-pick,不要盲目 merge 一堆东西。
  3. 遇到不知道哪次提交引入的 bug → git bisect,不要肉眼逐个人工排查。

🏃 下一章预告:个人技巧学完了。接下来是团队的战场——Git Flow 工作流。你会学到:main / develop / feature / release / hotfix 五条分支各自的分工、一次完整的发布流程、保护分支的配置、以及 Conventional Commits 规范。从「一个人能打」到「团队能战」。


# 📎 本章涉及的命令速查

# === stash(保存现场) ===
git stash                               # 只保存已跟踪文件(不推荐)
git stash -u                            # 推荐:含未跟踪文件
git stash -u -m "备注"                  # 带备注
git stash push -m "备注" -- <file>      # 只 stash 指定文件
git stash list                          # 查看 stash 列表
git stash show -p stash@{0}             # 查看 stash 里存了什么
git stash pop                           # 恢复 + 删除
git stash apply                         # 恢复 + 保留
git stash apply stash@{2}               # 恢复指定的
git stash drop stash@{1}                # 删除指定的
git stash clear                         # 清空所有

# === cherry-pick(精准移植) ===
git cherry-pick <hash>                  # 移植单个 commit
git cherry-pick <hash1> <hash2>         # 移植多个不连续
git cherry-pick <hashA>..<hashB>        # 移植连续范围(不含 A)
git cherry-pick <hashA>^..<hashB>       # 移植连续范围(含 A)
git cherry-pick -n <hash>               # 只取改动不 commit
git cherry-pick -e <hash>               # 移植时修改 message
git cherry-pick --continue              # 解决冲突后继续
git cherry-pick --abort                 # 放弃
git cherry-pick --skip                  # 跳过冲突的

# === bisect(二分定位) ===
git bisect start                        # 开始二分模式
git bisect bad HEAD                     # 标记当前是坏的
git bisect good <hash>                  # 标记已知好的版本
git bisect reset                        # 退出二分模式
git bisect run <test-command>           # 自动化二分

# === blame(追溯每行代码) ===
git blame <file>                        # 逐行显示作者+时间
git blame -L 5,10 <file>                # 只看指定行
git blame -w <file>                     # 忽略空白改动
git blame -M <file>                     # 追踪代码移动

# === reflog(终极后悔药) ===
git reflog                              # 查看 HEAD 移动记录
git reflog --date=iso                   # 带时间戳
git reflog show <branch>                # 查看特定分支的 reflog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#Git
上次更新: 2026/06/07, 10:26:12
远程协作:把你的代码推到全世界
团队工作流实战:从一个人能打到一队人能战

← 远程协作:把你的代码推到全世界 团队工作流实战:从一个人能打到一队人能战→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式