简体   繁体   English

Git-恢复与一个分支的历史合并

[英]Git - Revert merge with history of one branch

I have the great job to clean up a fu**ed up git repository. 我有出色的工作来清理功能强大的git存储库。 In the past someone merged the whole linux kernel src into the repository (with all the 650k commits). 过去有人将整个linux内核src合并到存储库中(包含所有650k提交)。 I know the commit id from the merge and also from the parent. 我知道合并和父提交的提交ID。 Of course there were changes in the time between merging linux with the masterbranch, so at the moment the tree looks similar to this 当然,在将linux与masterbranch合并之间的时间有所变化,因此此刻树看起来与此类似。

-x-x-x-x-LinuxMerge-x-x-x-x-x-x-x-x-x-today

What I want is to revert the LinuxMerge commit incl. 我想要的是还原LinuxMerge commit incl。 the history of this. 历史。 Is this possibly and how? 这可能吗?如何?

You can undo this by rebasing. 您可以通过重新定标来撤销此操作。

If you start out from this... 如果您从这里开始...

-x-x-x-x-LinuxMerge-x-x-x-x-x-x-x-x-x-today

... then you probably are talking about that, instead: ...那么您可能正在谈论的是:

-x-x-x-x-x-x-x-x-x-x-x-x-x-today
        /
-linus-/

Let's label some more commits: 让我们标记更多的提交:

-x-x-x-prev-merg-post-x-x-x-x-x-x-x-today
             /
     -linus-/

So, you want to glue prev and post together, and throw away merg . 因此,您想将prevpost粘合在一起,并丢弃merg The command for this is: 该命令是:

git rebase merg today --onto prev

(Note that in the command, we mention merg , not post ; this is the typical "+-1" issue with declaring commit ranges in git ). (请注意,在命令中,我们提到merg ,而不是post ;这是在git声明提交范围的典型“ + -1”问题)。

This rebase command will add a new line of commits and change the today branch to point at the new tail: 此rebase命令将添加新的提交行,并更改today分支以指向新的尾部:

          post'-y-y-y-y-y-y-y-today'
         /
-x-x-x-prev-merg-post-x-x-x-x-x-x-x-today
             /
     -linus-/

And if you just ignore the older stuff, this flattens out to: 而且,如果您只是忽略了较旧的内容,则可以扩展为:

-x-x-x-prev-post'-y-y-y-y-y-y-y-today'

The rebase will also change the today branch to point at the commit labeled today' in this ASCII picture. 重新设置还将更改“ today分支,以指向此ASCII图片中标记为“ today'的提交。

Note that post' and the y commits (as well as today' ) will all have different hashes than the originals, they are not the "same" commits. 请注意, post'y提交(以及today' )都将具有与原始提交不同的哈希值,它们不是“相同”的提交。

If no other tags or branches point to the history leading up to linus , then those commits and related objects will be purged eventually by the git garbage collection (which you could force with git gc to make sure). 如果没有其他标签或分支指向导致linus的历史记录,那么最终将通过git垃圾收集清除这些提交和相关对象(您可以使用git gc强制确保此操作)。

I think some confusion is raised with this question, because you phrase it as wanting to "revert" the mistake - and "revert" means something specific to git. 我认为这个问题引起了一些困惑,因为您将其表述为想“恢复”错误-“恢复”意味着特定于git的东西。 I know what you mean isn't what git means by that word, because with git's meaning a phrase like "revert history" isn't a thing. 我知道您的意思不是git这个词的意思,因为git的意思不是“恢复历史记录”之类的词。

Because you want to undo the change entirely, a history rewrite is the first step. 因为您想完全撤消更改,所以第一步是重写历史记录。 AnoE's answer shows one way to do this, assuming that there is just one ref from which the bad merge is reachable, and that there are no merge commits "between" that ref and the bad merge. AnoE的答案显示了执行此操作的一种方法,假设只有一个ref可以实现错误合并,并且没有ref与错误合并之间的合并提交。

In the event there are multiple refs, you'd need to do something more. 如果有多个裁判,则您需要做更多的事情。 For example if you had 例如,如果您有

x -- x -- x --- ML -- x -- A -- x <--(master)
               /            \
(linux history)              o <--(branch_one)

completing the rebase would give you 完成变基会给你

            x' -- A' -- x' <--(master)
           /
x -- x -- x --- ML -- x -- A -- o <--(branch_one)
               /
(linux history)

You'd then need to transplant the o commit, with something like 然后,您需要移植o commit,例如

git rebase --onto A' A branch_one

(replacing A and A' with either the commit ID or some other expression that names the appropriate commit). (用提交ID或其他命名适当提交的表达式替换AA' )。

If there are merges that need to be rewritten, then you have a bigger issue. 如果存在需要重写的合并,则问题更大。 The rebase command will try to write a linear history by default. 默认情况下, rebase命令将尝试写入线性历史记录。 You can tell it you want to keep the merge topology with the --preserve-merges option, but it may not work properly. 您可以使用--preserve-merges选项告诉您要保留合并拓扑,但是它可能无法正常工作。 If a merge commit had conflicts, you'll have to re-resolve it. 如果合并提交有冲突,则必须重新解决它。 Worse, if a merge commit doesn't have conflicts, but was not originally completed using the default merge result, then rebase will not recreate the merge (or any children of it) correctly. 更糟糕的是,如果合并提交没有冲突,但是最初不是使用默认合并结果完成的,那么rebase不会正确地重新创建合并(或其任何子级)。

So the only safe way to rebase, then, is in segments, manually reproducing merges as you encounter them. 因此,重新设置基准的唯一安全方法是分段处理,在遇到合并时手动重新生成合并。

Another option might be to use git filter-branch instead of rebase ; 另一种选择是使用git filter-branch而不是rebase ; but this is tricky, too. 但这也很棘手。 It's only workable if you can script the removal of anything the merge introduced. 仅当您可以编写脚本删除合并引入的所有内容时,它才可行。 For example, if the linux history is in different paths than your own work, so that you could clean up a given instance of the content by rm ing certain paths, then you could use filter-branch . 例如,如果Linux历史比你自己的工作不同的路径,这样你可以清理由内容的给定实例rm荷兰国际集团某些路径,那么你可以使用filter-branch

(Since this is an option that may or may not be viable for you, for now I won't spell out the detailed steps. The filter-branch documentation can fill in the blanks. Basically you'd use a parent-filter to bypass the merge commit (by re-parenting the following commit onto the first parent commit), plus an index-filter or tree-filter to remove the linux files from the subsequent commits.) (由于这是一个可能对您而言可行或不可行的选项,因此,我现在将不详细说明详细步骤filter-branch文档可以填补空白。基本上,您将使用parent-filter绕过合并提交(通过将以下提交重新绑定到第一个父提交上),再加上index-filtertree-filter以从后续提交中删除linux文件。)

One way or another, once you have the history cleaned up you would still have all that history in your repo's database. 一种或另一种方式是,一旦清除了历史记录,您仍将所有这些历史记录存储在存储库的数据库中。 At a minimum you need to make sure nothing references that history. 至少您需要确保没有任何内容引用该历史记录。 Then it would eventually get cleared out by gc (or you could force that to happen sooner). 然后,它将最终被gc清除(或者您可以强迫这种情况尽快发生)。

Mostly that means you have to find any refs that can reach the linux history. 通常,这意味着您必须找到可以达到linux历史记录的所有引用。 Since the rewrite moved "your" refs, this would likely comprise any refs (branches or tags) pulled in with the linux history itself. 由于重写操作移动了“您的”引用,因此这很可能包括随Linux历史记录本身引入的所有引用(分支或标签)。 So you'd just want to delete those. 因此,您只想删除它们。

There also will be reflogs that can (indirectly) reach the linux history, and gc can't remove history that's reachable in this way. 还有一些reflog可以(间接地)到达linux历史记录,并且gc无法删除以这种方式可以到达的历史记录。 Honestly at this point the easiest thing to do is probably to re-clone the repo (as a new clone should only get the current refs and their history) and replace origin with the result. 坦率地说,在这一点上,最简单的操作可能是重新克隆存储库(因为新的克隆应该只获取当前引用及其历史记录),并用结果替换origin。

If you want to repair an existing repo instead of re-cloning for whatever reason, the next step would be to wipe out reflogs (I usually just rm -r .git/logs ) and then run an aggressive gc (see the gc docs) 如果您想修复现有的仓库而不是出于任何原因而重新克隆,那么下一步将是清除reflog(我通常只是rm -r .git/logs ),然后运行一个激进的gc (请参阅gc文档)

You have a couple of options here. 您在这里有几个选择。

Option 1: Rewriting history 选项1:重写历史记录

If you are able to rewrite the history of the master branch without consequences, the quickest way to achieve what you want is to simply remove the merge commit altogether with git rebase --onto : 如果您能够重写 master分支的历史记录而不会产生任何后果,则实现所需目标的最快方法是简单地使用git rebase --onto完全删除合并提交:

git checkout master
git rebase --onto <SHA-1-of-the-linux-merge>^ <SHA-1-of-the-linux-merge>

This means: " rebase master on top of the first parent of the merge commit starting from the merge commit itself ". 这意味着:“ 从合并提交本身开始,在合并提交的第一个父节点之上重新设置master服务器 ”。 This will effectively remove the merge commit and apply all subsequent commits on top of its first parent. 这将有效地删除合并提交,并将所有后续提交应用到其第一个父级之上。 You can read more about how git rebase --onto works here . 您可以在此处阅读有关git rebase --onto如何工作的更多信息。

Option 2: Reverting the merge 选项2:还原合并

If you want to avoid rewriting history, you can always revert the "LinuxMerge" commit by using git-revert : 如果要避免重写历史记录,可以始终使用git-revert “ LinuxMerge”提交:

git revert --mainline-parent 1 --no-commit <SHA-1-of-the-linux-merge>

The --mainline-parent option tells Git which parent of the merge commit you want to revert to . --mainline-parent选项告诉Git您要将合并提交还原哪个父提交。 In this case, you want to revert to the first parent, that is the commit where the Linux kernel was merged to . 在这种情况下,您想还原为第一个父节点,即Linux内核被合并的提交。

From the documentation : 文档中

Usually you cannot revert a merge because you do not know which side of the merge should be considered the mainline. 通常,您无法还原合并,因为您不知道合并的哪一侧应被视为主线。 This option specifies the parent number (starting from 1) of the mainline and allows revert to reverse the change relative to the specified parent. 此选项指定主线的父级编号(从1开始),并允许还原以相对于指定的父级撤消更改。

Note that reverting a merge this way will cause later merges of the same branch to exclude the commits that were originally brought in by the reverted merge: 请注意,以这种方式还原合并将导致以后同一分支的合并排除由还原的合并最初带来的提交:

Reverting a merge commit declares that you will never want the tree changes brought in by the merge. 还原合并提交将声明您永远不需要合并带来的树更改。 As a result, later merges will only bring in tree changes introduced by commits that are not ancestors of the previously reverted merge. 结果,以后的合并将仅引入由不是先前还原的合并的祖先的提交引入的树更改。 This may or may not be what you want. 这可能是您想要的,也可能不是。

However, in this case it sounds like you won't be merging the Linux kernel again anytime soon. 但是,在这种情况下,听起来您很快就不会再合并Linux内核。

As for the --no-commit option, it lets you do a dry run to see whether you get any conflicts in your working directory without actually creating the commit. 至于--no-commit选项,它使您可以进行试运行以查看是否在未实际创建提交的情况下在工作目录中遇到任何冲突。

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

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