合并与变基
合并和变基都是Git 工具集中的优秀工具,尽管有时会被混淆和误用,但它们都有各自的独特用途。这种情况通常是因为团队规则最初是为了帮助和指导而制定的,但却常常妨碍了操作的简化。
人们使用 Git 的方式大多源于他们的文化和开发者经验。实际上,没有merge 和 rebase 之分,但在特定情况下,总有正确的做法。在某些情况下,人们可能会发现 rebase 更合适,而合并最终可能会更有用。
合并
来自 Git 文档:
git-merge - 将两个或多个开发历史合并在一起
合并操作的用例与“前进”的想法相关。常见目标包括:
main
使用功能(主题)分支更新分支。- 使用分支中的最新工作来更新功能(主题)分支
main
。
合并两个历史记录的最常见示例是将主题分支集成到父分支,这通常由 GitHub 等远程提供商完成:
git checkout A # Could be "main", or "master"
git merge B # Update A with B
遵循同样的原则,在恢复工作或准备向上合并之前,也可以使用其父级来更新主题分支:
git checkout B # A topic branch
git fetch remote A # Make sure to download the latest stuff
git merge remote/A # Update B with latest A
Git 也会尽力无缝合并代码,但有时它需要一些帮助将各个部分拼接在一起。这种情况被称为冲突,也令人担忧。不过,合并提交的一个好处是,无论内部提交如何,冲突都会在合并级别得到解决;而且由于合并的本质在于向前推进,一旦冲突得到解决,就不会再出现问题。
git checkout A # Could be "main", or "master"
git merge B # Update A with B
# --- conflict resolution work ---
git add conflicting-file.txt # Mark conflict as resolved
git commit # Finish merging (by creating a merge commit)
另一种非常常见的做法是使用快进策略合并分支。考虑到上面的例子,这意味着合并过程将不会生成合并提交,因为分支 B 的最左提交是在分支 B 的最右提交之后创建的。
git checkout A # Could be "main", or "master"
git merge --ff B # Update A with B with fast-forward strategy
虽然快速合并可以让合并后的历史记录看起来更清晰,但它也有一个缺点:用户永远不会知道哪些内容被合并了,因为合并的提交只会被附加到基础分支之后,而不会包含其父分支的信息。然而,有些团队并不认为这是一个缺点,反而会积极推广快速合并——随之而来的是频繁进行变基的需求。
变基
来自 Git 文档:
git-rebase - 在另一个基本提示之上重新应用提交
rebase 操作的用例与重写和清理的想法相关。常见目标包括:
main
通过重置分支main
并重放提交历史记录,使用分支中的最新工作来更新功能(主题)分支。
Rebase 在一些不太常见或高级的场景中也非常有用:
- 将提交历史移动到历史中的另一个点。
- 重写历史记录以更改(修复、压缩、重命名)以前的提交。
合并 (merge) 和变基 (rebase) 操作都可以用来更新分支,使其包含其他历史记录。变基 (rebase) 指的是从某个基点对现有历史记录进行变基。
git checkout A # Could be "main", or "master"
git merge --ff B # Hmmm no can do! B doesn't start after A
git checkout B # Go back to B
git rebase A # Reset to A then replay commits from B
git checkout A # Let's try again
git merge --ff B # Oh yeah
上图演示了 rebase 操作最常见的用例:以尽可能干净的方式,用最新的历史记录更新主题分支。不过请注意,“最干净”也意味着使用相似但不同的提交对象重写历史记录。
重新定基一个分支必然会改写它的历史。
如前所述,有些团队更喜欢清晰线性的提交历史记录,这就要求人们掌握 rebase 操作,并始终使用快进策略进行合并。每次有人需要 rebase 分支时,上面的示例都会重复执行,直到他们准备好合并为止。
git checkout B # We already know we need a rebase
git rebase A # Let's see...
# --- conflict resolution work on commit B2 ---
git add conflicting-file.txt # Looks good now
git commit --amend # Commit with the "amend" option to fixup B2
git rebase --continue # Now we go
# --- conflict resolution work on commit B3 ---
git add another-conflicting-file.txt # Ouch
git commit --amend # Fixup B3
git rebase --continue # Done
关于冲突,rebase 通常需要更多工作,因为冲突需要在提交级别解决。因此,如果在基础分支中引入了更改,则可能需要在 rebase 过程中再次解决冲突。
变基时需要考虑的另一件事是,它可能会丢弃或重写其他人推送的对象,无论是常规提交还是附带任何冲突解决方案的合并提交。强烈不建议这样做,因为它会干扰那些并非由执行变基操作的人创作的内容。一旦变基,一旦 Git 发现分支与远程副本断开连接,就应该发出警告,因此需要强制推送(git push --force
)该分支才能将其上传到远程。
git checkout B # Let's clean this up
git rebase A # Reset to A then replay commits from B
git push remote B # Upload rewritten history
# --- Branch A was updated by other people ---
git fetch remote # Make sure to download the latest stuff
git rebase remote/A # Reset to latest A then re-replay commits from B
git push remote B # Oops it doesn't work, histories differ!
git push --force remote B # Force-push branch B
考虑到历史记录,重新定基操作通常会忽略某人使用较新的历史记录更新其分支的事件,因为重放分支会留下线性提交日志。
重新定基和git pull
出于保持清晰线性历史记录的目的,强制推送分支后,Git 经验不足的用户可能会在不知不觉中使用常规 拉取 操作将分支的较新版本拉到自身之上git pull
,从而导致令人困惑且不必要的合并提交。这种情况在将点击转换为隐藏 Git 命令的图形用户界面中尤为常见。
Merge remote-tracking branch 'origin/feature-x' into feature-x
为了防止这种情况发生,git pull --rebase
会将分支重置为远程状态,然后在其上重新应用任何较新的提交——这通常是最初的意图。此行为可以在Git 配置中设置为默认行为:
git config --global pull.rebase true
我应该怎么办?
这取决于你的目标是什么,也取决于团队达成的共识。简而言之:
- 合并最适合合并历史记录,同时保持冗长和简洁。它还有助于记录冲突解决和类似“我上次从上游更新我的分支是在上周二”这样的事件。
- 当远程分支仍然未知,或者由于某种原因需要重写其历史记录时,重新定基最适合于管理任务。
一般来说,这一切都取决于团队使用 Git 的经验,以及每个人希望如何看到他们的贡献汇聚到一个共同的目标。
文章来源:https://dev.to/emyller/merge-vs-rebase-63e