返回博客

Git 高级技巧:变基、挑拣和交互式暂存

大多数开发者熟悉 git addgit commitgit push 这些基础命令。但 Git 还有许多强大的高级功能,掌握它们能显著提升代码管理效率和代码库质量。

本文覆盖:交互式变基、cherry-pick、交互式暂存、stash 进阶、bisect 调试、reflog 救援

一、交互式变基(Interactive Rebase)

git rebase -i 是整理提交历史的利器:

# 交互式变基最近 4 个提交
git rebase -i HEAD~4

# 或基于某个提交
git rebase -i <base-commit>

常用操作

# 打开的编辑器中会显示类似内容:
pick a1b2c3d 添加用户登录功能
pick e4f5g6h 修复登录验证 bug
pick i7j8k9l 优化登录性能
pick m0n1o2p 添加登出功能

# 修改命令前缀来操作提交:
# pick    - 保留提交
# reword  - 保留提交,但修改提交信息
# edit    - 保留提交,但停下来修改
# squash  - 合并到前一个提交,保留提交信息
# fixup   - 合并到前一个提交,丢弃提交信息
# drop    - 删除提交

实战场景

场景 1:合并多个小提交

# 将多个小修改合并为一个完整的提交
pick a1b2c3d 添加登录 API
squash e4f5g6h 修复 typo
squash i7j8k9l 添加日志

# 结果:一个干净的提交 "添加登录 API"

场景 2:修改历史提交

# 标记要修改的提交为 edit
pick a1b2c3d 添加用户模块
edit e4f5g6h 添加登录功能    # 停在这里修改
pick i7j8k9l 添加登出功能

# 执行后会停在该提交,你可以:
git add <files>           # 添加修改
git commit --amend        # 修改当前提交
git rebase --continue     # 继续变基
黄金法则:永远不要变基已推送到远程仓库的提交。变基会改写历史,可能导致协作混乱。

二、Cherry-pick(挑拣提交)

git cherry-pick 用于将特定提交应用到当前分支:

# 挑拣单个提交
git cherry-pick <commit-hash>

# 挑拣多个提交
git cherry-pick <commit1> <commit2>

# 挑拣提交范围(不包含 start)
git cherry-pick <start>..<end>

# 只挑拣但不提交(可以修改后再提交)
git cherry-pick -n <commit-hash>

实战场景

场景:将 hotfix 从开发分支应用到生产分支

# 当前在 develop 分支
git log --oneline -5
# abc1234 修复登录验证漏洞
# def5678 添加新功能
# ...

# 切换到 main 分支
git checkout main

# 只挑拣修复提交
git cherry-pick abc1234

# 推送到远程
git push origin main

处理冲突

# cherry-pick 遇到冲突时
# 1. 手动解决冲突
# 2. 标记为已解决
git add <resolved-files>

# 3. 继续 cherry-pick
git cherry-pick --continue

# 或放弃
git cherry-pick --abort

三、交互式暂存(git add -p)

git add -p(patch mode)允许你选择性地暂存文件的部分修改:

git add -p <file>

# Git 会逐块显示修改,询问操作:
# y - 暂存此块
# n - 不暂存此块
# q - 退出
# a - 暂存此块和后续所有块
# d - 不暂存此块和后续所有块
# e - 手动编辑此块
# s - 分割成更小的块

实战场景

场景:一个文件包含多个不相关的修改

# 你在 utils.js 中做了两件事:
# 1. 修复了一个 bug
# 2. 添加了一个新工具函数

# 想要分成两个提交
git add -p utils.js

# 第一块(bug 修复)
@@ -10,7 +10,7 @@
 function validate(input) {
-    return input !== null;
+    return input !== null && input !== undefined;
 }
(1/2) Stage this hunk [y,n,q,a,d,e,?]? y

# 第二块(新函数)
@@ -50,6 +50,10 @@
     return a + b;
 }
+
+function multiply(a, b) {
+    return a * b;
+}
(2/2) Stage this hunk [y,n,q,a,d,e,?]? n

# 现在暂存区只有 bug 修复
git commit -m "fix: 修复 validate 函数的 undefined 检查"

# 暂存新函数
git add utils.js
git commit -m "feat: 添加 multiply 工具函数"
最佳实践:每次提交只做一件事。使用 git add -p 可以帮你保持提交的原子性。

四、Stash 进阶

基础 git stash 很常用,但 stash 有更多技巧:

# 基础用法
git stash                    # 暂存当前修改
git stash pop                # 恢复并删除 stash
git stash list               # 查看所有 stash

# 进阶用法
git stash save "描述信息"     # 带描述的 stash
git stash push -m "描述"      # 同上(新语法)

# 包含未跟踪的文件
git stash -u                 # 包含 untracked 文件
git stash --include-untracked # 同上

# 只暂存部分文件
git stash push -p            # 交互式选择暂存内容

# 从 stash 创建分支
git stash branch <branch-name> <stash>

# 应用特定 stash
git stash apply stash@{2}

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

# 清空所有 stash
git stash clear

实战场景

场景:紧急修复生产问题

# 正在开发新功能
vim src/new-feature.js

# 收到紧急 bug 报告,需要立即修复
git stash push -m "WIP: 新功能开发"
git checkout main
git checkout -b hotfix/urgent

# 修复 bug
vim src/bug.js
git add . && git commit -m "fix: 紧急修复"
git push origin hotfix/urgent

# 回到开发分支
git checkout feature/new-feature

# 恢复工作进度
git stash pop

五、Bisect:二分查找 Bug

git bisect 用于定位引入 bug 的提交:

# 开始 bisect
git bisect start

# 标记当前提交为有 bug
git bisect bad

# 标记某个已知正常的提交
git bisect good v1.0.0

# Git 会自动 checkout 中间提交
# 测试后标记
git bisect good   # 如果这个提交是好的
git bisect bad    # 如果这个提交有问题

# 重复直到找到问题提交
# Git 会显示:
# abc1234 is the first bad commit

# 结束 bisect
git bisect reset

自动化 bisect

# 如果有自动化测试脚本
git bisect start
git bisect bad
git bisect good v1.0.0
git bisect run npm test

# Git 会自动运行测试并找到问题提交

六、Reflog:时光倒流

git reflog 记录所有 HEAD 的移动,可以找回"丢失"的提交:

# 查看操作历史
git reflog

# 输出示例:
# abc1234 HEAD@{0}: reset: moving to HEAD~2
# def5678 HEAD@{1}: commit: 添加新功能
# ghi9012 HEAD@{2}: commit: 修复 bug
# ...

# 恢复到某个状态
git reset --hard HEAD@{1}

# 或直接用提交 hash
git reset --hard def5678

实战场景

场景:误删分支或错误 reset

# 不小心 hard reset 丢了工作
git reset --hard HEAD~3

# 惊慌... 不怕!
git reflog

# 找到之前的状态
# abc1234 HEAD@{0}: reset: moving to HEAD~3
# def5678 HEAD@{1}: commit: 重要的工作

# 恢复
git reset --hard def5678

# 找回误删的分支
git reflog
# 找到分支删除前的提交
git checkout -b recovered-branch <commit-hash>
Reflog 有效期:默认 90 天。可以通过 gc.reflogExpire 配置。

七、其他实用技巧

1. 交互式重置

# 类似 add -p,但是针对重置
git reset -p

# 撤销部分暂存
git reset HEAD -p

2. 清理工作区

# 查看将被删除的文件(预览)
git clean -n

# 删除未跟踪的文件
git clean -f

# 删除未跟踪的文件和目录
git clean -fd

# 同时删除忽略的文件
git clean -fdx

3. 提交搜索

# 搜索提交信息
git log --grep="关键词"

# 搜索代码变更
git log -S "function_name"

# 搜索代码变更(正则)
git log -G "pattern"

# 查看某个文件的修改历史
git log -p -- path/to/file

# 谁修改了这行代码
git blame -L 10,20 path/to/file

4. 工作区 vs 暂存区 vs HEAD

# 工作区 vs 暂存区
git diff

# 暂存区 vs HEAD
git diff --staged
git diff --cached  # 同上

# 工作区 vs HEAD
git diff HEAD

# 两个提交之间
git diff abc123..def456

总结

命令用途危险程度
git rebase -i整理提交历史⚠️ 不要变基已推送的提交
git cherry-pick选择性应用提交✅ 安全
git add -p部分暂存✅ 安全
git stash -u暂存工作进度✅ 安全
git bisect二分查找 bug✅ 安全(只读)
git reflog找回丢失提交✅ 救命神器
git clean -fd清理未跟踪文件⚠️ 不可恢复
危险操作清单
git reset --hard - 丢弃所有未提交的修改
git clean -fd - 删除未跟踪的文件
git push --force - 强制推送,覆盖远程历史
git rebase 已推送的提交 - 改写公共历史
学习资源
Pro Git 书籍:git-scm.com/book/zh/v2
Git 官方文档:git-scm.com/docs
Learn Git Branching:learngitbranching.js.org