Git 进阶工作流:rebase、cherry-pick、bisect 的正确使用

merge 会了,但 rebase 总搞错?bisect 找 bug 提交?interactive rebase 整理历史?这篇一次说清楚。

$1.4k 字/约 6 min👁— views

Git 进阶工作流:rebase、cherry-pick、bisect 的正确使用

Git 的基础操作网上到处是,但 rebase 踩坑、bisect 不会用、interactive rebase 懵逼才是大多数开发者的真实状态。

merge vs rebase 的本质区别

merge: 创建一个新的合并提交,保留完整历史分支结构。

     A---B---C  feature
    /         \
D---E---F---G---H  main(merge 后)

rebase: 把 feature 上的提交一个个"重放"到 main 的最新提交之后,历史是线性的。

             A'--B'--C'  feature(rebase 后)
            /
D---E---F---G  main

选择逻辑:

场景 用哪个
同步主线到 feature branch rebase
合并 feature 到 main 看团队约定
PR 合并 squash mergerebase merge
hotfix 合并到多个分支 cherry-pick

rebase 正确使用

基本流程

git checkout feature/login
git fetch origin
git rebase origin/main

# 如果有冲突:
# 1. 解决冲突
# 2. git add <resolved-files>
# 3. git rebase --continue
# 4. 如果想放弃:git rebase --abort

rebase 的黄金禁忌

永远不要 rebase 已经 push 到远程的公共分支。

原因:rebase 会改变提交的 SHA,导致其他人的本地历史与远程不一致,造成灾难性的合并冲突。

# 危险操作(不要做)
git checkout main
git rebase feature/new-api  # 改写了 main 的历史!

# 安全操作
git checkout feature/new-api
git rebase main  # 只改写自己的 feature 分支

Interactive Rebase:整理提交历史

# 整理最近 5 个提交
git rebase -i HEAD~5

编辑器打开后每行代表一个提交:

pick a1b2c3d 添加用户登录功能
pick e4f5g6h 修复登录 bug
pick i7j8k9l 添加单元测试
pick m1n2o3p 修改注释拼写
pick q4r5s6t WIP: 半成品代码

# 命令说明:
# pick   = 保留此提交
# reword = 保留但修改提交信息
# edit   = 暂停以修改内容
# squash = 合并到上一个提交(保留提交信息)
# fixup  = 合并到上一个提交(丢弃本提交信息)
# drop   = 删除此提交

squash 合并示例:

pick a1b2c3d 添加用户登录功能
squash e4f5g6h 修复登录 bug
squash i7j8k9l 添加单元测试
drop m1n2o3p 修改注释拼写
drop q4r5s6t WIP: 半成品代码
# 结果:一个干净的提交"添加用户登录功能(含测试)"

cherry-pick:精准摘取提交

# 从另一个分支摘取单个提交
git cherry-pick <commit-sha>

# 摘取一个范围(不含起点)
git cherry-pick A..B

# 摘取一个范围(含起点)
git cherry-pick A^..B

# 保留原始作者信息
git cherry-pick -x <commit-sha>

# 有冲突时
git cherry-pick --continue  # 解决后继续
git cherry-pick --abort      # 放弃

典型使用场景:

# hotfix 需要同步到 release 分支
git log main --oneline -5
# abc1234 fix: 修复 XSS 漏洞(需要 cherry-pick)
# def5678 feat: 新增 dark mode(不需要)

git checkout release/2.1
git cherry-pick abc1234
git push origin release/2.1

bisect:二分查找 bug 引入的提交

这是很多人不知道的神器,能在有几百个提交的历史中快速定位引入 bug 的提交。

git bisect start
git bisect bad              # 当前版本有 bug
git bisect good v1.0.0      # 某个已知正常的版本

# Git 自动 checkout 中间的提交,测试后标记:
git bisect bad    # 有 bug
git bisect good   # 没有 bug

# 几次后 Git 精确定位到引入 bug 的提交
git bisect reset  # 完成,重置

自动化 bisect(推荐):

# 写一个测试脚本(退出码 0=good, 非0=bad)
cat > /tmp/test.sh << 'EOF'
#!/bin/bash
npm test -- --testPathPattern="login" 2>/dev/null
EOF
chmod +x /tmp/test.sh

git bisect start
git bisect bad HEAD
git bisect good v2.0.0
git bisect run /tmp/test.sh
# Git 全自动完成二分查找

reflog:后悔药

reflog 记录了 HEAD 的所有移动历史,是找回"丢失"提交的救命工具。

git reflog
# HEAD@{0}: commit: 添加功能 X
# HEAD@{1}: reset: moving to HEAD~3  <- 这里 reset 了
# HEAD@{2}: commit: 添加功能 W

# 找回被 reset 丢弃的提交
git checkout HEAD@{2}  # 查看
git branch recovery HEAD@{2}  # 创建分支保存

# 撤销错误的 rebase
git reflog | grep "rebase"
git reset --hard HEAD@{N}

worktree:同时工作在多个分支

# 不用 stash,直接开多个工作目录
git worktree add ../hotfix hotfix/critical-bug
cd ../hotfix
git commit -am "fix: 修复关键 bug"
git push

cd -  # 回到主目录,继续原来的工作

git worktree remove ../hotfix

submodule vs subtree

# submodule:引用另一个仓库的特定提交
git submodule add https://github.com/xxx/lib.git libs/lib
git submodule update --init --recursive  # 克隆时需要

# subtree:把另一个仓库的内容合并进来
git subtree add --prefix=libs/lib https://github.com/xxx/lib.git main --squash
git subtree pull --prefix=libs/lib https://github.com/xxx/lib.git main --squash

选择: 不需要向上游推送改动用 subtree;需要独立开发并推回上游用 submodule。

团队工作流对比

GitHub Flow:

main ---------------------------------------- (永远可部署)
      └── feature/xxx -------- PR --------┘

Git Flow:

main ---------------------------------------- (版本发布)
develop ------------------------------------- (集成)
         └── feature/xxx -----------------┘
hotfix -- (从 main 分出,修完合回 main 和 develop)
release -- (从 develop 分出,修 bug,合回两者)

Trunk-based Development:

main -------- (直接提交或极短命 feature 分支,<= 1天)

需要强大的 CI/CD 和 feature flags,是 Google、Meta 的选择。

hooks:自动化质量保障

# .git/hooks/pre-commit(提交前自动 lint)
#!/bin/bash
set -e
npx eslint --ext .js,.ts src/ || exit 1
npx tsc --noEmit || exit 1
# .git/hooks/commit-msg(校验提交信息格式)
#!/bin/bash
COMMIT_MSG=$(cat "$1")
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf)(\(.+\))?: .{1,72}$"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
    echo "提交信息格式错误!正确格式:type(scope): description"
    exit 1
fi

使用 husky 管理 hooks(推荐):

npm install --save-dev husky lint-staged
npx husky init
{
  "lint-staged": {
    "*.{js,ts}": ["eslint --fix", "git add"],
    "*.{json,md}": ["prettier --write", "git add"]
  }
}

常用别名和配置

# ~/.gitconfig
[alias]
    lg = log --oneline --graph --decorate --all
    st = status -sb
    unstage = reset HEAD --
    undo = reset --soft HEAD~1
    wip = commit -am "WIP"

[pull]
    rebase = true  # git pull 默认用 rebase

[push]
    default = current  # 默认推送当前分支