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 merge 或 rebase 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 # 默认推送当前分支