写代码的时候,你是不是也经常遇到这种情况:自己拉了个新分支 feature-A
,吭哧吭哧写了好几天功能,终于搞定了!这时候,你想把代码合并回主开发分支(比如 develop
或者 main
),一看 git log
,哇塞,图形化界面里那叫一个“盘根错节”,各种合并线交叉在一起,像蜘蛛网一样,看得人眼花缭乱。尤其是团队协作时,别人的提交、自己的提交、合并的提交混在一起,想找个特定功能的代码变更,简直是大海捞针。
这时候,你可能习惯性地就用了 git merge
,但看着那越来越复杂的提交历史,心里是不是也有点犯嘀咕:有没有更“优雅”的方式,让我们的提交历史看起来更清爽、更线性一点呢?
别急,今天老码就带你深入聊聊 Git 里的两大合并神器:
git merge
和 git rebase
。搞懂了它们的原理和使用场景,你就能轻松应对各种代码合并需求,还能让你的 Git 提交历史变得“赏心悦目”!
先说说大家的老朋友:git merge
git merge
应该是大家最常用的合并命令了。它的工作方式非常“实诚”。
想象一下,你的项目主分支 main
像一条主干道,你为了开发新功能 feature-X
,从 main
某个点拉了一条岔路出来。你在岔路 feature-X
上修修改改(进行 commits),同时主干道 main
可能也有其他小伙伴在继续往前走(也有新的 commits)。
当你完成了 feature-X
,想把它合并回 main
时,执行 git checkout main
然后 git merge feature-X
。Git 会做两件事:
- 找到
main
和feature-X
的共同“祖先”提交点。 - 将
main
当前最新的提交 和feature-X
最新的提交 进行合并,并 创建一个全新的“合并提交” (merge commit)。这个合并提交很特殊,它有两个父提交,分别指向main
和feature-X
的最新提交点。
优点:
- 保留完整的历史记录:它忠实地记录了代码的每一次分支和合并动作,你可以清晰地看到某个功能分支是何时、从哪个点分离出去,又在何时合并回来的。对于需要严格审计代码演变过程的场景,这很有用。
- 简单直观:操作相对简单,不易出错,是 Git 的默认合并策略。
缺点:
- 复杂的提交历史:每次合并都会产生一个额外的 merge commit。如果分支多、合并频繁,
git log
图形看起来就会非常杂乱,像前面说的“蜘蛛网”,可读性变差。
一张图看懂 git merge
:
graph LR
A[C1] --> B(C2);
A --> C(C3');
C --> D(C4');
B --> E{C5 - Merge};
D --> E;
subgraph main
A
B
E
end
subgraph feature-X
C
D
end
style E fill:#f9f,stroke:#333,stroke-width:2px
看到了吗?那个 C5 - Merge
就是 git merge
产生的合并提交,它把 main
分支(C2)和 feature-X
分支(C4')连接到了一起。
再聊聊让历史变整洁的“魔法师”:git rebase
git rebase
,中文可以理解为“变基”。这个操作就有点“骚气”了,它能让你的提交历史看起来非常“干净”,像一条直线。
还是上面的场景,你在 feature-X
分支开发,main
分支也在前进。当你想把 feature-X
的改动合入 main
时,你可以先切换到 feature-X
分支 (git checkout feature-X
),然后执行 git rebase main
。
这时候 Git 会干一件很“魔法”的事情:
- 它会先找到
feature-X
和main
的共同祖先。 - 然后,它把你
feature-X
分支上 独有的 提交(也就是从共同祖先之后的所有提交),一个一个地“摘”下来,暂时存起来。 - 接着,它把
feature-X
分支的起点,“移动”到main
分支当前最新的提交点后面。 - 最后,把刚才“摘”下来的那些
feature-X
的提交,按照原来的顺序,重新一个个地应用(或者叫“回放”) 在新的起点上。
关键点: rebase
实际上是在 重写历史。那些被重新应用的提交,虽然内容和原来的差不多(或者解决了冲突后内容有变),但它们的 Commit ID 会改变!因为 Commit ID 是根据提交内容、父提交 ID、作者、时间等信息生成的,父提交变了,ID 自然就变了。
优点:
- 线性的提交历史:
rebase
后的提交历史非常清爽,就像feature-X
的开发一直都是基于最新的main
分支进行的一样,没有分叉和合并的痕迹。这让git log
非常易读,便于追踪代码变更。 - 减少无意义的合并提交:避免了
merge
操作产生的那个额外的 merge commit。
缺点:
- 重写了历史:这是
rebase
最大的特点,也是最大的风险点。如果你的分支已经被其他人拉取并基于它开发,你再进行rebase
并强制推送 (git push -f
),会给其他人造成巨大的麻烦(他们的本地仓库和远程仓库历史对不上了)。 - 冲突处理可能更繁琐:如果在 rebase 过程中遇到冲突,你需要 依次解决每个提交 应用时产生的冲突,而不是像
merge
那样一次性解决所有冲突。
一张图看懂 git rebase
:
假设 feature-X
有 C3' 和 C4' 两个提交,main
在此期间更新到了 C2。
Rebase 前:
graph LR
A[C1] --> B(C2);
A --> C(C3');
C --> D(C4');
subgraph main
A
B
end
subgraph feature-X
C
D
end
执行 git rebase main
后 (在 feature-X 分支上):
graph LR
A[C1] --> B(C2);
B --> C_new(C3'');
C_new --> D_new(C4'');
subgraph main
A
B
end
subgraph feature-X-rebased
C_new
D_new
end
注意看,feature-X
分支现在的起点接在了 main
的最新提交 C2 后面,并且原来的 C3' 和 C4' 变成了新的提交 C3'' 和 C4''(它们的 Commit ID 不同了)。历史变成了一条直线!
merge
vs rebase
:一张表格看懂差异与选择
特性 | git merge | git rebase |
---|---|---|
提交历史 | 非线性,保留分支和合并的完整轨迹 | 线性,看起来更简洁,如同串行开发 |
历史保真性 | 高,忠实记录实际发生的操作 | 低,修改了提交发生的“基础”和Commit ID |
合并提交 | 会产生一个额外的 merge commit | 通常不会产生(除非是 rebase 一个 merge commit) |
冲突处理 | 在最终 merge 时一次性解决所有冲突 | 在 rebase 过程中,可能需要逐个解决每个提交的冲突 |
团队协作 | 对公共分支(如 main/develop)安全 | 严禁在已共享、多人协作的分支上使用 |
易用性 | 概念和操作相对简单 | 概念稍复杂,误操作风险(尤其push -f )更高 |
什么时候用 merge
?什么时候用 rebase
?
看了这么多,到底该怎么选呢?其实没有绝对的对错,关键看场景和团队规范。老码给你几条实战建议:
-
合并到公共分支时,通常推荐用
merge
- 像
main
、develop
、release
这些团队成员都会拉取和依赖的分支,最好使用merge
。这样可以保留所有开发的真实历史记录,避免因为某人rebase
了公共分支导致其他人的仓库“炸锅”。想象一下,你刚pull
了最新的develop
,结果张三rebase
了一下develop
并强制推送了,你下次pull
的时候 Git 就会告诉你历史不一致,那感觉,酸爽!
- 像
-
保持个人开发分支(未共享)的整洁,推荐用
rebase
- 当你自己在本地
feature
分支开发时,发现远端的develop
分支更新了好多,为了避免将来合并时产生一个巨大的 merge commit 和复杂的历史,可以在 提交你的代码之前,先git pull
(或者git fetch
+git rebase origin/develop
) 更新本地的develop
分支,然后切回你的feature
分支,执行git rebase develop
。 - 这样,你的
feature
分支上的提交就会“变基”到最新的develop
分支上,历史看起来就像你始终基于最新的代码开发的。等你的功能开发测试完毕,再切换到develop
分支,执行git merge feature
(这时通常会是 Fast-forward 合并,不会产生 merge commit,即使不是 Fast-forward,产生的 merge commit 也相对简单)。
- 当你自己在本地
-
更新本地分支与远程同步时,
pull --rebase
是个好选择- 很多人习惯用
git pull
来更新代码,它默认等于git fetch
+git merge FETCH_HEAD
。这可能会在你的本地分支产生一些不必要的 merge commit。 - 如果你希望本地提交始终保持在远程最新提交的“顶端”,形成一条直线,可以使用
git pull --rebase
。它相当于git fetch
+git rebase FETCH_HEAD
。当然,前提是你本地的提交还没有 push 到远程共享分支。
- 很多人习惯用
-
整理本地多次凌乱提交,
rebase -i
是神器- 在你的
feature
分支上,可能为了保存进度,commit 了很多次,比如 "fix typo"、"add debug log"、"revert debug log" 等。在准备合并回develop
之前,这些琐碎的提交会让历史很难看。 - 可以使用交互式 rebase (
git rebase -i
),比如git rebase -i HEAD~5
(表示你要整理最近 5 个提交)。它会打开一个编辑器,让你对这些提交进行编辑,比如:squash
: 合并多个提交为一个。fixup
: 类似squash
,但丢弃被合并提交的 commit message。edit
: 修改某个提交的内容或 message。reword
: 只修改 commit message。drop
: 删除某个提交。- 调整提交的顺序。
- 通过
rebase -i
,你可以把一堆零散的本地提交整理成几个逻辑清晰、有意义的 commit,然后再去合并,代码评审(Code Review)的同事会感谢你的!
- 在你的
Rebase 的黄金法则:永远不要 Rebase 共享的分支!
这条必须再三强调!不要对已经被推送到远程,并且可能有其他团队成员已经拉取或正在基于其工作的分支(比如 main
, develop
, 或者团队共享的 feature
分支)执行 rebase
操作,特别是之后需要 git push -f
(强制推送) 的情况。
一旦你这么做了,就改变了共享分支的历史。其他拉取了旧历史的成员,他们的本地仓库和远程仓库就产生了分歧。他们再 pull
代码时会遇到严重问题,需要复杂的操作来修复,甚至可能丢失代码。这绝对是团队协作中的大忌!
记住:只在你自己本地的、还未与他人共享的分支上进行 rebase
操作,或者在团队约定允许的情况下对特定分支 rebase(比如个人 feature 分支准备合入 develop 前)。
实战干货:Rebase 冲突处理
如果在 rebase
过程中遇到冲突,Git 会停下来,提示你哪个文件有冲突。你需要:
- 手动解决冲突的文件(和
merge
冲突一样,编辑文件,保留需要的代码,删除冲突标记符如<<<<<<<
,=======
,>>>>>>>
)。 - 使用
git add <resolved_file>
将解决后的文件标记为已解决。 - 注意:不需要
git commit
! - 执行
git rebase --continue
,让 Git 继续应用下一个提交。 - 如果中途想放弃这次 rebase,回到 rebase 开始前的状态,可以执行
git rebase --abort
。
好了,关于 git merge
和 git rebase
的选择题,现在你应该心里有谱了吧?它们就像工具箱里的锤子和螺丝刀,各有用途,关键在于理解它们的原理和适用场景,然后根据实际情况和团队约定做出明智的选择。用好了它们,不仅能让你的 Git 仓库管理得井井有条,也能提升团队协作的效率哦!
我是老码小张,一个喜欢钻研技术背后原理,在实战中不断踩坑、不断成长的程序员。希望今天的分享对你有帮助,也欢迎大家在评论区一起交流讨论,分享你在使用 Git 中的心得体会或者遇到的“坑”!下次再见!