引言
在我们日常团队开发过程中 git已经是我们团队开发中不可缺少的一种版本控制工具 我们在处理分支合并的时候 最常用到的命令就是 merge命令。但是除了merge命令之外 我们处理合并的命令还有rebase和cherry-pick命令 那么我们在处理合并分支的时候 什么时候用merge 什么时候用rabse或者cherry-pick呢。我们该怎么选择呢,本篇文章我来用图解来解释一下用这三种命令处理合并的情况。
场景模拟
为了更好地看出 merge
、rebase
和 cherry-pick
这三种合并方式的区别,我们以两个分支为例来模拟它们的效果。
假设我们从 master 分支创建了一个功能分支:
- feature/login:用于开发项目的登录功能模块。假如该分支是我们正在负责开发。
现在,同事 A 往 master分支中合并且提交了一个用于 校验用户信息 的功能。与此同时,我们正在 登录分支(feature/login) 中开发登录功能,而登录功能也需要用到这个 校验用户信息 的功能。
为了避免重复封装相同的功能,我们肯定是要将master这部分提交合并到我们的 登录分支 中。
接下来,我们可以使用 Git 提供的工具(例如 merge
、rebase
或 cherry-pick
)来实现该场景。
Merge合并
git merge
是 Git 中最基础的分支合并命令,能够将一个分支的改动与当前分支合并在一起。合并后会生成一个新的 合并提交(merge commit) ,这个提交将两个分支的历史连接起来。
在我们的示例中:
-
master 分支合并记录是这样的:
-
feature/login分支提交记录是这样的:
现在需要将 master 合并到 feature/login。
操作步骤
-
切换到 feature/login 分支:
bash
代码解读
复制代码
git checkout feature/login
-
执行合并命令:
bash
代码解读
复制代码
git merge masteer
-
如果有冲突,解决冲突后继续提交。
合并结果
执行merge命令后 我们再来看feature/login分支的记录:
关键点
- 历史中会出现分叉,
注册逻辑1
和注册逻辑2
提交记录也会存在,但最终通过 merge commit 连接到了 feature/login 分支。 - 合并提交记录中会注明来源分支(
Merge branch 'master' into feature/login
),清晰展示了合并过程。
这样我们就把master分支其他用户提交的 校验用户信息
记录合并到我们正在开发的feature/login分支上面了。这样我们就不用在重复这个功能了。
git merge 的优缺点
优点
git merge
不会修改原始提交历史,会保留所有的历史提交记录,这样我们能够清晰地保留每个分支的开发过程。在多人协作开发的时候尤为重要。
缺点
git merge
由于保留了分叉结构,历史记录可能显得冗长,尤其是在频繁合并的情况下。而且上面例子中我们只想要把校验用户信息
提交合并到我们分支中但是使用merge后会把其他的合并提交节点也引入进来。这样会污染我们分支的提交记录。
假设同事频繁地向 master 分支推送代码更新,比如:
- 统一升级了项目依赖库。
- 修改了项目的全局配置文件。
- 添加了一些基础工具函数。
为了确保自己的功能分支代码是基于最新的主分支,我们就需要不断从 master 拉取更新,并将其合并到当前分支。如果每次都使用 git merge
,提交历史可能会变成这样:
sql
代码解读
复制代码
Merge branch 'master' into feature/login feat: 修改配置1 Merge branch 'master' into feature/login feat: 修改配置2 Merge branch 'master' into feature/login feat: 修改配置3
在这种情况下,每次从 master 合并过来的更新都会生成一个 合并提交(merge commit) ,虽然功能分支的代码在逻辑上没有太多变化,但提交历史却充满了这些“无意义的”合并记录。这种提交记录带来了以下问题:
- 历史被污染:大量的合并提交让提交历史变得杂乱,很难快速找到真正的功能开发记录。
- 提交不清晰:实际的功能开发(如登录逻辑的提交)被频繁的合并提交分隔开,难以阅读和审查。
- 代码审查困难:当需要回溯某个功能的开发历程时,频繁的合并提交可能干扰视线。
适用场景
所以当我们需要保留分支独立开发的历史,且合并过程需要被完整记录时,我们可以使用 git merge
。或者我们在将功能分支合并到主分支(如 master
或 develop
)时,建议使用 merge
。如果是比较复杂的项目,且项目历史需要完整且清晰时,merge
是首选。
在上面如果遇到我们需要频繁合并到我们分支的场景下,使用 git rebase
会更加合适。通过 rebase
,我们可以将功能分支的提交重新整理到 master 的最新提交之上,保持提交记录的线性结构,避免生成额外的合并节点。这不仅让历史记录更简洁,还可以提高代码审查的效率。
接下来,我们将通过 git rebase 的方式演示如何避免频繁合并导致的历史复杂问题。
Rebase合并
git rebase
是一种重写提交历史的方式,它会将一个分支上的所有提交,重新应用到目标分支的最新提交之后,生成线性的提交历史,而不会生成合并提交(merge commit)。这种方式使得历史记录更加简洁清晰,避免了分叉和多余的合并节点。
我们仍以 master 和 feature/login 两个分支为例:
-
master 分支有三个同事合并过来的提交:
-
feature/login 分支有两个提交:
现在我们希望将 master 的提交整合到 feature/login 上,但使用
rebase
。
操作步骤
-
切换到 master 分支:
bash
代码解读
复制代码
git checkout master
-
执行
rebase
操作,将 master 的提交重新应用到 feature/login 的基础上:bash
代码解读
复制代码
git rebase feature/login
-
如果遇到冲突,解决冲突后继续:
bash
代码解读
复制代码
git rebase --continue
-
切换到 feature/login 分支,并合并 master(使用 fast-forward 合并):
bash
代码解读
复制代码
git checkout feature/login git merge master
合并结果
执行完上述操作后,提交历史变成线性的,类似于下面的结果:
关键点
- 所有的提交记录都按照时间顺序排列,形成一个线性历史。
- 没有额外的 merge commit,提交记录更加简洁、清晰。
rebase
通过“重新应用提交”的方式,将 master 的提交直接放到了 feature/login 提交的后面。
git rebase
的优缺点
优点
适用 git rebase
后提交记录呈线性,逻辑清晰,便于我们阅读和理解。而且没有多余的合并提交节点。在我们代码审查或追溯问题时,清晰的线性历史可以让我们能更快速地定位问题。 而且在多人协作中,当我们需要同步主分支的更新到个人开发分支时,rebase
是更优的选择,这样可以避免频繁的合并提交污染记录。
缺点
git rebase
会重写分支的提交历史,改变提交的哈希值。如果在 公共分支(如 master
或其他团队共享的分支)上使用 rebase
,或者变基后的分支再次与公共分支交互,就可能导致冲突和混乱。
假如master
分支是我们的公共分支,我们上面操作把master
变基了,然后我们在feature/login分支上面继续提交 就会被重新排列到 master 的最新提交之后,形成新的提交哈希值。
由于 rebase
修改了提交哈希值,当我们尝试将新的 feature/login 推送到远程仓库时,会发现提交与远程仓库的提交记录不一致,导致推送失败。可能 Git 提示:
vbnet
代码解读
复制代码
error: failed to push some refs to <remote-repository> hint: Updates were rejected because the tip of your current branch is behind
假如我们要强制推送的话 比如使用命令:
bash
代码解读
复制代码
git push --force
这会覆盖远程仓库的历史,导致其他同事(如A)拉取代码时产生冲突。比如同事A在拉取更新后会遇到以下提示:
go
代码解读
复制代码
error: Your branch and 'origin/master' have diverged
这具主要就是因为我们执行的 rebase
修改了历史记录,覆盖了同事A的提交。
还有一种情况就是当 master分支和我们feature/login 共同修改了一个同一个文件之后。假如这种情况下 我们feature/login 分支尝试变基到 master时,Git 会在变基过程中提示冲突:
lua
代码解读
复制代码
error: could not apply <commit-hash> feat: 在配置文件中添加用户认证字段 CONFLICT (content): Merge conflict in config.json
这样的原因是因为我们同一个文件在两个分支中被修改,但修改的内容冲突。这样就会导致我们每个提交都会暂停变基,要求我们手动解决冲突并继续,如果多个提交涉及相同的文件,每次冲突都需要单独处理。这会使整个操作变得非常繁琐。
适用场景
所有针对上面的情况 我们要避免在公共分支上使用 rebase
:(如 master
或 develop
)推荐使用 merge
,而不是 rebase
。如果需要同步主分支更新到功能分支,我们可以在功能分支中使用 rebase
,但避免在主分支上使用。当两个分支的修改点存在较多冲突时,我们可以使用 merge
一次性解决所有冲突,而不是逐个提交处理。
通过以上示例,我们看到了 rebase
的潜在风险和复杂性。在实际项目中,rebase
需要慎用,尤其是在多人协作的公共分支上。接下来,我们将探讨另一种灵活的分支整合方式:git cherry-pick,用于选择性地整合特定提交。
cherry-pick
合并
git cherry-pick
是一种选择性整合工具,它允许开发者将某个分支的指定提交(一个或多个)应用到当前分支,而无需合并整个分支的历史。被挑选的提交会在当前分支中生成新的提交,并保持当前分支的结构不变。
我们仍以 master 和 feature/login 两个分支为例:
-
master 分支有三个同事合并过来的提交:
-
feature/login 分支有两个提交:
现在,我们希望将 master 中的某些功能(例如 校验用户信息
)整合到 feature/login 中,但不需要整合整个分支的历史记录。
操作步骤
-
确认需要的提交哈希值:
bash
代码解读
复制代码
git log master
假设需要提取的提交哈希值为
<commit-hash-3>
。 -
切换到目标分支(
feature/login
):bash
代码解读
复制代码
git checkout feature/login
-
使用
cherry-pick
挑选提交:bash
代码解读
复制代码
git cherry-pick <commit-hash-3>
-
如果需要多个提交,可以用空格分隔提交哈希值:
bash
代码解读
复制代码
git cherry-pick <commit-hash-3> <commit-hash-2>
-
如果出现冲突,解决冲突后继续:
bash
代码解读
复制代码
git add <file> git cherry-pick --continue
合并结果
执行 git cherry-pick
后,feature/login
分支的提交历史如下(不要纠结commit记录值 这里只是一个示例):
关键点
校验用户信息
被成功引入到 feature/login。- 提交记录保持了原始的提交信息(提交内容和描述),但生成了一个新的提交哈希值。
git cherry-pick
的优缺点
优点
通过上面示例我们可以看到 使用git cherry-pick
命令我们能够选择性地引入特定提交,而不需要合并整个分支。只需要把 校验用户信息
这个我们需要的提交记录合并过来就满足我们的要求了。比较适合快速引入小功能、修复或单独的变更。 而且不会像 rebase
那样重写历史,也不会像 merge
那样引入额外的合并提交节点。我们只需要提交哈希值即可提取所需改动,操作比较直观。
缺点
同样的 我们使用git cherry-pick
命令后导致了上下文丢失,被引入的提交可能依赖于其他提交或上下文环境,若上下文不完整,可能导致我们功能不完整或冲突。 这个需要我们手动判断和验证所挑选的提交是否独立。而且如果提交过多或依赖复杂,手动挑选提交可能会遗漏重要变更,造成问题。 当我们需要引入多个提交时,逐个挑选可能比较耗时,特别是提交之间有依赖关系的情况下。
适用场景
比如我们在生产环境中发现问题,就可以从其他分支中提取修复提交,而无需等待整条分支的整合。而且就像上面的场景中 我们可以直接引入到其他分支,而不依赖其他改动。特别是当某个分支的功能需要在多个分支中复用时,cherry-pick
是一种高效的方式。
Git 合并方式对比表
方式 | 历史结构 | 特点 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|---|
git merge | 分叉 + 合并节点 | 将两个分支的所有改动合并到当前分支,并生成一个合并提交(merge commit)。 | 1. 保留完整历史,记录分支来源。 2. 操作简单,无需修改已有提交历史。 | 1. 提交历史包含分叉,可能显得复杂。 2. 多次合并会引入大量合并提交,污染记录。 | 1. 团队协作时保留分支的独立开发历史。 2. 主分支(如 main 或 develop )整合功能分支时推荐使用。 |
git merge --squash | 单一合并提交 | 将合并分支的所有提交压缩为一个提交,应用到当前分支。 | 1. 提交历史简洁,便于审查和管理。 2. 无需记录分支开发过程时非常高效。 | 1. 丢失分支的提交细节,无法追踪原始开发历史。 2. 冲突解决与普通 merge 一样复杂。 | 1. 小型功能分支合并到主分支,且不需要保留详细提交记录时。 |
git rebase | 线性历史 | 将分支的所有提交重新应用到目标分支的最新提交之后,生成线性历史。 | 1. 提交历史清晰简洁,无分叉。 2. 适合个人开发或整理历史。 | 1. 重写历史,可能导致公共分支冲突和混乱。 2. 冲突处理逐一提交,操作复杂且容易出错。 | 1. 个人分支的开发整理,提交到公共分支前清理历史。 2. 同步主分支的最新改动到功能分支时。 |
git cherry-pick | 原始历史不变 | 选择性地将指定提交从其他分支引入到当前分支。 | 1. 灵活性强,适合独立提交的快速引入。 2. 不影响原始分支的提交历史。 | 1. 容易丢失上下文,可能引入不完整的改动。 2. 多次操作可能繁琐,适合小范围提交。 | 1. 紧急修复生产环境问题时,快速引入修复提交。 2. 从其他分支提取独立功能或单个提交。 |
总结与选择建议
-
git merge
:- 最常用的分支合并方式,适合团队协作和保留完整开发历史的场景。
- 对于主分支整合功能分支,推荐使用
merge
。
-
git merge --squash
:- 适合小型功能分支合并到主分支时,关注结果而不关注分支的详细开发过程。
-
git rebase
:- 适合个人分支开发整理和需要简洁历史的场景,但需慎用在公共分支上。
-
git cherry-pick
:- 用于灵活快速引入独立提交,适合小范围的修复或功能引入。
根据实际开发需求和团队协作方式,选择最适合的合并工具,这样我们既能保持历史记录的可读性,也能提高开发效率。