Git 多人协作踩坑实录:rebase 与 merge 的选择

Git 的 merge 和 rebase 都能合并分支,但效果截然不同。团队协作中用错方案会导致提交历史混乱、冲突难解。本文从原理到实战,讲清楚何时用 merge、何时用 rebase。

merge 与 rebase 区别

merge:三路合并

merge 会创建一个新的合并提交,将两个分支的历史合并在一起:

1
2
3
        A---B---C feature
/ \
D---E---F---------G main (merge commit G)
1
2
3
# 在 main 分支执行
git checkout main
git merge feature

特点:

  • 不改变历史
  • 保留完整的分支信息
  • 合并提交可能看起来有点乱

rebase:变基

rebase 会将当前分支的提交重演到目标分支的顶部:

1
2
3
        A'--B'--C'  feature (rebase 后的提交)
/
D---E---F main
1
2
3
# 在 feature 分支执行
git checkout feature
git rebase main

特点:

  • 历史线性、整洁
  • 没有合并提交
  • 重写提交历史

黄金法则

绝对不要对已经推送到远程的提交执行 rebase!

1
2
# 危险操作!
git rebase main # 如果 main 已经推送,会出问题

原因:

  • rebase 会生成新的提交,内容相似但 hash 不同
  • 其他人的分支基于旧提交,会产生分叉
  • 强制推送会覆盖他人的工作

场景选择

何时用 merge

1. 合并主分支到特性分支

1
2
3
4
5
6
7
8
# feature 分支开发中,定期从 main 同步
git checkout feature
git merge main

# 保留完整的合并历史
# D---E---F main
# \
# A---B---C---M feature (M 是 merge commit)

2. 合并特性分支到主分支(Pull Request)

1
2
3
4
5
# GitHub/GitLab 上的 PR 通常用 merge
git checkout main
git merge feature

# 保留完整的特性分支信息

3. 公共分支

永远不要 rebase maindevelop 等公共分支。

何时用 rebase

1. 整理本地提交

在 feature 分支上整理提交历史:

1
2
3
4
5
6
# 压缩多个提交
git rebase -i HEAD~3

# pick 1a2b3c4 第一提交
# squash 5d6e7f8 第二提交
# squash 9a0b1c2 第三提交

2. 保持 feature 分支跟随 main

1
2
3
4
5
6
7
8
9
10
11
12
# 在 feature 分支上
git rebase main

# 之前:
# A---B---C feature
# /
# D---E---F main

# 之后:
# A'--B'--C' feature
# /
# D---E---F main

3. 干净的线性历史

如果团队要求线性历史,用 rebase。

冲突解决技巧

rebase 冲突

rebase 遇到冲突时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# rebase 过程遇到冲突
git rebase main
# 冲突:both modified: app.py

# 1. 解决冲突
vim app.py
git add app.py

# 2. 继续 rebase
git rebase --continue

# 或者跳过当前提交
git rebase --skip

# 或者中止 rebase,恢复原状态
git rebase --abort

多次冲突

rebase 会逐个提交重演,每个提交都可能冲突:

1
2
3
4
5
6
7
8
9
10
# 假设 rebase 遇到冲突
git rebase main
# 冲突在提交 C

# 解决第一个冲突
git add app.py
git rebase --continue
# 下一个冲突在提交 D

# 解决第二个冲突...

解决策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 接受对方的版本(main)
git checkout --theirs app.py
git add app.py
git rebase --continue

# 接受自己的版本(feature)
git checkout --ours app.py
git add app.py
git rebase --continue

# 或者手动合并
vim app.py # 手动解决
git add app.py
git rebase --continue

多人协作工作流

工作流1:Git Flow

适合有固定发布周期的项目。

1
2
3
4
5
main ──────────────────────► M1 ──────► M2 ──────►
│ │
develop ───► D1 ─► D2 ─► M1 ─┘ │
│ │
feature ──────► F1 ───────────────────────────────┘
1
2
3
4
5
6
7
8
9
# 1. 从 develop 创建 feature
git checkout -b feature/login develop

# 2. 开发完成,合并到 develop
git checkout develop
git merge --no-ff feature/login

# 3. 删除 feature
git branch -d feature/login

工作流2:Trunk-based Development

适合持续交付的团队。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 所有人在 main 上开发
git checkout main
git pull

# 创建短期 feature 分支
git checkout -b feature/quick-fix

# 频繁 rebase 到 main
git fetch origin
git rebase origin/main

# 完成后直接合并回 main
git checkout main
git merge --no-ff feature/quick-fix

工作流3:个人习惯(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 从 main 创建 feature 分支
git checkout -b feature/new-feature main

# 2. 开发过程中,定期 rebase main(保持同步)
git fetch origin
git rebase origin/main

# 3. 完成后,整理提交历史
git rebase -i origin/main

# 4. 推送(force push,因为是本地 rebase)
git push --force-with-lease # 比 --force 更安全

# 5. 创建 PR/MR

实战:修复混乱的提交历史

问题背景

1
2
3
4
5
git log --oneline
a1b2c3d 修复 bug
e5f6g7h WIP
i9j0k1l WIP
m2n3o4p 初始功能

方案1:压缩 WIP 提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git rebase -i HEAD~4

# 编辑:
pick m2n3o4p 初始功能
pick i9j0k1l WIP
pick e5f6g7h WIP
pick a1b2c3d 修复 bug

# 改为:
pick m2n3o4p 初始功能
squash i9j0k1l WIP
squash e5f6g7h WIP
squash a1b2c3d 修复 bug

# 保存退出,自动压缩

方案2:重排、编辑、删除

1
2
3
4
5
6
7
git rebase -i HEAD~4

# 编辑:
pick m2n3o4p 初始功能
pick i9j0k1l WIP # 改为 drop 删除
pick e5f6g7h WIP # 改为 squash 合并
pick a1b2c3d 修复 bug # 移到第一个

方案3:恢复误删的提交

1
2
3
4
5
6
7
8
9
10
11
# 查看 reflog
git reflog

# 输出:
a1b2c3d HEAD@{0}: rebase: 整理提交
e5f6g7h HEAD@{1}: commit: WIP
...

# 恢复到某个状态
git checkout feature-branch
git reset --hard e5f6g7h

方案4:拆分提交

1
2
3
4
5
6
7
8
9
10
11
12
git rebase -i HEAD~1

# 标记要拆分的提交为 edit
pick a1b2c3d 修复 bug

# 保存后
git reset HEAD~1
git add file1.py
git commit -m "第一部分修改"
git add file2.py
git commit -m "第二部分修改"
git rebase --continue

安全规范

1. 永远不要 rebase 公共分支

1
2
3
4
5
6
7
# 错误
git checkout main
git rebase feature # 绝对不要!

# 正确
git checkout main
git merge feature

2. 使用 –force-with-lease 代替 –force

1
2
3
4
# 更安全:检查远程是否有更新
git push --force-with-lease

# 如果远程有更新,会拒绝推送,避免覆盖他人的提交

3. 保护重要分支

在 GitHub/GitLab 设置:

1
2
3
4
5
6
7
8
9
# .github/workflows/branch-protection.yml
- name: Protect main branch
rules:
branches:
- main
required_status_checks:
strict: true
enforce_admins: true
prevent_force_push: true # 防止强制推送

4. 提交前检查

1
2
3
4
5
6
7
8
9
# .git/hooks/pre-push
#!/bin/bash

branch=$(git symbolic-ref --short HEAD)

if [ "$branch" = "main" ] || [ "$branch" = "develop" ]; then
echo "不允许直接推送公共分支,请使用 PR/MR"
exit 1
fi

总结

merge vs rebase 选择指南:

场景 推荐 原因
本地整理提交 rebase 保持历史整洁
同步 main 到 feature rebase 线性历史
合并 feature 到 main merge 保留完整历史
PR/MR merge 清晰的合并记录
公共分支 merge 不改变他人历史
紧急修复 merge 快速合并

黄金法则:

  • 本地 rebase:可以,整理提交历史
  • 已推送的分支 rebase:绝对不行
  • 公共分支:永远只 merge,不 rebase