为什么合并git中更改的文件时没有合并冲突?

时间:2018-12-09 13:11:08

标签: git github git-merge

我对使用git作为开发的版本控制系统是完全陌生的,并且仍在学习中。 我对拉取请求期间的合并冲突感到困惑。据我了解,合并冲突通常在文件的同一行出现 在两个不同的分支中是不同的。 我有一个具有以下树结构的git存储库。因此我从“ master”创建了一个“ dev”分支,然后从“ dev”创建了“ D1”和“ D2”分支。 master-> dev->(D1,D2) 在“ dev”分支中,我的文件名为“ file1.txt”,第一行为 1111111111111 ,在“ D1”分支中,我的文件名为“ file1.txt”,其第一行为 AAAAAAAAAAAAAAA 。我创建了一个合并请求,将“ D1”分支合并到“ dev”,并期望由于两个分支的第一行“ file1.txt”不同而发生合并冲突。

但是git显示能够合并消息,指示没有合并冲突,并且“ dev”分支更改被“ D1”分支更改覆盖。 不知道我在想什么。

2 个答案:

答案 0 :(得分:5)

  

据我所知,当文件的同一行在两个不同的分支中不同时,通常会发生合并冲突

这不是那样。当您在两个不同的分支中更改文件的同一行,然后尝试将它们合并在一起时,Git会检测到冲突。

在您的示例中,您已更改了分支D1中文件的第一行,但未在dev分支中对其进行触摸,因此合并过程看起来像“从分支D1中进行第一行更改并将其应用于第一行”在分支机构中”。

相反,如果您(或其他人)在创建D1之后更改了分支dev中文件的第一行,然后又在D1中更改了该行,则合并过程将如下所示:“ D1分支的第一行以及dev分支的第一行-我认为主要的变更是什么?” -这就是合并冲突的样子。

答案 1 :(得分:1)

这里有很多东西要学习。有人(at least one)已将Git的学习曲线称为“学习墙”。在我看来,在Git上添加GitHub之类的Web服务实际上会变得更加困难:不再可能确定 Git 中是否存在某些内容,或者是 GitHub 提供的内容。拉取请求实际上属于GitHub提供的类别。但是它们基于Git的 merge 动词(结合GitHub同时存储您的存储库和其他人的存储库的事实)。

让我们暂时搁置所有内容,然后从Git本身开始。理解某个文件中是否存在合并冲突以及何时发生冲突的关键隐藏在某些graph theory后面。

什么是提交

首先,让我们提到提交保留文件。更准确地说,自您提交该次提交时,每个提交都拥有文件的全部快照。但是,每个提交还具有关于该提交的 额外信息,例如谁进行提交,何时进行以及为什么执行(日志消息)。

在Git中,每个提交均由其哈希ID唯一标识。这是一个很大的,丑陋的,几乎不可读的,对人类完全无用的字符串,例如5d826e972970a784bd7a7bdf587512510097b8c7(在Git的Git存储库中的实际提交)。这些东西是Git查找提交的方式,尽管除了复制粘贴或链接目的外,我不建议 you 经常使用它们。 :-)

还是,重要的是要知道这就是Git的工作方式,因为几乎每个提交都列出了至少一个 parent 提交的原始哈希ID。父级是此提交之前 的提交。可以确定列出父级的一个提交是有史以来第一次提交的提交,当然也没有以前的提交。

这意味着从任何分支上的 last 提交开始,Git可以向后进行工作,直到先前的提交。然后,Git有了一个表,从诸如master之类的可读分支名称到原始提交哈希ID。要将 new 提交添加到分支:

  1. Git从分支名称获取 current 提交哈希ID。
  2. Git将其作为新提交的父项放入 new 提交中。当然,Git还会放入新的快照以及您的姓名等等。这样会产生一个新的,唯一的,难看的哈希ID。
  3. Git然后将 new 提交的哈希ID写入名称master

当某事物拥有提交哈希ID时,我们说该事物指向。这样,分支名称始终指向分支上的 last 提交。最后的提交指向其前身,后者指向另一步,依此类推,因此Git可以从那里向后工作。这种分步执行(一次提交)是Git保存您以及其他所有人曾经做过的所有事情的历史的方式:每次提交都会记录一次快照,并且每次提交(除了第一次提交)都有一个“以前,看起来像...”的历史链接。

绘制图形

创建新分支时,Git会创建一个新表条目,以便两个分支都指向 same 提交。出于绘图目的,让我对每个提交使用单个大写字母,而不是一个大的丑陋的哈希ID。那么我们可能会有这个:

... <-F <-G <-H   <-- master, develop (HEAD)

一旦进行了提交,其中的任何内容都无法更改。因此H 总是指向G,依此类推。 (分支名称可以并且确实一直在变化,通常是为了适应新提交。)因此,出于绘图目的,在StackOverflow文本中,我将省略内部箭头,而仅保留分支名称箭头:

...--F--G--H   <-- master, develop (HEAD)

名称HEAD附加在分支名称之一上。这就是Git知道我们正在使用哪个分支的原因,因为当我们进行 new 提交时,Git必须更新此分支名称。现在,我们进行一次新提交,并调用其哈希I

...--F--G--H   <-- master
            \
             I   <-- develop (HEAD)

现在让我们回到master并在那里进行新提交,我们可以将其称为J

             J   <-- master (HEAD)
            /
...--F--G--H
            \
             I   <-- develop

请注意,提交H和更早的版本都在两个分支上。

合并为动词

如果我们现在要求Git 合并 develop(即,提交I)回到master(即,提交J), Git将找到最佳共同祖先提交。松散地说,这是Git通过从两个分支技巧中进行反向操作可以找到的第一个 shared 提交。在这种情况下,很显然是提交H

已经找到了共同的祖先提交(Git称为合并基础),Git现在必须弄清楚我们改变了什么以及他们做了什么。 >已更改。也就是说,Git必须针对两个分支技巧diff提交H(合并基础):--ours,这是提交J--theirs,这是提交I。实际上,这需要两个单独的git diff命令:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed
git diff --find-renames <hash-of-H> <hash-of-I>   # what they changed

Git现在可以组合这两组更改。我们触摸了一些文件,而他们触摸了一些文件。当我们和他们触摸 same 文件时,我们将某些行从合并基础更改为 our 提交,并且他们将某些行从合并基础更改了来他们的提交。

如果Git决定我们触摸了相同行,但是对这些行进行了不同更改,则会发生冲突。 Same 这在大多数情况下是显而易见的-如果我更改了第17行,而他们又更改了第17行,我们显然也更改了相同的行。但是,相同可能有点奇怪:例如,如果我们都在文件的 end 处添加了不同的文本,则Git不知道将其放入哪个顺序,所以这也是一个冲突。

但是只要我们触摸不同行或不同的文件,就不会发生冲突:Git对每套应用两组套更改合并基本文件,以获取合并结果。然后,Git可以根据合并的更改进行新的提交。

合并过程称为three-way merge另请参见Why is a 3-way merge advantageous over a 2-way merge?

合并为形容词或名词

此新提交具有独特的属性;让我们通过绘制结果来显示它:

             J
            / \
...--F--G--H   K   <-- master (HEAD)
            \ /
             I   <-- develop

这里发生的是新提交K记住了两个先前提交:首先,它自己的先前提交为J。那是K第一父母。但是K也有第二个父级:它还记得它来自I。这使得Git在未来合并中所做的工作要少得多,因为它更改了哪个提交将成为后来的 合并。 (这是图论中最复杂的部分,我们将跳过。)

合并更改并进行新提交的过程是单词 merge 的动词形式,即要合并。通过此过程 进行的提交使用与形容词相同的单词:K合并提交。 Git和Git的用户经常将其简称为 K是合并,它使用词 merge 作为名词。

合并提交就是至少有两个父母的任何提交git merge命令通常进行此类提交。 (但并非总是如此!这是Git陡峭的学习曲线的另一部分。现在,让我们忽略它,因为GitHub试图(半成功地)向您隐藏了它。)最后,当您具有合并(名词),它是由合并(动词)过程完成的。该过程使用三个输入-合并基础和两个分支提示-产生合并的更改。然后git merge命令产生名词形式的结果。

grok此合并过程非常重要,这通常只需要时间和实践。原因是将{em> merge作为动词的动作不只是git merge:许多其他的Git命令也这样做,它们只是不进行 merge提交完成后。每次运行git rebasegit cherry-pickgit revert时,都将使用Git的合并机制。甚至看似简单(我认为是欺骗性的)的git stash都使用Git的合并机制。当Git靠自己正确地解决问题时(大多数时候),您不必考虑它,但是当它出错时,您需要知道该怎么做。

(对于Git,我发现将配置选项merge.conflictStyle设置为diff3也很有帮助,这样当Git留下混乱的合并错误,供您修复时,它会显示 input merge base 以及两个分支技巧。其他人则喜欢使用基于窗口的精美合并工具,所以这很有趣。)

在GitHub提取请求中

现在我们以上述内容为背景,让我们看看GitHub的“发出拉取请求”按钮是如何工作的。为了告诉您是否可以将您的分支与其他人的分支合并,GitHub所做的实际上是合并,作为测试合并。 1 此测试合并GitHub根本不在 any 分支上,但是它仍然遵循相同的基本思想。如果没有合并冲突,则测试成功,并且GitHub说能够合并。如果存在冲突,则GitHub会放弃测试合并(它无法比Git本身完成更多的工作),并告诉您存在冲突。

如果存在 冲突,则当然要由您自己决定如何解决。现在,您需要了解上述的三向合并过程,这需要了解图,或者,如果不是真的了解所有图论,至少要对合并内容有个好主意基地是。您可以找到基准,将其与两个技巧进行比较,并查看冲突是如何产生的。 (或者,将merge.conflictStyle设置为diff3,Git会将冲突源留在工作树中,您可以在其中直接对其进行编辑。)


1 我在这里跳过了Git 传输从一个存储库到另一个存储库的提交过程的一些重要方面。正如我所提到的,GitHub在同一网站上同时具有您的存储库,因此他们可以在这里做作弊-他们无需在任何地方进行任何转移,他们拥有全部。同样,它们实际上并没有运行git merge,因为这需要工作树:GitHub上的存储库都是所谓的 bare 存储库,没有工作树。但是所有这些都是挑剔的细节,破坏了“ GitHub运行测试合并”这个不错的概观:它们 do 实际上运行测试合并。他们只有很多横向技巧才能实现它。

在此脚注中,还值得一提的是GitHub在这里相当于git merge --no-ff。如果没有任何快进控制旋钮,则无法选择执行git merge --ff-onlygit merge的等效操作。

对于拉取请求 N ,拉取请求的提交本身可以在引用refs/pull/N/head下进行提取。如果测试合并成功,则在引用refs/pull/N/merge下。