编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • 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

怎么办?

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

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


# 二、暂停手艺

# 2.1 stash 做了什么?

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

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 前一模一样

# 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

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

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

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

💡 场景映射: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

注意: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

# 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 在哪次引入的!

开始二分定位:

# 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(-)

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

# 4.3 退出 bisect 模式

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

# 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

💡 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; }

输出解读:每一行前面都标注了「谁 + 什么时候 + 哪个 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

# 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 → 确认是功能改动还是错误引入

💡 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
# 🎉 分支回来了,代码全在!

# 6.2 找回 rebase 之前的原始 commit

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

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

# 七、综合实战

# 场景设定

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

  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   # 退出二分模式
'

# 🎯 时间线回顾

  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   继续开发

三件套的协作模式: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
#Git
上次更新: 2026/06/28, 17:55:19
远程协作:把你的代码推到全世界
团队工作流实战:从一个人能打到一队人能战

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

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式