更改 git 合并的方向(合并完成后)

Change direction of git merge (after the merge is done)

我在 git 存储库中有一个分支(例如 Feature-X)。

我所做的是以下内容

git checkout master
git merge Feature-X

我解决了很多冲突。我还没有提交更改。

但是,事实证明我想要的是进行反向合并(即将 master 合并到 Feature-x 中),因为分支可能仍然不稳定。

是否有任何命令可以挽救我在解决冲突时所做的工作,或者我是否需要再次解决,这次是在分支 Feature-X 中?

我在想是否有办法获取当前补丁,但按 "reverse" 顺序在分支 Feature-X 中应用它。例如。当补丁说 X 行更改为 Y 行时,这是相对于 Feature-X 将要掌握的。如果我想做相反的事情,补丁应该说 Y 行更改为 X,对吗?这只是我的想法。任何解决方案都可以。

你可以做几件事:

  1. 使用 git rebase -i 手动执行并手动反转提交的顺序。

  2. 编写一个脚本,它将遍历 git log --reverse 并按照您希望的顺序更新提交。

  3. 下次使用 git rebase --onto 并决定您希望从哪里开始提交

--onto <newbase>
Starting point at which to create the new commits. If the --onto option is not specified, the starting point is . May be any valid commit, and not just an existing branch name.

As a special case, you may use "A...B" as a shortcut for the merge base of A and B if there is exactly one merge base. You can leave out at most one of A and B, in which case it defaults to HEAD.

在 master 上提交合并,在该提交上创建一个新分支,并将 master 分支重置为上一个提交。

git branch temp
git reset --hard HEAD~1

(假设您的工作副本中没有其他您想要保留的内容,您会在重置时丢失它!)

您回到了开始的地方(合并前),但如果您需要,您的冲突解决方案是安全的。

现在再次尝试合并,正确的方式(master -> feature)。如果你幸运的话,这样不会有那么多冲突,你就可以完成它。

如果这对您不利,您可以将您的临时分支合并或挑选到功能中。它在图表上看起来不正确,但这就是价格!

完成后记得删除多余的分支。

TL;DR: 看到最后

最后有个"recipe";向下滚动找到它(然后向上滚动一点找到描述和解释以及更安全的方法)。

好消息和坏消息

从某种意义上说,合并并不完全一个"direction"。 (当您查看合并时 "see" 的方向是您想象力的产物,加上合并提交的消息,再加上一件更重要的事情,那就是我们的 "bad news"——但这并不是致命的,在结束。)

考虑这张以提交 * 作为合并基础的一对分支的图表:

          o--o--...--o   <-- branch1
         /
...--o--*
         \
          o--o--...--o   <-- branch2

合并("merge as a verb")branch1branch2的过程是通过:

  • 比较,即 git diff,合并基础提交 *branch1 的尖端提交;
  • 将合并基础与 branch2 的尖端进行比较;
  • 然后,从基础开始,结合两组变化。

因此实际的合并本身应该是这样的:

          o--o--...--o
         /            \
...--o--*              M
         \            /
          o--o--...--o

(我将合并提交命名为 M 因为我们不知道,或者真的不在乎,什么疯狂的 Git 哈希 ID deadbeefbadc0ffee 或其他什么会有。)最终合并提交M合并("merge as a noun");它基于 merge-as-a-verb 组合过程存储最终的源代码树。

这是哪个 "direction" 合并?好吧,这至少部分取决于我们贴在上面的标签,不是吗? :-) 请注意,我小心地去掉了 branch1branch2。让我们把它们放回去:

          o--o--...--o     <-- branch1?
         /            \
...--o--*              M   <-- branch1? branch2?
         \            /
          o--o--...--o     <-- branch2?

这可能看起来令人困惑。它可能 令人困惑。这是整个问题的重要组成部分。 坏消息是,这不是整个的事情。有些东西我们无法在这些图纸中完全捕捉到。这对您来说可能无关紧要,如果确实如此,那也没什么大不了的;我们将通过再进行一次合并提交来修复它。但现在,让我们继续前进。

进行合并提交

一旦我们自己进行合并提交M,我们必须选择——或者让Git选择——它在哪个分支上。在一个简单的、无冲突的合并中,我们最终总是 Git 选择这个。 Git 的事情很简单:无论我们在什么分支上,当我们启动git merge命令时,都是gets[=208的分支=] 合并提交。所以:

git checkout branch1
git merge branch2

表示最终图片为:

          o--o--...--o
         /            \
...--o--*              M   <-- branch1
         \            /
          o--o--...--o     <-- branch2

我们可以 re-draw 作为:

...--o--*--o--...--o--M   <-- branch1
         \           /
          o---...---o     <-- branch2

这很明显我们将 branch2 合并到 branch1,反之亦然。尽管如此,附加到提交 M源代码 并不依赖于此 "merge direction".

边栏:合并冲突

就最终结果而言,有冲突的合并与无冲突的合并一样。只是 Git 不能自己进行合并。它停止并让 you 解决冲突,然后 运行 git commit 进行合并提交。最后 git commit 步骤使合并提交 M.

新的合并提交在 当前分支 上进行,就像任何其他提交一样。这就是 Mbranch1 上的结局。请注意,合并提交的 消息 还说明了有关哪个分支名称已合并到其他分支名称中的信息 - 但是您有机会在进行提交时编辑它,因此您可以更改它以适应您可能有的任何想法。 :-)

(事实上,这与无冲突的情​​况没有什么不同:运行 git commit 都进行了最终的合并提交,并且都给了你编辑消息的机会。)

坏消息

坏消息是这些图表省略了一些可能对您重要的东西。 Git 有一个概念 first parent:M 这样的合并提交有两个 parent,所以其中一个是第一个 parent一个不是。1 第一个parent是当你进行合并时,你如何判断你在哪个分支上。

因此,虽然这些图和 merge-as-a-verb 过程 表明不完全是 "merge direction",但这个 first-parent概念证明存在。 如果您将使用这个 first-parent 概念, 您会关心它,并希望把它做好。幸运的是,有一个解决方案。 (事实上​​,有多种解决方案,但我会先显示 klunky-but-straightforward 个。)


1很明显,另一个就是第二个parent:只有两个parent。然而,Git 允许合并三个或更多 parent 的提交。您可以在需要时枚举所有 parent;但是 Git 认为 第一个 特别重要,并且有 --first-parent 各种命令的标志,以便遵循 "the original branch".


得到我们想要的合并

让我们继续进行 "wrong" 合并提交:

# git add ...    (if needed)
git commit

现在我们有:

          o--o--...--o     <-- [old branch1 tip]
         /            \
...--o--*              M   <-- branch1
         \            /
          o--o--...--o     <-- branch2

在哪里M 第一个 parent 是旧的 branch1 小费。

我们现在想要做的是 new 合并提交(具有不同的 ID),与 M相同除了:

  • branch2指向它
  • 它的第一个 parent 是 branch2
  • 的尖端
  • 它的第二个parent是branch1
  • 前一个提示

诀窍是以某种方式保存提交 M——这很简单,我们将使用一个临时名称,这样我们就不必复制 M 的哈希 ID——然后重置 branch1:

git branch temp          # or git tag temp
git reset --hard HEAD~1  # move branch1 back, dropping M

(请注意,到目前为止,这与 相同)。我们现在有了这张图,除了每个提交附加的 标签 外,它几乎没有变化:

          o--o--...--o     <-- branch1
         /            \
...--o--*              M   <-- temp
         \            /
          o--o--...--o     <-- branch2

现在再次检查 branch2 和 运行 合并;它会像以前一样因冲突而失败:

git checkout branch2
git merge branch1        # use --no-commit if Git would commit the merge

从某种意义上说,我们尚未进行的提交与合并提交 M 相同——但是一旦我们进行了提交,它的 first parent将是branch2的当前提示,其parent将是branch1的当前提示。我们只需要获得正确的 source 来完成我们将要进行的提交。

现在我们使用合并 result 我们保存的名称 temp。这假设您位于树的顶层,因此 . 命名所有内容:

git rm -rf -- .          # remove EVERYTHING
git checkout temp -- .   # get it all back from temp

我们现在正在使用之前提交的合并结果。我们甚至不需要 git add 任何东西,因为这种形式的 git checkout 会更新索引,所以:

git commit

并且我们根据需要在分支 branch2 上进行了新的合并 M2

          o--o--...--o__   <-- branch1
         /            \ \
...--o--*              M \ <-- temp
         \            /   \
          o--o--...--o-----M2   <-- branch2

现在我们可以删除临时分支或标签:

git branch -D temp   # or git tag -d temp

我们都完成了。随着临时名称的消失,我们无法再看到原始合并M

偷偷摸摸的管道方法

实际上有一个更短的方法来完成所有这些,使用 Git 的 "plumbing" 命令,但它有点棘手。一旦我们拥有了所有 git add-ed 和 运行 git commit,我们就有了正确的树,我们只有一个提交的第一个和第二个 parent ID 错误。您可能还希望编辑提交 message,这样看起来您正在将消息中的 branch1 合并到 branch2 中。那么:

# git add ...    (if / as needed, as above)
# git commit     (make the merge)
git branch -f branch2 $(git log --pretty=format:%B --no-walk HEAD |
    git commit-tree -p HEAD^2 -p HEAD^1 -F -)
git reset --hard HEAD^1

git commit-tree 步骤创建了一个新的合并提交,它是我们刚刚创建的副本的副本,但交换了两个 parent。然后我们使用 git branch -f 使 branch2 指向这个提交。 确保你在这一步得到了名字(branch2)。 HEAD^1HEAD^2 名字指的是两个(第一个和第二个) current commit的parents,当然就是我们刚刚做的merge commit。有正确的树和正确的提交消息文本;它只是有错误的第一个和第二个 parent 哈希值。

一旦我们将新提交安全地添加到 branch2,我们只需重置当前 (branch1) 分支以删除我们所做的合并提交。