简体   繁体   English

合并来自 git 的冲突还原 - 我应该接受当前更改还是传入,为什么?

[英]Merge conflicts from git revert - Should I accept current change or incoming and why?

I have commits like so - A <- B <- C <- D <- E <- Head我有这样的承诺 - A <- B <- C <- D <- E <- Head

I'm using git revert --no-commit [git hash] to undo specific commits in between commits I want to keep.我正在使用git revert --no-commit [git hash]来撤消我想保留的提交之间的特定提交。 Say I want to revert D and B.假设我想还原 D 和 B。

Based on this post , the right way to revert is to start with the most recent commit you want to revert - Eg,根据这篇文章,正确的还原方法是从您要还原的最新提交开始 - 例如,

git revert --no-commit D
git revert --no-commit B
git commit

I'm getting a merge conflict and I'm not sure whether I should accept the current change or incoming change since this is essentially going backwards.我遇到了合并冲突,我不确定是应该接受当前更改还是传入更改,因为这实际上是倒退。

TL;DR长话短说

In general, you're going to have to think about the result.通常,您将不得不考虑结果。 You don't want to blindly accept "ours" as that will keep the commit you're trying to undo.您不想盲目地接受“我们的”,因为这会保留您试图撤消的提交。 You don't want to blindly take "theirs" as that almost certainly will eradicate one of, or part of, the other commits you wanted to keep .您不想盲目接受“他们的”,因为这几乎肯定会消除您想要保留其他提交中的一个或部分。 Overall, you might generally favor "theirs"—but thinking will be required.总的来说,您可能通常更喜欢“他们的”——但需要思考。 To see why, read on.要了解原因,请继续阅读。

Long长的

This is a small point, not directly relevant to your question and its answer, but worth mentioning: Git, internally, works backwards (because it must).这是一个小问题,与您的问题及其答案没有直接关系,但值得一提:Git,在内部,向后工作(因为它必须)。 1 Hence commits link backwards rather than forwards. 1因此向后而不是向前提交链接。 The actual link, from a later commit to an earlier one, is part of the later commit .从后面的提交到前面的提交的实际链接是后面提交的一部分 So your drawing would be more accurate like this:所以你的绘图会像这样更准确:

A <-B <-C <-D <-E   <-- main (HEAD)

(assuming you're on branch main , so that the name main selects commit E ). (假设您在分支main上,因此名称main选择提交E )。 But I usually get lazy about this and draw connecting lines, because it's easier and because the arrow fonts with diagonal arrows don't come out very well, while \ and / for slanting connecting lines work fine.但是我通常对此很懒惰并绘制连接线,因为它更容易并且因为带有对角线箭头的箭头 fonts 不是很好,而\/用于倾斜连接线工作正常。

In any case, the reason to do the revert "backwards" is that if we want to undo the effect of commit E , and run git revert E to make commit Ǝ :无论如何,“向后”还原的原因是,如果我们想撤消提交E的影响,并运行git revert E以进行提交Ǝ

A--B--C--D--E--Ǝ   <-- main (HEAD)

the resulting source snapshot , in commit Ǝ , will exactly match the source snapshot in commit D .生成的源快照,在提交Ǝ中,将与提交D中的源快照完全匹配。 That means we can now run git revert D and get a commit that "undoes" the effect of D , too, without ever seeing any merge conflicts.这意味着我们现在可以运行git revert D并获得“撤消” D效果的提交,也不会看到任何合并冲突。 The resulting snapshot matches that in C , making it trivial to revert C , resulting in a snapshot that matches B , and so on.生成的快照与C中的快照相匹配,这使得还原C变得微不足道,从而生成与B匹配的快照,依此类推。

In other words, by reverting in reverse order, we make sure we never have any conflicts.换句话说,通过以相反的顺序恢复,我们确保我们永远不会有任何冲突。 With no conflicts , our job is easier.没有冲突,我们的工作就容易多了。

If we're going to pick and choose specific commits to revert, this strategy of avoiding conflicts falls apart, and there may be no strong reason to revert in reverse order.如果我们要挑选特定的提交来恢复,这种避免冲突的策略就会失败,并且可能没有充分的理由以相反的顺序恢复。 Using reverse order might still be good—if it results in fewer conflicts, for instance—or it might be neutral or even bad (if it results in more/worse conflicts, though this is unlikely in most realistic scenarios).使用倒序可能仍然是好的——例如,如果它导致更少的冲突——或者它可能是中性的甚至是坏的(如果它导致更多/更严重的冲突,尽管这在大多数现实场景中不太可能)。

With that out of the way, let's get to your question... well, almost to your question.有了这个,让我们开始你的问题......好吧,几乎是你的问题。 Both cherry-pick and revert are implemented as a three-way merge operation. cherry-pick 和 revert 都是作为三向合并操作实现的。 To understand this properly, we need to look at how Git does a three-way merge in the first place, and why it works (and when it works, and what a conflict means).要正确理解这一点,我们需要首先了解 Git 如何进行三向合并,以及它为何有效(以及何时有效,以及冲突意味着什么)。


1 The reason that this is necessary is that no part of any commit can ever be changed, not even by Git itself. 1这是必要的原因是任何提交的任何部分都不能更改,即使是 Git 本身也是如此。 Since the earlier commit is set in stone once it's made, there's no way to reach back into it and make it link to the later one.由于较早的提交一旦完成就一成不变,因此无法返回并使其链接到较晚的提交。


A standard git merge标准git merge

Our usual simple merge case looks like this:我们通常的简单合并案例如下所示:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

Here we have two branches that share commits up through and including commit H , but then diverge.在这里,我们有两个分支共享提交,包括提交H ,但随后发生分歧。 Commits I and J are only on branch1 , while KL are only on branch2 for now.提交IJ仅在branch1,而KL目前仅在branch2上。

We know that each commit holds a full snapshot—not a set of changes, but a snapshot—with the files compressed and de-duplicated and otherwise Git-ified.我们知道每个提交都包含一个完整的快照——不是一组更改,而是一个快照——其中的文件经过压缩和去重,并以其他方式 Git 化。 But each commit represents some change: by comparing the snapshot in H to that in I , for instance, we can see that whoever made commit I fixed the spelling of a word in the README file, on line 17, for instance.但是每次提交都代表了一些变化:例如,通过将H中的快照与I中的快照进行比较,我们可以看到无论是谁提交的, I都修复了README文件中单词的拼写,例如第 17 行。

All of this means that to see changes , Git always has to compare two commits .所有这一切意味着要查看更改,Git 始终必须比较两个提交 2 Given this reality, it's easy to see that Git can figure out what we changed on branch1 by comparing the best shared commit, commit H , to our last commit, commit J . 2鉴于这一现实,很容易看出branch1可以通过比较最佳共享提交H与我们最后一次提交J来找出我们在 branch1 上更改的内容。 Whatever files are different here, with whatever changes we made, those are our changes.无论这里的文件有什么不同,无论我们做了什么更改,这些都是我们的更改。

Meanwhile, the goal of a merge is to combine changes .同时,合并的目标是合并更改 So Git should run this diff—this comparison of two commits—to see our changes, but also should run a similar diff to see their changes.所以 Git 应该运行这个 diff(两次提交的比较)来查看我们的更改,但也应该运行一个类似的 diff 来查看它们的更改。 To see what they changed, Git should start from the same best shared commit H and diff that against their last commit L :要查看他们更改了什么,Git 应该从相同的最佳共享提交H和与他们上次提交L的差异开始:

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

Git will now combine these two sets of changes: if we changed the README file and they didn't, that means use our version of the README file . Git 现在将合并这两组更改:如果我们更改了README文件而他们没有,这意味着使用我们的README文件版本 If they changed some file and we didn't, that means use their version of that file .如果他们更改了一些文件而我们没有,那就意味着使用他们的那个文件版本 If we both touched the same file, Git has to figure out how to combine those changes, and if nobody touched some file—if all three versions match —Git can just take any of those three versions.如果我们都触及同一个文件,Git 必须弄清楚如何组合这些更改,如果没有人触及某个文件——如果所有三个版本都匹配——Git 可以只采用这三个版本中的任何一个。

These give Git a bunch of short-cuts.这些给了 Git 一堆捷径。 The slow and simple way to combine our changes is to extract all the files from H itself, apply our and their changes where they don't conflict, and apply the conflicting changes with conflict markers where they do conflict.合并我们的更改的缓慢而简单的方法是从H本身提取所有文件,在冲突的地方应用我们的和他们的更改,在冲突的地方应用带有冲突标记的冲突更改。 What Git really does has this same effect. Git 真正做的事情具有相同的效果。 If there aren't any conflicts, the resulting files are all ready to go into a new merge commit M :如果没有任何冲突,生成的文件都准备好 go 到新的合并提交M中:

          I--J
         /    \
...--G--H      M   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

The new commit becomes the last commit for branch1 .新提交成为branch1的最后一次提交。 It links back to commit J , the way any new commit would, but it also links back to commit L , the commit that is still currently the last commit of branch2 .它链接回提交J ,就像任何新提交的方式一样,但它链接回提交L ,该提交目前仍然是branch2的最后一次提交。

Now all the commits are on branch1 (including the new one).现在所有的提交都在branch1 (包括新的)。 Commits KL , which used to be only on branch2 , are now on branch1 as well.以前只在branch2上的提交KL现在也在branch1上。 This means that in a future merge, the best shared commit is going to be commit L , rather than commit H .这意味着在未来的合并中,最好的共享提交将是提交L ,而不是提交H We won't have to repeat the same merge work.我们将不必重复相同的合并工作。

Note that commit M contains the final merged results: a simple snapshot of all files, with the correctly-merged contents.请注意,提交M包含最终的合并结果:所有文件的简单快照,以及正确合并的内容。 Commit M is special in only one way: instead of one parent J , it has two parents, J and L .提交M只有一个方面是特殊的:它有两个JL而不是一个J

If there are conflicts, though, Git makes you—the programmer—fix them.但是,如果存在冲突,Git 会让您(程序员)修复它们。 You edit the files in your working tree, and/or access the three input copies that Git had—from commits H , J , and L respectively—and combine the files to produce the correct result.您编辑工作树中的文件,和/或访问 Git 拥有的三个输入副本(分别来自提交HJL ),然后组合文件以产生正确的结果。 Whatever that correct result is, you run git add to put that into the future snapshot.无论正确结果是什么,都可以运行git add将其放入未来的快照中。 When you are done with this, you run:完成此操作后,运行:

git merge --continue

or:或者:

git commit

( merge --continue just makes sure there's a merge to finish, then runs git commit for you, so the effect is the same). merge --continue只是确保有一个合并要完成,然后为您运行git commit ,所以效果是一样的)。 This makes commit M , with the snapshot you provided when you resolved all the conflicts.这使得提交M ,使用您在解决所有冲突时提供的快照。 Note that in the end, there's nothing different about a resolved-conflict merge vs a Git-made, no-conflict merge: it's still just a snapshot of files.请注意,最终,解决冲突的合并与 Git 制作的无冲突合并没有什么不同:它仍然只是文件的快照。 The only thing special about this conflicted merge is that Git had to stop and get your help to come up with that snapshot.此冲突合并的唯一特殊之处在于 Git 必须停下来并寻求您的帮助才能得出该快照。


2 Git can also compare one commit's snapshot to some set of ordinary files stored outside of any commit, or two sets of files both of which are outside commits, or whatever. 2 Git 还可以将一个提交的快照与存储在任何提交之外的一组普通文件或两组文件(均在提交之外)或其他任何内容进行比较。 But mostly we'll be working with files-in-commits, here.但大多数情况下,我们将在这里处理提交中的文件。


Copying the effect of a commit with cherry-pick使用 cherry-pick 复制提交的效果

We now take a side trip through the cherry-pick command, whose goal is to copy the changes of a commit (and the commit message) to some different commit (with different hash ID, often on a different branch):我们现在通过 cherry-pick 命令进行旁路旅行,其目标是将提交(和提交消息)的更改复制到一些不同的提交(具有不同的 hash ID,通常在不同的分支上):

        (the cherry)
              |
              v
...--o--o--P--C--o--...   <-- somebranch
      \
       E--F--G--H   <-- our-branch (HEAD)

Here, we are on some commit with some hash H , at the tip of our branch, and are about to do some work when we realize: Hey, I saw Bob fix this bug yesterday / last-week / whenever .在这里,我们在我们分支的顶端进行了一些 hash H的提交,并且当我们意识到:嘿,我看到 Bob 在昨天/上周/无论什么时候修复了这个错误 We realize that we don't have to do any work: we can just copy Bob's fix, in a "cherry" commit C .我们意识到我们不需要做任何工作:我们可以在“cherry”提交C中复制 Bob 的修复。 So we run:所以我们运行:

git cherry-pick <hash-of-C>

For Git to do its job, Git has to compare the parent of C , commit P , to commit C .为了让 Git 完成它的工作,Git 必须比较C的父级,提交P ,提交C That's a job for git diff of course.这当然是git diff的工作。 So Git runs git diff (with the usual --find-renames and so on) to see what Bob changed.因此 Git 运行git diff (使用通常的--find-renames等)来查看 Bob 更改了什么。

Now, Git needs to apply that change to our commit H .现在, Git 需要将该更改应用于我们的提交H But: what if the file(s) that need fixing, in commit H , have a bunch of unrelated changes that skew the line numbers?但是:如果需要修复的文件在提交H中有一堆不相关的更改会扭曲行号怎么办? Git needs to find where those changes moved to . Git 需要找到这些更改移动到的位置

There are a lot of ways to do that, but there's one way that works pretty well every time: Git can run a git diff to compare the snapshot in P —the parent of our cherry—to the snapshot in our commit H .有很多方法可以做到这一点,但有一种方法每次都能很好地工作:Git 可以运行git diff比较P中的快照——我们 cherry 的父级——与我们提交H中的快照。 That will find any differences in the files that are different between H and the PC pair, including long stretches of inserted or deleted code that move the places where Bob's fix needs to go.这将发现HPC对之间不同的文件中的任何差异,包括插入或删除的长段代码,这些代码将 Bob 的修复需要的位置移动到 go。

This is of course going to turn up a bunch of irrelevant changes too, where P -vs- H is different just because they're on different lines of development.这当然也会带来一堆不相关的变化,其中P -vs- H不同只是因为它们处于不同的开发路线上。 We started from some shared (but uninteresting) commit o ;我们从一些共享的(但无趣的)提交o they made a bunch of changes—and commits—leading to P ;他们做了一堆改变——并提交——导致P we made a bunch of changes and commits, E and F and G , leading to our commit H .我们做了一堆更改和提交, EFG ,导致我们的提交H But: so what?但是:那又怎样? Given that git merge is going to take our files where there's no conflict at all, we'll just get our files from H .鉴于git merge将把我们的文件带到根本没有冲突的地方,我们将只从H获取我们的文件。 And, given that, where both "we" and "they" changed some files, Git will "keep our changes" from P to H , then add their changes from P to C , that will pick up Bob's changes.并且,假设“我们”和“他们”都更改了一些文件,Git 将“保留我们的更改”从PH ,然后将他们的更改P添加到C ,这将获取 Bob 的更改。

So this is the key realization: if we run the merge machinery, the only place we'll get conflicts is where Bob's changes don't fit in. Therefore, we do run the merge machinery:所以这是关键的认识:如果我们运行合并机制,我们唯一会遇到冲突的地方就是 Bob 的更改不适合的地方。因此,我们确实运行合并机制:

git diff --find-renames <hash-of-P> <hash-of-H>   # what we changed
git diff --find-renames <hash-of-P> <hash-of-C>   # what Bob changed

and then we have Git combine these changes, applying them to the "common" or "merge base" commit P .然后我们有 Git 组合这些更改,将它们应用于“公共”或“合并基础”提交P The fact that it isn't common to both branches does not matter.两个分支都不通用的事实并不重要。 We get the right result , which is all that does matter.我们得到正确的结果,这才最重要的。

When we're done "combining" these changes (getting our own files back, for files that Bob didn't touch, and applying Bob's changes, for files that Bob did touch), we have Git make a new commit on its own, if all went well.当我们完成“组合”这些更改后(取回我们自己的文件,对于 Bob 未触及的文件,并应用 Bob 的更改,对于 Bob 触及的文件),我们有 Git 自己进行新的提交,如果一切顺利的话。 This new commit isn't a merge commit though.不过,这个新提交不是合并提交。 It's just a regular, ordinary, everyday commit, with the usual parent:这只是一个常规的、普通的、日常的提交,与通常的父母:

...--o--o--P--C--o--...   <-- somebranch
      \
       E--F--G--H--I   <-- our-branch (HEAD)

The git diff from H to I introduces the same changes as the git diff from P to C .HIgit diff引入了与从PCgit diff相同的变化 The line numbers might be moved about if necessary, and if so, the moving-about happened automatically using the merge machinery.如有必要,行号可能会移动,如果是这样,移动会使用合并机制自动发生。 Also, new commit I re-uses the commit message from commit C (though we can modify it with git cherry-pick --edit , for instance).此外,新提交I重新使用来自提交C提交消息(尽管我们可以使用git cherry-pick --edit修改它,例如)。

What if there are conflicts?如果有冲突怎么办? Well, think about this: if there is a conflict in some file F , that means that Bob's fix to F affects some lines in that file that are different in their parent P and in our commit H .好吧,想一想:如果某个文件F中存在冲突,这意味着 Bob 对F的修复会影响该文件中的某些行,这些行在其父P和我们的提交H中是不同的。 Why are these lines different?为什么这些线不同? Either we don't have something we might need —maybe there's some commit before C that has some key setup code we need—or there's something we do have, that we don't want to lose .要么我们没有我们可能需要的东西——也许在C之前有一些提交有一些我们需要的关键设置代码——或者我们确实有一些我们不想丢失的东西。 So it's rarely correct to just accept ours, because then we don't get Bob's fix to the file.所以只接受我们的很少是正确的,因为这样我们就得不到 Bob 对文件的修复 But it's rarely correct to just accept theirs either, because then we're missing something , or we lose something we had .但是仅仅接受他们的也很少是正确的,因为那样我们就会失去一些东西,或者我们会失去一些我们拥有的东西。

Reverting is backwards cherry-picking恢复是向后的樱桃采摘

Suppose instead of this:假设不是这个:

...--o--o--P--C--o--...   <-- somebranch
      \
       E--F--G--H   <-- our-branch (HEAD)

we have this:我们有这个:

...--o--o--P--C--D--...   <-- somebranch
                  \
                   E--F--G--H   <-- our-branch (HEAD)

Commit C , perhaps still made by Bob, has a bug in it, and the way to get rid of the bug is to undo the entire change from commit C .提交C可能仍然是 Bob 所做的,其中有一个错误,消除错误的方法是撤消提交C的整个更改。

What we'd like to do, in effect, is diff C vs P —the same diff we did earlier for our cherry-pick, but backwards.实际上,我们想要做的是 diff C vs P — 与我们之前为我们的 cherry-pick 所做的相同的 diff,但向后。 Now, instead of add some lines here to add some feature (that's actually a bug), we get remove those same lines here (which removes the bug).现在,我们不再在此处添加一些行来添加一些功能(这实际上是一个错误),而是在此处删除相同的行(这会删除错误)。

We now want Git to apply this "backwards diff" to our commit H .我们现在希望 Git 将这个“向后差异”应用于我们的提交H But, as before, maybe the line numbers are off.但是,和以前一样,行号可能已关闭。 If you suspect that the merge machinery is an answer here, you're right.如果您怀疑合并机制是这里的答案,那么您是对的。

What we do is a simple trick: we pick commit C as the "parent", or the fake merge base.我们所做的是一个简单的技巧:我们选择提交C作为“父”,或假合并基础。 Commit H , our current commit, is the --ours or HEAD commit as always, and commit P , the parent of commit C , is the other or --theirs commit.提交H ,我们当前的提交,一如既往地是--oursHEAD提交,提交P ,提交C的父级,是另一个或--theirs提交。 We run the same two diffs, but with slightly different hash IDs this time:我们运行相同的两个差异,但这次 hash ID 略有不同:

git diff --find-renames <hash-of-C> <hash-of-H>   # what we changed
git diff --find-renames <hash-of-C> <hash-of-P>   # "undo Bob's changes"

and we have the merge machinery combine these, as before.和以前一样,我们有合并机制将这些结合起来。 This time the merge base is commit C , the commit we're "undoing".这次合并基础是提交C ,我们正在“撤消”的提交。

As with any merge, including that from cherry-pick, any conflicts here have to be considered carefully.对于任何合并,包括来自 cherry-pick 的合并,这里的任何冲突都必须仔细考虑。 "Their" change is something that backs out commit C , while "our" change is something that's different between P —what they are starting with when they back this out—and our commit H . “他们的”更改是回退提交C的内容,而“我们的”更改是P之间的不同内容——他们在回退时从什么开始——和我们的提交H There is no royal short-cut here, no -X ours or -X theirs , that will always be right.这里没有皇家捷径,没有-X ours-X theirs ,那永远是对的。 You'll just have to think about this.你只需要考虑一下。

Be careful with -n : consider not using it小心-n :考虑不使用它

If you're getting conflicts when using git cherry-pick or git revert , you must resolve them.如果您在使用git cherry-pickgit revert时遇到冲突,您必须解决它们。 If you're not using -n , you resolve them and then commit .如果您使用-n ,则解决它们然后提交 If you are doing this with multiple commits, your next operation might get a conflict too.如果您通过多次提交执行此操作,则您的下一个操作也可能会发生冲突。

If you committed, the next cherry-pick or revert starts with your commit as the HEAD version.如果你提交了,下一个 cherry-pick 或 revert 以你的提交作为HEAD版本开始。 If you got something wrong in any of the intermediate versions, that alone might cause a conflict;如果您在任何中间版本中出现问题,仅此一项就可能导致冲突; or, there might be a conflict here that would arise no matter what.或者,这里可能会发生无论如何都会发生的冲突。 As long as you resolve this one and commit too, you leave a trail.只要你解决了这个问题并且也做出了承诺,你就会留下痕迹。 You can go back and look at each individual cherry-pick or revert and see if you did it correctly, or not.您可以 go 返回并查看每个单独的 cherry-pick 或 revert ,看看您是否做对了。

Now, you can use git cherry-pick -n or git revert -n to skip the commit at the end .现在,您可以使用git cherry-pick -ngit revert -n跳过最后的提交 If you do that, the next cherry-pick or revert uses your working tree files as if they were the HEAD -commit versions.如果您这样做,下一个cherry-pick 或 revert 将使用您的工作树文件,就好像它们是HEAD提交版本一样。 This works the same way as before, but this time, you do not leave a trail .这与以前的工作方式相同,但这次您不留下踪迹 If something goes wrong, you can't look back at your previous work and see where it went wrong.如果出现问题,您无法回顾以前的工作并查看哪里出了问题。

If you leave off the -n , you'll get a whole series of commits:如果你离开-n ,你会得到一系列的提交:

A--B--C--D--E--Ↄ   <-- main (HEAD)

for instance, after reverting C .例如,在还原C之后。 If you then go to revert A and it all goes well, you might get:如果你然后 go 恢复A并且一切顺利,你可能会得到:

A--B--C--D--E--Ↄ--∀   <-- main (HEAD)

If you now say "that's nice but I don't really want in the mix", it's easy to get rid of it while keeping its effect , using git rebase -i or git reset --soft .如果你现在说“这很好,但我真的不想混合使用 ”,很容易在保持其效果的同时使用git rebase -igit reset --soft摆脱它。 For instance, a git reset --soft with the hash ID of commit E results in:例如,带有提交E的 hash ID 的git reset --soft导致:

              Ↄ--∀   ???
             /
A--B--C--D--E   <-- main (HEAD)

but leaves Git's index and your working tree full of the files that make up the contents of commit .留下 Git 的索引和你的工作树充满了构成提交内容的文件。 So you can now run git commit and get a new commit:所以你现在可以运行git commit并获得一个新的提交:

              Ↄ--∀   ???
             /
A--B--C--D--E--Ↄ∀   <-- main (HEAD)

where Ↄ∀ is the effect of combining (ie, squashing) and .其中Ↄ∀是组合(即挤压) 的效果。

If nothing went wrong, you will have to do this squashing, but if something did go wrong, you don't have to start from scratch.如果没有出错,您将不得不执行此压缩操作,但如果go出错,您不必从头开始。

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

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