Merge 还是 Rebase?告别混乱 Git 历史,选对方法很重要!

1,020 阅读10分钟

写代码的时候,你是不是也经常遇到这种情况:自己拉了个新分支 feature-A,吭哧吭哧写了好几天功能,终于搞定了!这时候,你想把代码合并回主开发分支(比如 develop 或者 main),一看 git log,哇塞,图形化界面里那叫一个“盘根错节”,各种合并线交叉在一起,像蜘蛛网一样,看得人眼花缭乱。尤其是团队协作时,别人的提交、自己的提交、合并的提交混在一起,想找个特定功能的代码变更,简直是大海捞针。

这时候,你可能习惯性地就用了 git merge,但看着那越来越复杂的提交历史,心里是不是也有点犯嘀咕:有没有更“优雅”的方式,让我们的提交历史看起来更清爽、更线性一点呢?

image.png 别急,今天老码就带你深入聊聊 Git 里的两大合并神器:git mergegit 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 会做两件事:

  1. 找到 mainfeature-X 的共同“祖先”提交点。
  2. main 当前最新的提交 和 feature-X 最新的提交 进行合并,并 创建一个全新的“合并提交” (merge commit)。这个合并提交很特殊,它有两个父提交,分别指向 mainfeature-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 会干一件很“魔法”的事情:

  1. 它会先找到 feature-Xmain 的共同祖先。
  2. 然后,它把你 feature-X 分支上 独有的 提交(也就是从共同祖先之后的所有提交),一个一个地“摘”下来,暂时存起来。
  3. 接着,它把 feature-X 分支的起点,“移动”到 main 分支当前最新的提交点后面。
  4. 最后,把刚才“摘”下来的那些 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 mergegit rebase
提交历史非线性,保留分支和合并的完整轨迹线性,看起来更简洁,如同串行开发
历史保真性高,忠实记录实际发生的操作低,修改了提交发生的“基础”和Commit ID
合并提交会产生一个额外的 merge commit通常不会产生(除非是 rebase 一个 merge commit)
冲突处理在最终 merge 时一次性解决所有冲突在 rebase 过程中,可能需要逐个解决每个提交的冲突
团队协作对公共分支(如 main/develop)安全严禁在已共享、多人协作的分支上使用
易用性概念和操作相对简单概念稍复杂,误操作风险(尤其push -f)更高

什么时候用 merge?什么时候用 rebase

看了这么多,到底该怎么选呢?其实没有绝对的对错,关键看场景和团队规范。老码给你几条实战建议:

  1. 合并到公共分支时,通常推荐用 merge

    • maindeveloprelease 这些团队成员都会拉取和依赖的分支,最好使用 merge。这样可以保留所有开发的真实历史记录,避免因为某人 rebase 了公共分支导致其他人的仓库“炸锅”。想象一下,你刚 pull 了最新的 develop,结果张三 rebase 了一下 develop 并强制推送了,你下次 pull 的时候 Git 就会告诉你历史不一致,那感觉,酸爽!
  2. 保持个人开发分支(未共享)的整洁,推荐用 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 也相对简单)。
  3. 更新本地分支与远程同步时,pull --rebase 是个好选择

    • 很多人习惯用 git pull 来更新代码,它默认等于 git fetch + git merge FETCH_HEAD。这可能会在你的本地分支产生一些不必要的 merge commit。
    • 如果你希望本地提交始终保持在远程最新提交的“顶端”,形成一条直线,可以使用 git pull --rebase。它相当于 git fetch + git rebase FETCH_HEAD。当然,前提是你本地的提交还没有 push 到远程共享分支。
  4. 整理本地多次凌乱提交,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 会停下来,提示你哪个文件有冲突。你需要:

  1. 手动解决冲突的文件(和 merge 冲突一样,编辑文件,保留需要的代码,删除冲突标记符如 <<<<<<<, =======, >>>>>>>)。
  2. 使用 git add <resolved_file> 将解决后的文件标记为已解决。
  3. 注意:不需要 git commit
  4. 执行 git rebase --continue,让 Git 继续应用下一个提交。
  5. 如果中途想放弃这次 rebase,回到 rebase 开始前的状态,可以执行 git rebase --abort

好了,关于 git mergegit rebase 的选择题,现在你应该心里有谱了吧?它们就像工具箱里的锤子和螺丝刀,各有用途,关键在于理解它们的原理和适用场景,然后根据实际情况和团队约定做出明智的选择。用好了它们,不仅能让你的 Git 仓库管理得井井有条,也能提升团队协作的效率哦!


我是老码小张,一个喜欢钻研技术背后原理,在实战中不断踩坑、不断成长的程序员。希望今天的分享对你有帮助,也欢迎大家在评论区一起交流讨论,分享你在使用 Git 中的心得体会或者遇到的“坑”!下次再见!