2.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 传递给 diff3 或 merge(这是默认值)。如果你传递 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'
这是一个很好的列表,包含总共六个提交,以及每个提交所在的开发线。
不过,我们可以进一步简化它,以提供更具体的上下文。如果我们向 git log 添加 --merge 选项,它将只显示合并中触及当前有冲突文件的提交。
$ git log --oneline --left-right --merge
< 694971d Update phrase to 'hola world'
> c3ffff1 Change text to 'hello mundo'
如果你改用 -p 选项运行它,你将只获得导致冲突的文件的 diff。这对于快速提供你所需的上下文,以帮助理解为什么会出现冲突以及如何更智能地解决它,将非常有用。
合并的组合 diff 格式
由于 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()
这种格式称为“Combined Diff”,并在每行旁边提供两列数据。第一列显示该行在“我们的”分支和你的工作目录文件之间是否有差异(添加或删除),第二列显示“他们的”分支和你的工作目录副本之间是否有差异。
所以在这个例子中,你可以看到 <<<<<<< 和 >>>>>>> 行存在于工作副本中,但并未存在于合并的任何一方。这是有道理的,因为合并工具为了我们的上下文放入了它们,但我们应该删除它们。
如果我们解决了冲突并再次运行 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
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()