高级技巧
本章要点
- rebase:重放提交、整理历史(与 merge 的对比、冲突处理)
- cherry-pick:精准移植某个提交
- stash:临时保存未完成的工作
- bisect:二分定位坏提交
- blame:追溯每一行代码的来历
- submodule / subtree / worktree:仓库的扩展形态
- 常用黑科技:
git grep、git clean、交互式 rebase 整理历史
rebase:变基
概念:把提交"重新播放"到另一个基点上
text
合并前(分叉了):
●───●───● main
│
└───●───● feature/login
git merge 之后(产生 merge commit):
●───●───●───────● main
│ ╱
└───●───●───
git rebase 之后(重放,历史变线性):
●───●───● main
└───●'───●' feature/login(提交被"复制"到新基点)bash
$ git switch feature/login
$ git rebase main # 把 feature 的提交依次"重放"到 main 最新之上- rebase 会改写 feature 上这些提交的哈希(提交被重新创建)。
- 好处:历史干净、线性,没有多余的 merge commit。
- 代价:改写历史,⚠️ 不要 rebase 已推送且被他人使用的分支。
rebase 冲突的处理
bash
$ git rebase main
# CONFLICT (content): Merge conflict in config.js
# 解决冲突文件 → git add config.js
$ git rebase --continue # 继续重放下一个提交
# (或)$ git rebase --abort # 放弃整个 rebase,回到开始前
# (或)$ git rebase --skip # 跳过当前提交💡 merge 与 rebase 的选择(面试高频):
- merge:保留真实历史("发生过合并"是事实),适合共享分支。
- rebase:历史线性美观,适合尚未共享的特性分支。
- 团队约定俗成:pull 用 rebase,功能合入用 merge(--no-ff)。
pull --rebase 的日常用法
bash
$ git pull --rebase origin main # 拉取远程更新,把自己的提交垫到最上面好处:避免产生"拉取合并"的噪音提交,保持本地历史线性。
交互式 rebase:整理提交历史
git rebase -i 是重写本地历史的瑞士军刀:
bash
$ git rebase -i HEAD~3 # 交互式整理最近 3 个提交
# 打开编辑器,每行一个提交,可修改动作:text
pick 3b1e7d0 feat(a): 功能A
pick c9a21f4 feat(b): 功能B
pick 8f4a2c9 fix(a): 修复A
# Commands:
# p, pick = 保留
# r, reword = 保留但修改提交信息
# e, edit = 保留但停下来修改内容
# s, squash = 合并到上一个提交
# f, fixup = 合并到上一个提交(丢弃其提交信息)
# d, drop = 删除该提交常用组合:
bash
# 把多个小提交合并成一个(squash)
pick 3b1e7d0 feat(a)
squash c9a21f4 feat(b)
squash 8f4a2c9 fix(a)
# 修改某个历史提交的信息(reword)
pick 3b1e7d0 feat(a)
reword c9a21f4 feat(b) # 保存退出后提示输入新信息⚠️ rebase -i 会改写历史哈希,只对未推送的本地提交使用。
cherry-pick:精准移植提交
把某个提交(不一定是当前分支的)原样复制到当前分支:
bash
$ git cherry-pick 8f4a2c9 # 把 8f4a2c9 的改动应用到当前分支
$ git cherry-pick 3b1e7d0..8f4a2c9 # 移植一段连续的提交(开区间)
$ git cherry-pick -n 8f4a2c9 # 只应用改动不自动提交
$ git cherry-pick --abort # 冲突时放弃典型场景:hotfix 在 main 上修好了,要把同一修复搬到 release 分支;或误提交到错误分支后移植到正确分支。
stash:临时保存工作现场
手头改到一半,但需要立刻切分支处理别的紧急任务——stash 把所有未提交的改动(含暂存区)收起来,工作区恢复干净:
bash
$ git stash # 保存当前改动(工作区变干净)
$ git stash push -m "登录页改到一半" # 带说明保存
$ git stash list # 查看保存列表
$ git stash pop # 恢复最近一次 stash 并删除记录
$ git stash apply stash@{1} # 恢复指定 stash(不删除记录)
$ git stash drop stash@{0} # 删除指定 stash
$ git stash clear # 清空所有 stashbash
# 完整场景
$ git stash # ① 保存现场
$ git switch main # ② 切分支处理紧急任务
# ...处理完...
$ git switch feature/login
$ git stash pop # ③ 恢复现场继续开发⚠️ stash 默认不包含未跟踪文件(untracked),需要加
-u:git stash -u。 💡git stash之后如果 pop 冲突(工作区变化导致),手动解决即可,stash 记录仍保留直到 drop。
bisect:二分法定位"坏提交"
当"昨天还好好的,今天坏了",用二分查找快速定位引入 bug 的提交:
bash
$ git bisect start
$ git bisect bad # 标记当前(已知坏的)提交
$ git bisect good v1.0.0 # 标记一个已知好的提交
# Git 自动检出中间提交,你来判断:
$ git bisect good # 这个提交还好 → 往新方向查
$ git bisect bad # 这个提交坏了 → 往旧方向查
# 反复几次后 Git 给出第一个坏提交:
# 8f4a2c9 is the first bad commit
$ git bisect reset # 结束并回到原分支自动化版本(可写脚本判断):
bash
$ git bisect run npm test # 用测试命令自动判断每个提交好坏blame:逐行追溯
bash
$ git blame src/app.js # 每一行的作者、提交、时间
$ git blame -L 10,20 src/app.js # 只看 10–20 行
$ git blame -w src/app.js # 忽略空白改动(-w 只看实际内容变化)输出示例:
text
3b1e7d0 (张三 2025-01-10) 10: const timeout = 3000;
8f4a2c9 (李四 2025-01-12) 11: if (isMobile) timeout = 5000;💡 结合
git log -p -L 10,20:src/app.js可以查看某几行的完整演化历史。
仓库扩展形态
submodule:子模块
把另一个 Git 仓库作为当前仓库的子目录(版本相互独立):
bash
$ git submodule add https://github.com/user/lib.git lib
$ git submodule update --init --recursive # 克隆后初始化子模块
$ git submodule update --remote # 更新子模块到远程最新- 优点:子仓库版本独立管理。
- 缺点:子模块更新需要单独操作,协作体验较差,团队需谨慎采用。
worktree:多个工作区
同一个仓库同时 checkout 多个分支到不同目录(互不干扰地并行开发):
bash
$ git worktree add ../hotfix-fix hotfix # 在 ../hotfix-fix 目录检出 hotfix 分支
$ git worktree list
$ git worktree remove ../hotfix-fix其他高频技巧
git clean:清理未跟踪文件
bash
$ git clean -n # 预览将删除哪些文件(务必先预览!)
$ git clean -f # 删除未跟踪文件
$ git clean -fd # 连同未跟踪目录一起删⚠️
git clean删除的是 Git 从未记录过的文件,删除后无法恢复,务必先用-n预览。
git grep:在历史/工作区搜索
bash
$ git grep "TODO" # 在当前工作区所有跟踪文件中搜索
$ git grep "bug" 8f4a2c9 # 在某个提交的内容里搜索查看特定提交之间的差异统计
bash
$ git diff --stat v1.0.0..v1.1.0 # 两个版本间的文件变更统计
$ git log --oneline v1.0.0..v1.1.0 # 两个版本间的提交列表配置推荐的实用行为
bash
$ git config --global rebase.autoStash true # rebase 前自动 stash 未提交改动
$ git config --global pull.rebase true # pull 默认使用 rebase
$ git config --global init.defaultBranch main # 新仓库默认主分支名 main