Skip to content
This page has been auto-translated and may contain errors.View in English

合并分支和解决冲突

docs.scrimba.com

你的功能分支已完成。开关按钮可以工作,你已提交了更改,现在你想让这些工作进入 main 分支,那是团队其他代码所在的地方。大多数时候,将其合并进来很快且顺利:Git 比较两个分支,发现没有重叠的部分,然后顺利地合并工作,无需额外步骤。但有时,你和队友在不同的分支上改动了完全相同的几行代码,Git 无法猜测你想保留哪个版本。这种情况在几乎每个有多人提交代码的项目中都会出现。这是分支工作中正常、日常的一部分,它意味着 Git 发现了同一行代码的两个想法,需要你决定哪一个获胜。

使用 git merge 合并分支

假设你在自己的分支 add-fahrenheit-toggle 上构建了华氏度开关,而 main 自你分支出来后就没有改动过。切换到 main 并运行 git merge,后面跟上你想合并的分支名称:

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Updating 4f2a891..8c3d1a0
Fast-forward
 src/index.js | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

git merge <branch> 总是将指定的分支合并到你当前所在的分支中,所以首先切换到 main 很重要。你所在的分支就是接收合并的那个分支。运行完毕后,main 拥有 add-fahrenheit-toggle 的每一个提交。该功能分支本身保持不变:你可以继续在上面工作,或者现在删除它,因为它的工作已经在 main 上了。

输出中的 Fast-forward 这一行值得仔细阅读。它表示自你分支出来后 main 没有获得任何新提交,所以 Git 不需要合并任何东西:它只是将 main 标签向前移动,指向 add-fahrenheit-toggle 已经指向的同一个提交。快进合并 是 Git 将标签追上到它本应指向的位置,仅此而已。

如果 main 在你工作期间获得了其他提交,比如队友在这期间合并了一个错误修复,Git 就不能再向前移动标签了,因为 main 和你的分支自分叉以来已经朝不同方向发展。相反,它会创建一个新的提交来连接两个历史,称为 合并提交

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Merge made by the 'ort' strategy.
 src/index.js | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

之后用 git log --graph --oneline 显示合并的结果:两条历史线并行运行,在合并提交处重新汇合。这两种结果都不会对你有额外的要求。Git 根据分支是否发散来决定应用哪一种,无论哪种方式,你的功能现在都是 main 的一部分了。

上面的两种结果都基于同一个底层思想:三路合并。为了合并两个分支,Git 比较的不仅仅是它们的两个末端。它首先找到两个分支共享的作为公共起点的提交(合并基)。然后查看三个快照:基提交,以及每个分支的末端。将每个末端与共享的基进行比较,让 Git 确切地知道每一侧改动了哪些行,这正是它能够自动合并两组编辑的原因,而不是每次都问你。

快进是三个快照之一变得冗余的情况:基和其中一个末端是同一个提交,所以那一侧没有什么要合并的,Git 移动标签而不创建新东西。合并提交是通常的情况,两侧自基以来都改动了什么,Git 记录一个有两个父提交的新快照,一个指向每个分支的历史。下一节中你遇到的每个合并冲突都来自这个三路比较,发现两侧改动了相同的行,没有办法自动合并它们。

Juno使用 git merge 合并分支git merge branch-name 将该分支的提交合并到你当前所在的分支,所以先切换到 main。大多数合并在后台悄悄进行,你继续你的工作。功能分支本身不会改变;一旦 main 有了这项工作,你可以继续使用它或删除它。
Juno使用 git merge 合并分支 快进意味着 main 没有移动,所以 Git 只是向前滑动了标签。一旦 main 在这期间获得其他提交,合并就会创建一个真实的有两个父提交的合并提交。无论哪种方式,命令都是一样的 git merge branch-name;Git 决定哪种合并方式适合。
Juno使用 git merge 合并分支 三路合并将共享的基提交与每个分支末端进行比较,以确定每一侧改动了什么,这正是大多数时候合并两个分支是自动的原因。快进是基和其中一个末端已经匹配的特殊情况。在一个复杂的图上,我仍然会停下来思考哪个提交算作基,所以如果你不确定,就画出来。

合并冲突看起来什么样

当同一行代码在合并的两侧都改动了,而 Git 无法判断你想要哪个版本时,就会发生冲突。假设队友合并了一个加载微调器到 main,改动了 src/index.js 中的一个函数,而你的 add-fahrenheit-toggle 分支改动了同一个函数来添加开关。现在合并会在中途停下来,而不是完成:

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Auto-merging src/index.js
CONFLICT (content): Merge conflict in src/index.js
Automatic merge failed; fix conflicts and then commit the result.

这就是 合并冲突。打开文件,你会发现 Git 标记了两个版本不同的确切位置:

<<<<<<< HEAD
  showLoadingSpinner(true);
=======
  const tempF = celsiusToFahrenheit(tempC);
>>>>>>> add-fahrenheit-toggle

<<<<<<< HEAD 标记了你当前分支(本例中是 main)上的内容的开始。======= 分隔两个版本。>>>>>>> add-fahrenheit-toggle 标记了传入分支版本的结束。标记之间的所有内容是同一几行代码,用两种不同的方式编写。

冲突意味着 Git 需要你的判断;没有什么是坏的。Git 发现了对同一行的两个改动,无法猜测你想要哪一个,所以它暂停并要求你决定。编辑文件直到它按你想要的方式读取,保留任一版本、两者,或结合它们的新内容,然后删除标记行本身:

  showLoadingSpinner(true);
  const tempF = celsiusToFahrenheit(tempC);

文件看起来正确后,暂存并提交它,就像任何其他改动一样:

bash
$ git add src/index.js
$ git commit

这里不带消息运行 git commit 会打开你的编辑器,Git 已经准备好了描述合并的消息。保存并关闭它,合并就完成了。

当冲突打开时,git status 是首先值得检查的。它在"未合并路径"下列出每个仍需要注意的文件,所以在有多个冲突文件的较大合并中,你确切知道还有多少文件要修复:

bash
$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   src/index.js

git add 告诉 Git 一个文件的冲突已解决。它不检查你是否真的删除了标记,所以在暂存文件前读一遍。如果合并看起来比你预期的更复杂,或者你选了错误的分支,git merge --abort 会完全退出并将你的工作目录返回到运行 git merge 之前的样子,没有什么半完成的留下。

冲突不是 git merge 独有的。变基 是另一种方式来使一条历史线保持与另一条同步,它会碰到同样类型的冲突,但解决后的结果不同。合并用一个有两个父提交的合并提交将两条历史连接起来,保留分支曾经存在过以及它何时重新加入的事实。变基相反,它一次一个地重放你的分支提交到另一个分支的末端上,产生一条笔直的历史线,没有合并提交,也没有两者曾经分叉过的痕迹:

bash
$ git switch add-fahrenheit-toggle
$ git rebase main

权衡是真实的。变基产生的线性历史在 git log 中读得很干净,一个提交接一个,一些团队更喜欢一个整洁的故事。合并保留了真实发生的事情的形状,包括功能分支重新加入 main 的确切时刻,一些团队更喜欢准确性。两个选择都没有错;为团队选择一个约定并保持一致。

无论你选择哪个约定,一条规则总是成立的:永远不要变基其他人已经拉取或在其上构建工作的分支。变基将提交重写为有新 ID 的新提交,如果共享分支的历史在协作者身下改变,他们的本地副本和重写的远程副本不再一致,强制他们进行手动修复。在任何人拉取之前,自由地在你自己的分支上变基。一旦它被共享,就使用合并。

Juno合并冲突看起来什么样 合并冲突意味着两个分支改动了相同的行,Git 需要你选择结果。打开文件,查找 <<<<<<<=======>>>>>>>,编辑直到它按你想要的方式读取,然后删除这些标记行。用 git addgit commit 完成。我的第一个冲突感觉像一场紧急情况;结果是 Git 问我一个相当普通的问题。
Juno合并冲突看起来什么样git status 在未合并路径下列出每个未解决的文件,当冲突涉及多个时很方便。git add 标记文件已解决而不检查你的工作,所以在暂存前读一遍。如果合并出了问题,git merge --abort 让你回到干净的状态,没有什么半完成的。
Juno合并冲突看起来什么样 变基遇到与合并相同的冲突;它重放提交到一个新的基而不是连接历史,用一条笔直的线换取保留的分支形状。将这个权衡保持在只有你在使用的分支上。一旦分支被共享,用变基重写它的提交对任何已经拉取它的人都会很困难,所以改用合并。

重新联系起来

合并使分支值得做:你在自己的历史线上安全地进行实验,在分支中覆盖,然后一旦准备好就将该工作折叠回 main,冲突和所有。在 GitHub 上,同样的操作通常通过拉取请求而不是你机器上的本地 git merge 进行;拉取请求流章节覆盖了在共享项目上提议、审查和合并一个改动。如果合并在你已经提交后落地在你没有意图的地方,撤销事情覆盖了如何安全地退出。