Git
章节 ▾ 第二版

7.8 Git 工具 - 高级合并

高级合并

在 Git 中合并通常非常简单。由于 Git 使多次合并另一个分支变得容易,这意味着您可以拥有一个非常长期的分支,但您可以随时保持其最新状态,经常解决小的冲突,而不是在系列结束时遇到一个巨大的冲突。

但是,有时确实会出现棘手的冲突。与其他一些版本控制系统不同,Git 不会尝试过度聪明地解决合并冲突。Git 的理念是在确定合并解决方法明确时变得聪明,但如果存在冲突,它不会尝试聪明地自动解决它。因此,如果您等待太久才合并两个快速分歧的分支,您可能会遇到一些问题。

在本节中,我们将介绍其中一些问题可能是什么以及 Git 提供了哪些工具来帮助您处理这些更棘手的情况。我们还将介绍一些您可以执行的不同非标准类型的合并,以及了解如何撤消已执行的合并。

合并冲突

虽然我们在 基本合并冲突 中介绍了一些关于解决合并冲突的基础知识,但对于更复杂的冲突,Git 提供了一些工具来帮助您弄清楚发生了什么以及如何更好地处理冲突。

首先,如果可能,请尝试在进行可能存在冲突的合并之前确保您的工作目录是干净的。如果您正在进行工作,请将其提交到临时分支或将其暂存。这样一来,您就可以撤消您在此处尝试的 **任何** 内容。如果您在尝试合并时在工作目录中存在未保存的更改,以下一些提示可能有助于您保留这些工作。

让我们来看一个非常简单的例子。我们有一个超级简单的 Ruby 文件,它打印“hello world”。

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

在我们的代码仓库中,我们创建一个名为whitespace的新分支,然后将所有 Unix 行结束符更改为 DOS 行结束符,从本质上讲,更改了文件的每一行,但只是更改了空格。然后我们将“hello world”这一行更改为“hello mundo”。

$ git checkout -b whitespace
Switched to a new branch 'whitespace'

$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'Convert hello.rb to DOS'
[whitespace 3270f76] Convert hello.rb to DOS
 1 file changed, 7 insertions(+), 7 deletions(-)

$ vim hello.rb
$ git diff -b
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-  puts 'hello world'
+  puts 'hello mundo'^M
 end

 hello()

$ git commit -am 'Use Spanish instead of English'
[whitespace 6d338d2] Use Spanish instead of English
 1 file changed, 1 insertion(+), 1 deletion(-)

现在我们切换回我们的master分支并为该函数添加一些文档。

$ git checkout master
Switched to branch 'master'

$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello world'
 end

$ git commit -am 'Add comment documenting the function'
[master bec6336] Add comment documenting the function
 1 file changed, 1 insertion(+)

现在我们尝试合并我们的whitespace分支,由于空格更改,我们将遇到冲突。

$ git merge whitespace
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

中止合并

现在我们有几个选择。首先,让我们介绍一下如何摆脱这种情况。如果您可能没有预料到冲突,并且不想立即处理这种情况,您可以简单地使用git merge --abort退出合并。

$ git status -sb
## master
UU hello.rb

$ git merge --abort

$ git status -sb
## master

git merge --abort选项尝试恢复到运行合并之前的状态。唯一可能无法完美执行此操作的情况是,当您运行它时,您的工作目录中是否有未暂存、未提交的更改,否则它应该可以正常工作。

如果出于某种原因您只想重新开始,您还可以运行git reset --hard HEAD,您的代码仓库将恢复到上次提交的状态。请记住,任何未提交的工作都将丢失,因此请确保您不需要任何更改。

忽略空格

在这种特定情况下,冲突与空格相关。我们知道这一点是因为这种情况很简单,但在查看冲突的实际情况时也很容易分辨出来,因为每一行都在一侧被删除并在另一侧重新添加。默认情况下,Git 将所有这些行视为已更改,因此无法合并文件。

但是,默认合并策略可以接受参数,其中一些参数与正确忽略空格更改有关。如果您发现合并中存在大量空格问题,您可以简单地中止它并再次执行,这次使用-Xignore-all-space-Xignore-space-change。第一个选项在比较行时**完全**忽略空格,第二个选项将一个或多个空格字符的序列视为等效。

$ git merge -Xignore-space-change whitespace
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

由于在这种情况下,实际的文件更改没有冲突,因此一旦我们忽略了空格更改,所有内容都将顺利合并。

如果您团队中的某个人喜欢偶尔将所有内容从空格重新格式化为制表符或反之亦然,那么这将是一个救星。

手动文件重新合并

尽管 Git 能够很好地处理空格预处理,但还有其他类型的更改,Git 可能无法自动处理,但可以通过脚本修复。例如,让我们假设 Git 无法处理空格更改,我们需要手动进行。

我们需要做的实际上是在尝试实际文件合并之前,通过dos2unix程序运行我们尝试合并的文件。那么我们该怎么做呢?

首先,我们进入合并冲突状态。然后,我们想要获取我们版本的文件副本、他们的版本(来自我们正在合并的分支)和公共版本(来自双方分支的位置)。然后我们想要修复他们的那一侧或我们的一侧,并再次尝试仅针对此单个文件进行合并。

获取这三个文件版本实际上非常容易。Git 将所有这些版本存储在索引下的“阶段”中,每个阶段都有与其关联的数字。阶段 1 是共同祖先,阶段 2 是您的版本,阶段 3 来自MERGE_HEAD,即您正在合并的版本(“他们的”)。

您可以使用git show命令和特殊语法提取每个版本的冲突文件的副本。

$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb

如果您想更深入一些,您还可以使用ls-files -u管道命令来获取每个文件的 Git blob 的实际 SHA-1。

$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1	hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2	hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3	hello.rb

:1:hello.rb只是查找该 blob SHA-1 的简写。

现在我们已经在工作目录中拥有了所有三个阶段的内容,我们可以手动修复他们的内容以解决空格问题,并使用鲜为人知的git merge-file命令重新合并文件,该命令就是这样做的。

$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...

$ git merge-file -p \
    hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb

$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
  #! /usr/bin/env ruby

 +# prints out a greeting
  def hello
-   puts 'hello world'
+   puts 'hello mundo'
  end

  hello()

此时,我们已经很好地合并了文件。事实上,这实际上比ignore-space-change选项更好,因为这实际上在合并之前修复了空格更改,而不是简单地忽略它们。在ignore-space-change合并中,我们实际上最终得到了一些带有 DOS 行结束符的行,使事情变得混乱。

如果您想在最终确定此提交之前了解在某一方或另一方之间实际发生了什么更改,您可以要求git diff比较您即将作为合并结果提交到工作目录中的内容与这些阶段中的任何一个。让我们逐一介绍。

要将您的结果与合并之前您在分支中拥有的内容进行比较,换句话说,要查看合并引入了什么,您可以运行git diff --ours

$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@

 # prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

因此,在这里我们可以很容易地看到,在我们分支中发生的事情,我们实际上通过此合并引入此文件的内容,是更改了该行。

如果我们想看看合并的结果与他们那一侧的不同之处,可以运行git diff --theirs。在此示例和以下示例中,我们必须使用-b来去除空格,因为我们将其与 Git 中的内容进行比较,而不是我们清理过的hello.theirs.rb文件。

$ git diff --theirs -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello mundo'
 end

最后,您可以使用git diff --base查看文件从双方发生了哪些变化。

$ git diff --base -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

此时,我们可以使用git clean命令清除我们创建的用于手动合并但不再需要的额外文件。

$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb

检出冲突

也许出于某种原因,我们对此时的解决方案不满意,或者也许手动编辑一侧或两侧仍然效果不佳,我们需要更多上下文。

让我们稍微更改一下示例。对于此示例,我们有两个生命周期较长的分支,每个分支都有几个提交,但在合并时会产生合法的內容冲突。

$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) Update README
* 9af9d3b Create README
* 694971d Update phrase to 'hola world'
| * e3eb223 (mundo) Add more tests
| * 7cff591 Create initial testing script
| * c3ffff1 Change text to 'hello mundo'
|/
* b7dcc89 Initial hello world code

现在我们有三个仅存在于master分支上的唯一提交,以及三个存在于mundo分支上的其他提交。如果我们尝试合并mundo分支,我们将遇到冲突。

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

我们想看看合并冲突是什么。如果我们打开文件,我们将看到类似以下内容

#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()

合并的双方都向此文件添加了内容,但一些提交修改了导致此冲突的相同位置的文件。

让我们探讨一些您现在可以使用的工具来确定此冲突是如何发生的。也许不清楚您应该如何解决此冲突。您需要更多上下文。

一个有用的工具是带有--conflict选项的git checkout。这将再次检出文件并替换合并冲突标记。如果您想重置标记并再次尝试解决它们,这将非常有用。

您可以将--conflict传递diff3merge(这是默认值)。如果您传递diff3,Git 将使用略微不同的冲突标记版本,不仅提供“我们的”和“他们的”版本,还提供内联的“基础”版本以提供更多上下文。

$ git checkout --conflict=diff3 hello.rb

运行该命令后,文件将如下所示

#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

hello()

如果您喜欢这种格式,您可以将其设置为将来合并冲突的默认格式,方法是将merge.conflictstyle设置设置为diff3

$ git config --global merge.conflictstyle diff3

git checkout命令还可以接受--ours--theirs选项,这可以成为一种非常快速的方法,只需选择一侧或另一侧,而无需合并任何内容。

这对于二进制文件的冲突特别有用,在这些冲突中,您可以简单地选择一侧,或者在您只想从另一个分支合并某些文件时,您可以执行合并,然后在提交之前从一侧或另一侧检出某些文件。

合并日志

解决合并冲突时,另一个有用的工具是git log。这可以帮助您了解可能导致冲突的原因。回顾一些历史以记住为什么两行开发触及了代码的同一区域有时非常有帮助。

要获取包含此合并中涉及的任一分支的所有唯一提交的完整列表,我们可以使用我们在三点中学到的“三点”语法。

$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 Update README
< 9af9d3b Create README
< 694971d Update phrase to 'hola world'
> e3eb223 Add more tests
> 7cff591 Create initial testing script
> c3ffff1 Change text to 'hello mundo'

这是一个包含的六个总提交的不错列表,以及每个提交位于哪一行开发中。

但是,我们可以进一步简化这一点,为我们提供更具体的上下文。如果我们将--merge选项添加到git log,它将仅显示合并任一侧中触及当前冲突文件的提交。

$ git log --oneline --left-right --merge
< 694971d Update phrase to 'hola world'
> c3ffff1 Change text to 'hello mundo'

如果您改为使用-p选项运行它,您将只获得导致冲突的文件的差异。这在快速为您提供帮助理解某事为何发生冲突以及如何更智能地解决它的上下文方面非常有帮助。

组合差异格式

由于 Git 对任何成功的合并结果进行暂存,因此当您在有冲突的合并状态下运行git diff时,您只会得到当前仍处于冲突状态的内容。这有助于查看您仍需解决的问题。

在合并冲突后直接运行git diff时,它将以相当独特的 diff 输出格式提供信息。

$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
  #! /usr/bin/env ruby

  def hello
++<<<<<<< HEAD
 +  puts 'hola world'
++=======
+   puts 'hello mundo'
++>>>>>>> mundo
  end

  hello()

该格式称为“组合差异”,并在每行旁边提供两列数据。第一列显示该行在“我们的”分支和工作目录中的文件之间是否不同(添加或删除),第二列在“他们的”分支和工作目录副本之间执行相同的操作。

因此,在该示例中,您可以看到<<<<<<<>>>>>>>行在工作副本中,但不在合并的任一侧。这是有道理的,因为合并工具将它们放在那里供我们参考,但我们预计会删除它们。

如果我们解决冲突并再次运行git diff,我们将看到相同的内容,但它更有用。

$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

这向我们表明,“hola world”在我们的一侧,但在工作副本中不存在,“hello mundo”在他们的一侧,但在工作副本中不存在,最后“hola mundo”在任何一侧都不存在,但现在在工作副本中存在。这在提交解决方案之前进行审查很有用。

您还可以从任何合并的git log中获取此信息,以查看事后如何解决某些问题。如果您在合并提交上运行git show,或者如果您在git log -p(默认情况下仅显示非合并提交的补丁)中添加--cc选项,Git 将输出此格式。

$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <[email protected]>
Date:   Fri Sep 19 18:14:49 2014 +0200

    Merge branch 'mundo'

    Conflicts:
        hello.rb

diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

撤消合并

既然您知道如何创建合并提交,您可能会错误地创建一些提交。使用 Git 工作的一大好处是犯错误是可以的,因为可以(在许多情况下很容易)修复它们。

合并提交也不例外。假设您开始在一个主题分支上工作,意外地将其合并到master中,现在您的提交历史如下所示

Accidental merge commit
图 155. 意外的合并提交

有两种方法可以解决此问题,具体取决于您想要的结果。

修复引用

如果不需要的合并提交仅存在于您的本地代码仓库中,则最简单、最好的解决方案是移动分支,使其指向您希望它们指向的位置。在大多数情况下,如果您在错误的git merge之后跟随git reset --hard HEAD~,这将重置分支指针,使其看起来像这样

History after `git reset --hard HEAD~`
图 156. git reset --hard HEAD~之后的历史记录

我们在重置详解中介绍过reset命令,所以这里发生的事情应该不难理解。下面是一个快速回顾:reset --hard通常会经历三个步骤

  1. 移动分支 HEAD 指向的位置。在本例中,我们希望将master移动到合并提交(C6)之前的状态。

  2. 使索引看起来像 HEAD。

  3. 使工作目录看起来像索引。

这种方法的缺点是它会重写历史,这在共享仓库中可能存在问题。请查看变基的风险以了解更多可能发生的情况;简而言之,如果其他人拥有您正在重写的提交,则您应该避免使用reset。如果自合并以来创建了任何其他提交,此方法也不起作用;移动引用实际上会导致这些更改丢失。

反转提交

如果移动分支指针对您不起作用,Git 提供了创建新提交以撤消现有提交中所有更改的选项。Git 将此操作称为“反转”,在本例中,您将这样调用它

$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"

-m 1标志指示哪个父分支是“主线”并应保留。当您将合并内容引入HEADgit merge topic)时,新的提交有两个父分支:第一个是HEADC6),第二个是被合并分支的顶端(C4)。在本例中,我们希望撤消合并父分支 #2(C4)引入的所有更改,同时保留父分支 #1(C6)的所有内容。

包含反转提交的历史记录如下所示

History after `git revert -m 1`
图 157. 执行git revert -m 1后的历史记录

新的提交^MC6的内容完全相同,因此从这里开始,就好像从未发生过合并一样,除了现在未合并的提交仍然在HEAD的历史记录中。如果您尝试再次将topic合并到master,Git 会感到困惑

$ git merge topic
Already up-to-date.

topic中没有任何内容无法从master访问。更糟糕的是,如果您向topic添加工作并再次合并,Git 仅会引入反转合并以来的更改

History with a bad merge
图 158. 错误合并的历史记录

解决此问题的最佳方法是取消原始合并的反转,因为现在您希望引入已反转的更改,然后创建新的合并提交

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
History after re-merging a reverted merge
图 159. 重新合并已反转的合并后的历史记录

在本例中,M^M相互抵消。^^M有效地合并了来自C3C4的更改,而C8合并了来自C7的更改,因此现在topic已完全合并。

其他类型的合并

到目前为止,我们已经介绍了两个分支的普通合并,通常使用称为“递归”的合并策略来处理。但是,还有其他方法可以将分支合并在一起。让我们快速介绍其中的一些。

我们的或他们的优先级

首先,我们可以对普通“递归”合并模式执行另一项有用的操作。我们已经看到了ignore-all-spaceignore-space-change选项,它们是通过-X传递的,但我们还可以告诉 Git 在遇到冲突时偏向某一方。

默认情况下,当 Git 看到两个正在合并的分支之间存在冲突时,它会将合并冲突标记添加到您的代码中,并将文件标记为冲突,并让您解决它。如果您希望 Git 简单地选择特定的一方并忽略另一方,而不是让您手动解决冲突,您可以向merge命令传递-Xours-Xtheirs

如果 Git 看到此选项,它不会添加冲突标记。任何可合并的差异,它都会合并。任何冲突的差异,它将简单地选择您指定的整个一方,包括二进制文件。

如果我们回到之前使用的“hello world”示例,我们可以看到合并我们的分支会导致冲突。

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

但是,如果我们使用-Xours-Xtheirs运行它,则不会出现冲突。

$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 test.sh  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 test.sh

在这种情况下,它不会在文件中获取带有“hello mundo”在一侧和“hola world”在另一侧的冲突标记,而是简单地选择“hola world”。但是,该分支上所有其他非冲突更改都已成功合并。

此选项还可以传递给我们之前看到的git merge-file命令,方法是运行类似git merge-file --ours的内容来进行单个文件合并。

如果您想执行类似的操作,但不想让 Git 尝试从另一侧合并更改,则有一个更严厉的选项,即“ours”合并策略。这与“ours”递归合并选项不同。

这基本上会执行一个伪合并。它会记录一个新的合并提交,并将两个分支作为父分支,但它甚至不会查看您正在合并的分支。它只会将当前分支中的确切代码记录为合并的结果。

$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$

您可以看到我们所在的分支与合并结果之间没有区别。

这通常可以用来欺骗 Git 认为某个分支已合并,以便在稍后进行合并。例如,假设您从release分支中分支出来,并在其上完成了一些工作,您希望在某个时候将其合并回master分支。同时,master上的一些错误修复需要反向移植到您的release分支中。您可以将错误修复分支合并到release分支中,并将同一个分支merge -s ours到您的master分支中(即使修复已存在),这样当您稍后再次合并release分支时,不会出现来自错误修复的冲突。

子树合并

子树合并的想法是,您有两个项目,其中一个项目映射到另一个项目的子目录。当您指定子树合并时,Git 通常足够智能,可以确定一个项目是另一个项目的子树,并进行相应的合并。

我们将逐步介绍如何将一个单独的项目添加到现有项目中,然后将第二个项目的代码合并到第一个项目的子目录中。

首先,我们将 Rack 应用程序添加到我们的项目中。我们将在自己的项目中添加 Rack 项目作为远程引用,然后将其检出到它自己的分支中

$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote --no-tags
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"

现在,我们在rack_branch分支中拥有 Rack 项目的根目录,在master分支中拥有我们自己的项目。如果您检出一个分支,然后再检出另一个分支,您会发现它们具有不同的项目根目录

$ ls
AUTHORS         KNOWN-ISSUES   Rakefile      contrib         lib
COPYING         README         bin           example         test
$ git checkout master
Switched to branch "master"
$ ls
README

这是一种有点奇怪的概念。存储库中并非所有分支都必须是同一项目的子分支。这并不常见,因为它很少有用,但拥有包含完全不同历史记录的分支相当容易。

在本例中,我们希望将 Rack 项目作为子目录拉取到我们的master项目中。我们可以在 Git 中使用git read-tree来实现。您将在Git 内部机制中了解有关read-tree及其相关命令的更多信息,但现在请知道它会将一个分支的根目录读取到您当前的暂存区和工作目录中。我们刚刚切换回您的master分支,并将rack_branch分支拉取到主项目的master分支的rack子目录中

$ git read-tree --prefix=rack/ -u rack_branch

当我们提交时,看起来我们在该子目录下拥有所有 Rack 文件——就像我们从 tarball 中复制它们一样。有趣的是,我们可以相当容易地将一个分支的更改合并到另一个分支中。因此,如果 Rack 项目更新,我们可以通过切换到该分支并拉取来引入上游更改

$ git checkout rack_branch
$ git pull

然后,我们可以将这些更改合并回我们的master分支。要引入更改并预填充提交消息,请使用--squash选项以及递归合并策略的-Xsubtree选项。递归策略是这里的默认策略,但为了清楚起见,我们将其包含在内。

$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

Rack 项目的所有更改都已合并并准备在本地提交。您也可以执行相反的操作——在master分支的rack子目录中进行更改,然后稍后将其合并到rack_branch分支中,以将其提交给维护者或将其推送到上游。

这为我们提供了一种方法,可以拥有类似于子模块工作流程的工作流程,而无需使用子模块(我们将在子模块中介绍)。我们可以将其他相关项目的子分支保留在我们的存储库中,并偶尔将其子树合并到我们的项目中。在某些方面它很好,例如所有代码都提交到一个地方。但是,它也有一些缺点,因为它有点复杂,并且更容易在重新集成更改或意外地将分支推送到不相关的存储库时出错。

另一件稍微奇怪的事情是,要获取您在rack子目录中拥有的内容与rack_branch分支中的代码之间的差异——以查看您是否需要合并它们——您不能使用普通的diff命令。相反,您必须使用要比较的分支运行git diff-tree

$ git diff-tree -p rack_branch

或者,要将rack子目录中的内容与上次提取时服务器上的master分支进行比较,您可以运行

$ git diff-tree -p rack_remote/master
scroll-to-top