简体   繁体   English

Git - 即使没有冲突,如何强制手动合并

[英]Git - how to force manual merge even if there is no conflict

This is a question which was asked many times over the years. 这是多年来多次被问过的问题。 I have found a number of answers, in particular this one: 我找到了很多答案,尤其是这个答案:

Git - how to force merge conflict and manual merge on selected file (@Dan Moulding) Git - 如何在选定文件上强制合并冲突和手动合并 (@Dan Molding)

This page contains is a detailed guide how to set up a merge driver that would always return failure and thus make possible a manual merge. 此页面包含有关如何设置合并驱动程序的详细指南,该合并驱动程序始终会返回失败,从而可以进行手动合并。 I have tried to adapt that solution for Windows: 我试图适应Windows的解决方案:

  1. I added the following to my %homepath%\\.gitconfig : 我将以下内容添加到我的%homepath%\\.gitconfig

    [merge "verify"] name = merge and verify driver driver = %homepath%\\\\merge-and-verify-driver.bat %A %O %B

  2. I changed the driver to 我改变了驱动程序

    cmd /K "echo Working > merge.log & git merge-file %1% %2% %3% & exit 1"

    ( echo Working > merge.log was added to check whether the driver was invoked). (添加echo Working > merge.log以检查是否调用了驱动程序)。

  3. and, at the root of the repo, created a file .gitattributes with the following line: 并且,在repo的根目录下,使用以下行创建了一个.gitattributes文件:

    *.txt merge=verify

Unfortunately, it does not work. 不幸的是,它不起作用。 I tried to merge a file, feature.txt , and, alas, the merge completed successfully. 我试图合并一个文件, feature.txt ,唉,合并成功完成。 It seems that the driver was not invoked at all, since the merge.log file was not created. 似乎根本没有调用驱动程序,因为未创建merge.log文件。

Do I do anything wrong? 我做错了吗? Any solution to the problem of forcing manual merge is most welcome. 任何强制手动合并问题的解决方案都是最受欢迎的。

There are two parts to the problem. 这个问题有两个部分。 The relatively easy one is writing the custom merge driver, as you did in steps 1 and 2. The hard one is that Git doesn't actually bother running the custom driver if, in Git's opinion, it's not necessary. 相对容易的是编写自定义合并驱动程序,正如您在步骤1和2中所做的那样。困难的是,如果Git认为没有必要,Git实际上并不打扰运行自定义驱动程序。 This is what you have observed in step 3. 这是您在步骤3中观察到的。

So, when does Git run your merge driver? 所以,当的Git运行合并驱动程序? The answer is fairly complicated, and to get there we have to define the term merge base , which we'll get to in a moment. 答案是相当复杂的,为了实现这一目标,我们必须定义术语合并基础 ,我们将在稍后介绍。 You also need to know that Git identifies files—in fact, pretty much everything: commits, files, patches, and so on—by their hash IDs . 您还需要知道Git识别文件 - 事实上,几乎所有内容:提交,文件,补丁等等 - 它们的哈希ID If you already know all of this, you can skip directly to the last section. 如果您已经知道所有这些,可以直接跳到最后一节。

Hash IDs 哈希ID

Hash IDs (or sometimes object IDs or OIDs) are those big ugly names you see for commits: 哈希ID(或有时是对象ID或OID)是您在提交时看到的那些丑陋的名字:

$ git rev-parse HEAD
7f453578c70960158569e63d90374eee06104adc
$ git log
commit 7f453578c70960158569e63d90374eee06104adc
Author: ...

Everything Git stores has a unique hash ID, computed from the contents of the object (the file or commit or whatever). Git存储的所有东西都有一个唯一的哈希ID,根据对象的内容(文件或提交或其他)计算。

If you store the same file twice (or more), you get the same hash ID twice (or more). 如果将同一文件存储两次 (或更多次),则会获得两次(或更多)相同的哈希ID。 Since each commit ultimately stores a snapshot of every file as of the time of that commit, each commit therefore has a copy of every file, listed by its hash ID. 由于每次提交最终都会存储截至提交时每个文件的快照,因此每个提交都有一个每个文件的副本,由其哈希ID列出。 You can in fact view these: 你其实可以看到这些:

$ git ls-tree HEAD
100644 blob b22d69ec6378de44eacb9be8b61fdc59c4651453    README
100644 blob b92abd58c398714eb74cbe66671c7c3d5c030e2e    integer.txt
100644 blob 27dfc5306fbd27883ca227f08f06ee037cdcb9e2    lorem.txt

The three big ugly IDs in the middle are the three hash IDs. 中间的三个丑陋的ID是三个哈希ID。 Those three files are in the HEAD commit under those IDs. 这三个文件位于这些ID下的HEAD提交中。 I have the same three files in several more commits, usually with slightly different contents. 我在几个提交中有相同的三个文件,通常内容略有不同。

Getting to the merge base: the DAG 进入合并基地:DAG

The DAG , or D irected A cyclic G raph, is a way of drawing the relationships between commits. 所述DAG,d irected 环状ģ拍摄和,是绘制提交之间的关系的一种方式。 To really use Git properly, you need at least a vague idea of what the DAG is. 要真正正确地使用Git,您至少需要对DAG的含义有一个模糊的概念。 It's also called the commit graph , which is a nicer term in some ways since it avoids specialized informatics jargon. 它也被称为提交图 ,它在某些方面是一个更好的术语,因为它避免了专门的信息学术语。

In Git, when we make branches, we can draw them in any number of various ways. 在Git中,当我们创建分支时,我们可以通过各种方式绘制它们。 The method I like to use here (in text, on StackOverflow) is to put earlier commits on the left and later commits on the right, and to label each commit with a single uppercase letter. 我喜欢在这里使用的方法(在文本中,在StackOverflow上)是在左边提交早期提交,在右边提交稍后提交,并用单个大写字母标记每个提交。 Ideally, we'd draw these the way Git keeps them, which is rather backwards: 理想情况下,我们以Git保留它们的方式绘制它们,这是相反的:

A <- B <- C   <-- master

Here we have just three commits, all on master . 在这里,我们只有三个提交,全部都在master The branch name master "points to" the last of the three commits. 分支名称 master “指向”三个提交中的最后一个。 This is how Git actually finds commit C , by reading its hash ID from the branch name master , and in fact the name master effectively stores just this one ID. 这就是Git实际上通过从分支名称master读取其哈希ID来实际找到提交C ,实际上名称master实际上存储这一个ID。

Git finds commit B by reading commit C . Git通过读取提交C找到提交B Commit C has, inside it, the hash ID of commit B . Commit C里面有commit B的哈希ID。 We say that C "points to" B , hence the backwards-pointing arrow. 我们说C “指向” B ,因此向后指向箭头。 Likewise, B "points to" A . 同样, B “指向” A Since A is the very first commit, it has no previous commit so it has no back-pointer. 由于A是第一次提交,因此它没有先前的提交,因此它没有后向指针。

These internal arrows tell Git about the parent commit of each commit. 这些内部箭头告诉Git每个提交的提交。 Most of the time, we don't care that they are all backwards, so we can draw this more simply as: 大多数时候,我们并不关心它们都是倒退的,所以我们可以更简单地将它绘制为:

A--B--C   <-- master

which lets us pretend that it's obvious that C comes after B , even though in fact that's quite hard in Git. 这让我们假装C显然是在B之后,尽管事实上在Git中很难。 (Compare with the claim " B comes before C ", which is very easy in Git: It's easy to go backwards, because the internal arrows are all backwards.) (与声称“ B来自C之前”相比,这在Git中非常容易:它很容易向后移动,因为内部箭头都是向后的。)

Now let's draw an actual branch. 现在让我们画一个实际的分支。 Suppose we make a new branch, starting at commit B , and make a fourth commit D (it's not clear exactly when we make it but in the end it doesn't matter anyway): 假设我们创建一个新的分支,从提交B开始,并进行第四次提交D (我们不确定它们到底是什么时候 ,但最后它无关紧要):

A--B--C   <-- master
    \
     D   <-- sidebr

The name sidebr now points to commit D , while the name master points to commit C . 名称sidebr现在指向提交D ,而名称master指向提交C

One key Git concept here is that commit B is on both branches. 这里的一个关键Git概念是提交B两个分支上。 It's on master and sidebr . 这是master sidebr This is true for commit A as well. 对于提交A也是如此。 In Git, any given commit can be, and often is, on many branches simultaneously. 在Git中,任何给定的提交都可以并且通常在许多分支上同时进行。

There's another key concept hidden in Git here that is quite different from most other version control systems, which I will just mention in passing. Git中隐藏的另一个关键概念与大多数其他版本控制系统完全不同,我将顺便提及。 This is that the actual branch is actually formed by the commits themselves , and that the branch names have almost no meaning or contribution here. 这是实际的分支实际上由提交本身形成,并且分支名称在这里几乎没有意义或贡献。 The names merely serve to find the branch tips : commits C and D in this case. 这些名称仅用于查找分支提示 :在这种情况下提交CD The branch itself is what we get by drawing the connecting lines, going from newer (child) commits back to older (parent) commits. 分支本身就是我们通过绘制连接线获得的,从较新的(子)提交回到较旧的(父)提交。

It's also worth noting, as a side point, that this weird backwards linkage allows Git to never, ever change anything about any commit . 作为一个侧面点,值得注意的是,这种奇怪的向后链接允许Git 永远不会改变任何提交 Note that both C and D are children of B , but we didn't necessarily know , back when we made B , that we were going to make both C and D . 请注意, CD都是B孩子,但是当我们制作B时,我们不一定知道我们要同时制作C D But, because the parent doesn't "know" its children, Git did not have to store the IDs of C and D inside B at all. 但是,因为父母不“知道”它的孩子,Git根本不必将CD的ID存储在B中。 It just stores the ID of B —which definitely did exist by then—inside each of C and D when it creates each of C and D . 它只是存储的ID B哪位绝对没有由当时的内部的每一个存在CD时,它创建的每个的CD

These drawings that we make show (part of) the commit graph . 我们制作的这些图纸显示了提交图 (的一部分)。

The merge base 合并基地

A proper definition of merge bases is too long to go into here, but now that we've drawn the graph, an informal definition is very easy, and visually obvious. 合并基础的正确定义太长了,无法进入这里,但现在我们已经绘制了图形,非正式定义非常简单,并且在视觉上很明显。 The merge base of two branches is the point at which they first come together , when we work backwards as Git does. 当我们像Git那样向后工作时,两个分支的合并基础是它们首先聚集在一起的点。 That is, it's the first such commit that's on both branches . 也就是说,这是两个分支上的第一个这样的提交。

Thus, in: 因此,在:

A--B--C   <-- master
    \
     D   <-- sidebr

the merge base is commit B . 合并基础是提交B If we make more commits: 如果我们提交更多提交:

A--B--C--F   <-- master
    \
     D--E--G   <-- sidebr

the merge base remains commit B . 合并基础仍然是提交B If we actually make a successful merge, the new merge commit has two parent commits instead of just one: 如果我们实际上成功合并,则新的合并提交有两个父提交而不是一个:

A--B--C--F---H   <-- master
    \       /
     D--E--G   <-- sidebr

Here, commit H is the merge, which we made on master by running git merge sidebr , and its two parents are F (the commit that used to be the tip of master ) and G (the commit that still is the tip of sidebr ). 这里,提交H是合并,我们通过运行git merge sidebrmastergit merge sidebr ,它的两个父项是F (以前是master的提示)和G (仍然 sidebr ) 。

If we now continue making commits, and later decide to do another merge, G will be the new merge base: 如果我们现在继续提交,然后决定进行另一次合并, G将成为新的合并基础:

A--B--C--F---H--I   <-- master
    \       /
     D--E--G--J   <-- sidebr

H has two parents, and we (and Git) follow both parents "simultaneously" when we look backwards. H两个父母,当我们向后看时,我们(和Git)“同时”跟随父母。 Hence, commit G is the first one that's on both branches, if and when we run another merge. 因此,如果我们运行另一个合并,则提交G是两个分支上的第一个。

Aside: cross merges 旁白:交叉合并

Note that F is not, in this case, on sidebr : we have to follow the parent links as we encounter them, so J leads back to G which leads back to E , etc., so that we never get to F when starting from sidebr . 请注意,在这种情况下, F不在sidebr :我们必须在遇到它们时遵循父链接,因此J返回到G ,它返回到E等等,这样我们就不会在开始时到达F sidebr If, however, we make our next merge from master into sidebr : 但是,如果我们将下一个 master合并 sidebr

A--B--C--F---H--I   <-- master
    \       /    \
     D--E--G--J---K   <-- sidebr

Now commit F is on both branches. 现在提交F在两个分支上。 But in fact, commit I is also on both branches, so even though this makes merges going both ways, we're OK here. 但事实上,提交I也在两个分支上,所以即使这使得合并双向进行,我们也可以。 We can get in trouble with so called "criss cross merges", and I will draw one just to illustrate the problem, but not go into it here: 我们可以通过所谓的“纵横交错”来解决问题,我会画出一个只是为了说明问题,但不是在这里进行说明:

A--B--C--E-G--I   <-- br1
    \     X
     D---F-H--J   <-- br2

We get this by starting with the two branches going out to E and F respectively, then doing git checkout br1; git merge br2; git checkout br2; git merge br1 我们从两个分支开始分别到EF开始,然后做git checkout br1; git merge br2; git checkout br2; git merge br1 git checkout br1; git merge br2; git checkout br2; git merge br1 git checkout br1; git merge br2; git checkout br2; git merge br1 to make G (a merge of E and F , added to br1 ) and then immediately also make H (a merge of F and E , added to br2 ). git checkout br1; git merge br2; git checkout br2; git merge br1 make GEF的合并,添加到br1 )然后立即生成HFE的合并,添加到br2 )。 We can continue committing to both branches, but eventually, when we go to merge again, we have a problem picking a merge base, because both E and F are "best candidates". 我们可以继续承诺两个分支,但最终,当我们再次合并时,我们在选择合并基础时遇到问题,因为E F都是“最佳候选者”。

Usually, even this "just works", but sometimes criss-cross merges create issues that Git tries to handle in a fancy way using its default "recursive" merge strategy. 通常,即使这只是“正常工作”,但有时纵横交错合并会产生Git尝试使用其默认的“递归”合并策略以奇特的方式处理的问题。 In these (rare) cases you can see some weird-looking merge conflicts, especially if you set merge.conflictstyle = diff3 (which I normally recommend: it shows you the merge base version in conflicted merges). 在这些(罕见的)情况下,您可以看到一些奇怪的合并冲突,特别是如果您设置merge.conflictstyle = diff3 (我通常建议:它显示冲突合并中的合并基础版本)。

When does your merge driver run? 你的合并驱动程序何时运行?

Now that we have defined the merge base and seen the way hashes identify objects (including files), we can now answer the original question. 现在我们已经定义了合并基础,并且看到哈希识别对象(包括文件)的方式,我们现在可以回答原始问题。

When you run git merge branch-name , Git: 当你运行git merge branch-name ,Git:

  1. Identifies the current commit, aka HEAD . 标识当前提交,即HEAD This is also called the local or --ours commit. 这也称为本地--ours提交。
  2. identifies the other commit, the one you gave via branch-name . 标识另一个提交,即通过branch-name提供的提交。 That's the tip commit of the other branch, and is variously called the other, --theirs , or sometimes remote commit ("remote" is a very poor name since Git uses that term for other purposes too). 这是另一个分支的提示,并且被不同地称为另一个, - --theirs ,或者有时远程提交(“远程”是一个非常糟糕的名称,因为Git也将该术语用于其他目的)。
  3. Identifies the merge base. 标识合并基础。 Let's call this commit "base". 我们称这个提交为“基础”。 The letter B is also good but with a merge driver, %A and %B refer to the --ours and --theirs versions respectively, with %O referring to the base. 字母B也很好,但有一个合并驱动程序, %A%B表示--ours和 - --theirs版本, %O指的是基数。
  4. Effectively, runs two separate git diff commands: git diff base ours and git diff base theirs . 实际上,运行两个单独的git diff命令: git diff base ours git diff base theirs

These two diffs tell Git "what happened". 这两个差异告诉Git“发生了什么”。 Git's goal , remember, is to combine two sets of changes : "what we did in ours" and "what they did in theirs". 记住,Git的目标是结合两组变化 :“我们在我们的工作中做了什么”和“他们在他们的工作中做了什么”。 That's what the two git diffs show: "base vs ours" is what we did, and "base vs theirs" is what they did. 这就是两个git diffs显示的结果:“base vs our”就是我们所做的,而“base vs theirs”就是他们所做的。 (This is also how Git discovers if any files were added, deleted, and/or renamed, in base-to-ours and/or base-to-theirs—but this is an unnecessary complication right now, which we will ignore.) (这也是Git如果发现任何文件被添加,删除和/或重命名的方式,从基础到我们和/或基础到他们的 - 但这是一个不必要的复杂现在,我们将忽略。)

It's the actual mechanics of combining these changes that invokes merge drivers, or—as in our problem cases—doesn't. 这是组合这些变化的实际机制,它们调用合并驱动程序,或者 - 在我们的问题情况下 - 不是。

Remember that Git has every object catalogued by its hash ID. 请记住,Git的每个对象都由其哈希ID编目。 Each ID is unique based on the object's contents. 根据对象的内容,每个ID都是唯一的。 This means it can instantly tell whether any two files are 100% identical: they are exactly the same if and only if they have the same hash. 这意味着它可以立即判断任何两个文件是否100%相同:当且仅当它们具有相同的散列时,它们才完全相同。

This means that if, in base-vs-ours or base-vs-theirs, the two files have the same hashes, then either we made no changes, or they made no changes. 这意味着如果在base-vs-ours或base-vs-theirs中,这两个文件具有相同的哈希值,那么要么我们没有做任何更改,要么他们没有做任何更改。 If we made no changes and they made changes, why then, obviously the result of combining these changes is their file . 如果我们没有进行任何更改并且他们进行了更改,那么为什么显然结合这些更改的结果就是他们的文件 Or, if they made no changes and we made changes, the result is our file. 或者,如果他们没有做任何更改并且我们进行了更改,结果就是我们的文件。

Similarly, if ours and theirs have the same hash, then we both made the same changes. 同样,如果我们和他们的哈希相同 ,那么我们都做了相同的更改。 In this case, the result of combining the changes is either file—they're the same, so it won't even matter which one Git picks. 在这种情况下,组合更改的结果是文件 - 它们是相同的,所以Git选择哪一个甚至都不重要。

Hence, for all of these cases, Git simply picks whichever new file has a different hash (if any) from the base version. 因此,对于所有这些情况,Git只选择基本版本中具有不同散列(如果有)的文件。 That's the merge result, and there is no merge conflict, and Git is done merging that file. 这是合并结果,没有合并冲突,Git完成了合并该文件。 It never runs your merge driver because there is clearly no need. 它永远不会运行您的合并驱动程序,因为显然没有必要。

Only if all three files have three different hashes does Git have to do a real three-way merge. 只有当所有三个文件都有三个不同的哈希值时,Git才能进行真正的三向合并。 This is when it will run your custom merge driver, if you have defined one. 这是它将运行您的自定义合并驱动程序,如果您已定义一个。

There is a way around this, but it is not for the faint of heart. 有一种解决方法,但它不适合胆小的人。 Git offers not just custom merge drivers , but also custom merge strategies . Git不仅提供自定义合并驱动程序 ,还提供自定义合并策略 There are four built-in merge strategies , all selected via the -s option: -s ours , -s recursive , -s resolve , and -s octopus . 有四种内置合并策略 ,都通过-s选项选择: -s ours-s recursive-s resolve-s octopus You can, however, use -s custom-strategy to invoke your own. 但是,您可以使用-s custom-strategy来调用自己-s custom-strategy

The problem is that to write a merge strategy, you must identify the merge base(s), do any recursive merging you want (a la -s recursive ) in the case of ambiguous merge bases, run the two git diff s, figure out file add/delete/rename operations, and then run your various drivers. 问题是要编写合并策略, 必须识别合并基础,在模糊合并基础的情况下做任何你想要的递归合并(la -s recursive ),运行两个git diff ,弄清楚文件添加/删除/重命名操作,然后运行您的各种驱动程序。 Because this takes over the whole megillah , you can do whatever you want—but you must do quite a lot. 因为这会接管整个megillah ,你可以做任何你想做的事 - 但你必须做很多事情。 As far as I know there is no canned solution using this technique. 据我所知,没有使用这种技术的固定解决方案。

tl;dr: I tried to repeat what you described and it seems to work. tl; dr:我试着重复你描述的内容,似乎有效。 There were 2 changes compared to yours version but without them I have merge failed as well (because driver basically failed to run) 与您的版本相比有2个更改,但没有它们我也合并失败(因为驱动程序基本上无法运行)

I have tried this: 我试过这个:

Create a merge driver $HOME/bin/errorout.bat : 创建合并驱动程序$HOME/bin/errorout.bat

exit 1

Create a section for the merge type 为合并类型创建一个部分

[merge "errorout"]
   name = errorout
   driver = ~/bin/errorout.bat %A %O %B

Create the .gitattributes file: 创建.gitattributes文件:

*.txt merge=errorout

After that, error is reported as I think you want it to be reported: 之后,报告错误,因为我认为您希望报告错误:

 $ git merge a

 C:\...>exit 1
 Auto-merging f.txt
 CONFLICT (content): Merge conflict in f.txt
 Automatic merge failed; fix conflicts and then commit the result.

I have git version 2.11.0.rc1.windows.1. 我有git版本2.11.0.rc1.windows.1。 I was not able to make the complicated command as you specified run successfully, it was reporting some syntax errors. 我无法按照您指定的运行成功执行复杂命令,它报告了一些语法错误。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM