
如果你在团队协作开发中遇到过这些情况提交 PR/MR 后被要求“请先 rebase 一下主分支”或者看到合并按钮旁有“Merge”和“Rebase and Merge”两个选项却不知如何选择甚至因为错误的合并操作导致提交历史混乱、引入冲突或丢失代码那么这篇文章就是为你准备的。这不仅仅是关于git merge和git rebase两个命令的简单对比。其核心在于在代码审查Code Review这个关键的质量关卡前如何通过一次正确的“整理”操作来提升代码集成的安全性、可维护性以及团队协作的效率。很多人只是机械地执行命令却不理解背后的“为什么”这往往为项目埋下了隐患。本文将深入探讨在 GitHub Pull Request (PR) 或 GitLab Merge Request (MR) 合并前进行 Merge 或 Rebase 操作的根本原因、适用场景、具体操作步骤以及必须避开的“坑”。你会明白为什么直接合并可能不是最佳选择看似简单的操作如何让提交历史变成一团乱麻。Merge 与 Rebase 的本质区别不仅仅是历史线是“直线”还是“折线”那么简单。如何根据团队规范和项目阶段做出正确选择没有银弹只有最适合当前场景的策略。从操作到验证的完整流程提供可复现的命令和配置确保你动手后能达到预期效果。1. 混乱的提交历史一个被忽视的工程问题在深入命令之前我们先看一个典型的、由不当合并操作引发的“事故现场”。假设你和小王在同一个功能分支feature/login上开发。分支是从main的A提交点拉取的。你先后提交了B1和B2小王提交了C1。与此同时main分支由其他同事向前推进了两次产生了提交M1和M2。此时分支情况如下main: A --- M1 --- M2 \ feature/login: B1 --- B2 --- C1糟糕做法直接合并如果你直接在本地执行git merge main将main的新内容合并到你的特性分支历史会变成这样feature/login: A --- B1 --- B2 --- C1 --- M1 --- M2这看起来没问题但当你完成开发将feature/login推送到远程并创建 PR/MR 时问题来了。这个 PR 会包含B1, B2, C1, M1, M2所有这些提交。而M1和M2是已经存在于main分支的、与你功能无关的提交。这会导致代码审查噪音审查者需要在你真正的功能代码B, C和无关的主干更新M之间来回切换分散注意力。历史污染合并后main分支的历史线会出现“分叉又合并”的冗余节点使得git log --oneline --graph的输出像一团纠缠的毛线难以追溯某个功能完整的引入过程。潜在冲突掩盖如果M1、M2与你的修改有冲突在这次合并中解决了但解决过程可能不完美问题被隐藏在了这次本地合并提交中而非在清晰的、针对主干更新的整合提交中暴露。问题的核心你的特性分支已经“落后”于目标分支main。在合并前你需要将目标分支的最新变更“整合”到你的分支中并确保你的功能在这些新变更上依然能正常工作。Merge和Rebase就是两种不同的“整合”策略它们塑造了最终项目历史的不同面貌和协作流程。2. 核心概念Merge 与 Rebase 的本质区别理解区别不能只记命令要从 Git 如何存储代码变更这个根本模型入手。2.1 Git 提交模型与分支指针Git 的每次提交Commit都是一个完整的快照并指向其父提交形成一条链。分支Branch本质上只是一个指向某个提交的轻量级指针。main指针指向链的末端。当你创建feature分支时只是创建了一个新的指针指向当前main指向的同一个提交。2.2 Merge合并创建新的交汇点通俗解释git merge好比说“我知道主干道main已经往前修了新的路段M1, M2。我现在从我这条辅路feature的尽头直接修一条新的连接道通到主干道最新的位置。这样从主干道来看我的所有工作B1, B2, C1是一次性送达的。”操作在feature分支上执行git merge main。结果Git 会尝试自动合并代码。如果成功它会创建一个新的“合并提交”。这个提交有两个父提交一个是feature分支原来的最新提交C1另一个是main分支的最新提交M2。历史图示会产生一个分叉与交汇的节点。A --- M1 --- M2 \ \ B1---B2---C1--- Merge Commit (有两个父提交C1 和 M2)特点保留了分支开发的原始上下文和时序历史是真实的记录。但如果分支多且合并频繁历史图会变得复杂。2.3 Rebase变基重整队伍再出发通俗解释git rebase好比说“我发现主干道已经往前修了M1, M2。我决定把我这条辅路上已经铺好的砖B1, B2, C1全部拆下来然后跑到主干道最新的位置M2后面按照原来的顺序重新铺一遍。这样我的工作看起来就像是直接在最新主干道上完成的。”操作在feature分支上执行git rebase main。结果Git 会暂时保存你分支上的所有变更B1, B2, C1然后将feature分支的基点从A移动到M2最后再将保存的变更依次应用到新的基点上生成全新的提交B1‘, B2‘, C1‘。这些新提交的哈希值会改变。历史图示历史变成一条直线。A --- M1 --- M2 --- B1‘ --- B2‘ --- C1‘特点历史清晰、线性便于二分查找git bisect和阅读。但重写了历史如果分支已共享推送到远程强制推送git push -f会破坏其他协作者的工作需谨慎。2.4 对比表格特性MergeRebase历史记录保留分支结构和合并上下文历史真实。重写历史形成线性序列历史整洁。提交哈希原有提交哈希不变新增一个合并提交。生成全新的提交哈希值全部改变。适用阶段分支开发中或结束时尤其适用于已共享的分支。最佳时机是在本地分支、未共享时准备集成前。风险历史图可能复杂。重写历史对已推送的分支强制推送会引发协作灾难。核心思想“合并”两个分支的工作承认它们是并行发展的。“整理”我的工作使其看起来是基于最新代码完成的。3. 环境准备与操作前置认知在动手前请确保你理解以下前提它们能帮你避免大多数低级错误。3.1 理解你的 Git 工作流你的团队使用的是GitHub Flow功能分支直接合并到 main、GitLab Flow带环境分支、还是Git Flow有 develop、release 分支这会影响你的目标合并分支。本文以最通用的GitHub Flow目标分支为main为例原理相通。3.2 黄金法则Rebase 的禁区绝对不要在已经推送到远程仓库、并且可能有其他人基于其进行工作的分支上执行git rebase。如果你在共享分支上 rebase 并强制推送你的同事下次拉取代码时会陷入“地狱般的”合并冲突因为你们的历史已经分叉。解决这种冲突非常棘手。3.3 操作前备份创建备份分支在进行任何合并或变基操作前尤其是 rebase创建一个备份分支是成本最低的保险。# 假设当前在 feature/login 分支 git checkout -b feature/login-backup # 现在你可以放心地回到原分支进行操作了 git checkout feature/login如果操作失误你可以轻松地git reset --hard feature/login-backup回退到操作前的状态。4. 核心流程拆解PR/MR 合并前的标准操作无论是 Merge 还是 Rebase在创建 PR/MR 前都遵循一个核心目标将目标分支如main的最新变更同步到你的特性分支并在本地解决所有冲突确保你的功能在最新代码基础上测试通过。4.1 流程概览获取最新代码从远程仓库拉取目标分支的最新提交。同步到特性分支使用 Merge 或 Rebase 策略将目标分支的变更整合进来。解决冲突在本地解决所有合并冲突这是保证 PR/MR 可合并的关键。测试验证运行测试套件确保功能正常。推送更新将处理后的特性分支推送到远程PR/MR 会自动更新。4.2 方法一使用 Merge 进行同步保守稳健这种方法在特性分支上创建一个合并提交包含目标分支的更新。它安全适合任何阶段的分支。操作步骤确保你在特性分支上。git checkout feature/login获取远程所有分支的最新信息。git fetch origin将origin/main分支的更改合并到当前分支。git merge origin/main如果自动合并成功Git 会创建一个合并提交你需要编辑提交信息通常默认即可。如果发生冲突Git 会暂停并在文件中标记冲突。你需要手动解决它们。解决冲突如果发生使用git status查看冲突文件。编辑这些文件解决冲突删除,,标记保留正确的代码。将解决后的文件标记为已解决git add 冲突文件路径完成合并提交git commit推送到远程仓库更新你的 PR/MR。git push origin feature/login结果你的特性分支历史中多了一个合并提交它包含了main的更新。你的 PR/MR 现在基于一个包含了main最新状态的分支。4.3 方法二使用 Rebase 进行同步追求整洁这种方法重写你的提交历史使其看起来像是直接在最新的main分支上进行的。务必确保此分支未与他人共享或已与协作者充分沟通。操作步骤确保你在特性分支上。git checkout feature/login获取远程main分支的最新信息。git fetch origin main执行变基操作。git rebase origin/mainGit 会依次将你的每个提交应用到origin/main之后。如果在应用某个提交时发生冲突rebase 会暂停。解决冲突如果发生解决冲突的方式与 merge 相同编辑文件删除冲突标记。但处理方式不同将解决后的文件加入暂存区然后继续变基过程git add 冲突文件路径 git rebase --continue如果你想跳过当前这个引起冲突的提交谨慎使用可以用git rebase --skip。如果想完全中止 rebase回到操作前的状态git rebase --abort。变基完成后你的本地feature/login分支已经指向了新的提交序列。由于历史被重写你需要强制推送到远程。git push origin feature/login --force-with-lease重要使用--force-with-lease比-f更安全它会在强制推送前检查远程分支是否已被其他人更新避免覆盖他人工作。结果你的特性分支历史变成了一条干净的直线所有提交都排在main分支最新提交之后。PR/MR 的提交列表变得非常清晰。5. 平台操作GitHub 与 GitLab 的合并选项当你的 PR/MR 通过审查准备合并到目标分支时平台通常会提供几种合并方式理解它们与你本地操作的关系至关重要。5.1 GitHub 的合并选项在 PR 页面点击 “Merge pull request” 时你可能看到下拉选项Create a merge commit这是标准的git merge --no-ff非快进合并。无论是否有分叉它总会创建一个合并提交。这是最常用、保留信息最全的方式。提交信息默认为 “Merge pull request #xxx from branch-name”。Squash and merge将 PR 中的所有提交压缩Squash成一个全新的提交然后合并到目标分支。这能保持目标分支历史的极度简洁但丢失了 PR 内部的详细提交历史。适合小功能或修复。Rebase and merge尝试将 PR 中的提交以 rebase 的方式应用到目标分支。这要求 PR 分支的历史已经是线性的即你已经本地 rebase 过且无冲突。如果成功目标分支历史是线性的且保留了原始提交信息。最佳实践建议对于团队协作“Create a merge commit”是默认的稳健选择。它保留了完整的协作上下文。“Squash and merge” 可用于整理琐碎的提交。“Rebase and merge” 使用前需确保分支状态符合条件。5.2 GitLab 的合并选项GitLab 的 MR 合并策略通常在项目设置中配置合并时根据设置执行Merge commit与 GitHub 的 “Create a merge commit” 相同。Merge commit with semi-linear history相当于先尝试 rebase如果成功再创建一个合并提交。这保证了目标分支历史的线性同时有一个合并节点。这是 GitLab 推荐的一种方式因为它结合了 rebase 的清晰和 merge 的上下文。Fast-forward merge仅当可以快进合并时即目标分支没有新提交或分支历史是线性的且无冲突才执行合并。不会创建合并提交。这保持了历史的线性但可能不适用于频繁协作的分支。配置路径项目 Settings - General - Merge requests - Merge method。6. 完整示例从冲突产生到解决让我们通过一个具体的代码示例完整走一遍git merge时解决冲突的流程。场景main分支有一个hello.txt文件内容为Hello, World!。你从main创建了feature/greeting分支并将文件修改为Hello, Git!并提交。与此同时另一同事在main分支上将文件修改为Hello, CSDN!并推送。初始状态与修改# 在 main 分支上初始 echo Hello, World! hello.txt git add hello.txt git commit -m Initial commit # 创建并切换到特性分支 git checkout -b feature/greeting # 在特性分支上修改 echo Hello, Git! hello.txt git add hello.txt git commit -m Change greeting to Git模拟同事更新 main 分支# 切换回 main并模拟同事的提交 git checkout main echo Hello, CSDN! hello.txt git add hello.txt git commit -m Change greeting to CSDN git push origin main # 假设有远程仓库在特性分支上尝试合并引发冲突git checkout feature/greeting git fetch origin git merge origin/main此时Git 会输出冲突信息Auto-merging hello.txt CONFLICT (content): Merge conflict in hello.txt Automatic merge failed; fix conflicts and then commit the result.查看并解决冲突运行git status会看到hello.txt处于 “Unmerged paths” 状态。用编辑器打开hello.txt内容如下 HEAD Hello, Git! Hello, CSDN! origin/main HEAD到之间是你当前分支feature/greeting的内容。到 origin/main之间是你要合并进来的分支origin/main的内容。你需要决定保留哪一个或者进行整合。假设我们整合为Hello, Git and CSDN!。编辑文件删除冲突标记只保留最终内容Hello, Git and CSDN!标记冲突已解决并完成合并git add hello.txt git commit在打开的编辑器中Git 已经生成了默认的合并提交信息Merge branch main into feature/greeting你可以修改或直接保存退出。验证结果cat hello.txt # 输出Hello, Git and CSDN! git log --oneline --graph # 你会看到一个分叉合并的历史图7. 常见问题与排查思路在 Merge 和 Rebase 操作中你会遇到一些典型问题。下表提供了快速排查指南。问题现象可能原因排查方式解决方案git merge后进入奇怪编辑状态如 vi 界面合并成功需要你输入合并提交的信息。查看命令行提示。输入:wq保存并退出vi或按提示操作。若想使用默认信息可在合并时加--no-edit参数。git rebase冲突后不知所措Rebase 是逐个提交应用冲突可能发生在中间某个提交。git status查看冲突文件。git rebase --continue提示。解决冲突 -git add-git rebase --continue。或用--skip跳过此提交用--abort完全中止。推送被拒绝[rejected]1. 你的分支落后于远程分支别人推送了。2. 你 rebase 后历史与远程不一致。git fetch origin后git log --oneline origin/你的分支..你的分支对比差异。1. 落后先git pull或pull --rebase合并远程更新。2. 历史不一致rebase导致确认可强制推送后用git push --force-with-lease。合并后历史图极其复杂“意大利面条”频繁使用git merge且未整理分支或大量使用git pull默认 merge。git log --oneline --graph --all可视化。未来考虑1. 对小功能使用squash merge。2. 在合并到主干前对特性分支进行 rebase。3. 使用git pull --rebase。git rebase后某些提交“消失”了在 rebase 交互模式-i中可能误将提交设置为drop或squash。git reflog查看操作历史找到 rebase 前的提交哈希。使用git reset --hard rebase前的哈希回退到 rebase 前的状态。操作前务必确认。PR/MR 中包含了大量无关的别人的提交创建 PR/MR 前没有将目标分支同步到特性分支。检查 PR/MR 的 “Commits” 或 “Changes” 标签页。在本地特性分支上按照第4节的方法执行git merge origin/main或git rebase origin/main解决冲突后推送。8. 最佳实践与工程建议选择 Merge 还是 Rebase没有绝对答案取决于团队规范和项目阶段。以下是综合建议团队统一规范是第一位在项目开始前团队应明确约定默认使用哪种策略处理 PR/MR 前的同步Merge or Rebase以及合并时使用哪种方式Create merge commit, Squash, etc.。并写入项目 CONTRIBUTING.md 文档。个人分支优先 Rebase如果你在一个只有你一个人工作的特性分支上开发在准备创建 PR/MR 前使用git rebase origin/main来整理历史。这能提供最清晰的提交序列方便审查。共享分支使用 Merge如果多人同时在同一个特性分支上协作绝对不要 rebase。使用git merge origin/main来同步更新避免历史重写带来的协作灾难。保持提交的原子性无论是 Merge 还是 Rebase清晰的历史都依赖于有意义的提交。每个提交应只做一件事并且有清晰的提交信息。这能让 rebase 时的冲突解决更简单也让审查更容易。利用交互式 Rebase (git rebase -i) 整理本地历史在推送到远程前你可以使用交互式变基来整理你的本地提交合并squash琐碎的提交、修改提交信息、调整提交顺序。这是一个强大的本地历史美化工具。理解git pull的默认行为git pull默认等于git fetchgit merge。如果你希望拉取远程更新时保持线性历史可以使用git pull --rebase。你可以通过配置使其成为默认行为git config --global pull.rebase true。代码审查前解决冲突务必在本地解决完所有与目标分支的冲突并通过测试后再推送更新并请求审查。将冲突解决过程留在 PR/MR 的评论中并不是好习惯它增加了审查者的认知负担。保护主分支在 GitHub/GitLab 中设置main分支为受保护分支要求 PR/MR 必须经过审查、CI 流水线通过后才能合并。这是保证代码质量的基石。掌握 Merge 和 Rebase 的精髓意味着你不仅是在使用 Git 命令更是在实践一种清晰、高效、可协作的软件开发工程文化。它让你的代码提交历史从一团乱麻变为一份有价值的项目日志让团队协作更加顺畅也让问题的追溯变得简单。下次在点击“Merge pull request”前不妨花一分钟想想哪种方式能为你的项目和团队带来更大的长期价值。