-
1. 起步
-
2. Git 基础
-
3. Git 分支
-
4. 服务器上的 Git
- 4.1 协议
- 4.2 在服务器上部署 Git
- 4.3 生成 SSH 公钥
- 4.4 架设服务器
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管服务
- 4.10 小结
-
5. 分布式 Git
-
A1. 附录 A: Git 在其他环境
- A1.1 图形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小结
-
A2. 附录 B: 在应用程序中嵌入 Git
-
A3. 附录 C: 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`。这将再次检出文件并替换合并冲突标记。如果你想重置标记并尝试再次解决它们,这会很有用。
你可以传递 `diff3` 或 `merge`(默认值)给 `--conflict`。如果你传递 `diff3`,Git 将使用略有不同的冲突标记版本,不仅给你“ours”和“theirs”版本,还在线内提供“base”版本,给你更多上下文。
$ 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`,它会以一种相当独特的差异输出格式向你提供信息。
$ 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()
该格式称为“组合差异”,它在每一行旁边为你提供两列数据。第一列显示该行在“ours”分支和工作目录中的文件之间是否不同(添加或删除),第二列显示“theirs”分支和工作目录副本之间是否相同。
因此,在该示例中,你可以看到 `<<<<<<<` 和 `>>>>>>>` 行位于工作副本中,但不在合并的任何一侧。这是有道理的,因为合并工具将它们放在那里是为了提供上下文,但我们预计会将其删除。
如果解决冲突并再次运行 `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 <schacon@gmail.com>
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` 中,现在你的提交历史看起来像这样
解决这个问题有两种方法,取决于你期望的结果是什么。
修复引用
如果不需要的合并提交只存在于你的本地仓库中,最简单和最好的解决方案是移动分支,使它们指向你想要的位置。在大多数情况下,如果你在错误的 `git merge` 之后执行 `git reset --hard HEAD~`,这将重置分支指针,使它们看起来像这样
git reset --hard HEAD~ 后的历史记录我们在揭秘 Reset中已经介绍了 `reset`,所以理解这里发生的事情应该不会太难。这里快速回顾一下:`reset --hard` 通常会经历三个步骤
-
移动 HEAD 所指向的分支。在本例中,我们希望将 `master` 移动到合并提交 (C6) 之前的位置。
-
使索引看起来像 HEAD。
-
使工作目录看起来像索引。
这种方法的缺点是它会重写历史,这对于共享仓库来说可能会有问题。请查看变基的危险以了解更多可能发生的情况;简而言之,如果其他人拥有你正在重写的提交,你应该避免使用 `reset`。如果合并后创建了任何其他提交,这种方法也不会奏效;移动引用实际上会丢失这些更改。
撤销提交
如果移动分支指针不适合你,Git 提供了创建一个新提交的选项,该提交会撤消现有提交的所有更改。Git 将此操作称为“revert”,在这种特定情况下,你会像这样调用它
$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"
`-m 1` 标志指示哪个父级是“主线”并且应该保留。当你调用合并到 `HEAD`(`git merge topic`)时,新的提交有两个父级:第一个是 `HEAD`(`C6`),第二个是被合并分支的尖端(`C4`)。在这种情况下,我们想撤销合并父级 #2(`C4`)引入的所有更改,同时保留父级 #1(`C6`)的所有内容。
带有 revert 提交的历史记录如下所示
git revert -m 1 后的历史记录新的提交 `^M` 与 `C6` 的内容完全相同,所以从这里开始,就好像合并从未发生过一样,只是现在未合并的提交仍然在 `HEAD` 的历史记录中。如果你尝试再次将 `topic` 合并到 `master` 中,Git 会感到困惑
$ git merge topic
Already up-to-date.
`topic` 中没有任何内容是 `master` 已经可以访问的。更糟糕的是,如果你向 `topic` 添加工作并再次合并,Git 只会引入**自**被撤销合并以来的更改
解决此问题的最佳方法是撤销原始合并(因为现在您要引入被撤销的更改),**然后**创建新的合并提交。
$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
在这个例子中,`M` 和 `^M` 互相抵消。`^^M` 有效地合并了 `C3` 和 `C4` 的更改,`C8` 合并了 `C7` 的更改,所以现在 `topic` 已完全合并。
其他合并类型
到目前为止,我们已经介绍了两个分支的正常合并,通常通过所谓的“递归”合并策略进行处理。然而,还有其他方法可以将分支合并在一起。让我们快速介绍其中几种。
我们的或他们的偏好
首先,我们可以在正常的“递归”合并模式下做另一件有用的事情。我们已经看到了通过 `-X` 传递的 `ignore-all-space` 和 `ignore-space-change` 选项,但我们也可以告诉 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 --ours` 之类的命令传递给我们之前看到的 `git merge-file` 命令,用于单个文件合并。
如果你想做类似的事情,但不希望 Git 甚至尝试从另一方合并更改,还有一个更严厉的选项,那就是“ours”合并*策略*。这与“ours”递归合并*选项*不同。
这基本上会进行一次假合并。它会记录一个新的合并提交,并将两个分支都作为父级,但它甚至不会查看你正在合并的分支。它只会将你当前分支中的确切代码记录为合并结果。
$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$
你可以看到,我们所在的分支与合并结果之间没有区别。
这通常很有用,可以有效地欺骗 Git 认为某个分支在稍后合并时已经合并。例如,假设你从一个 `release` 分支分出,并做了一些工作,你希望在某个时候将其合并回 `master` 分支。与此同时,`master` 上的一个 bug 修复需要回溯到你的 `release` 分支。你可以将 bug 修复分支合并到 `release` 分支中,同时也将相同的分支 `merge -s ours` 到你的 `master` 分支中(即使修复已经在那里),这样当你稍后再次合并 `release` 分支时,就不会出现来自 bug 修复的冲突。
子树合并
子树合并的理念是你有两个项目,其中一个项目映射到另一个项目的子目录。当你指定子树合并时,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 read-tree` 在 Git 中完成此操作。你将在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