我如何修复一个破碎的git合并从错误的起源分支



我们使用的git中央存储库有两个分支,它们很久以前就分离了。有人不小心将旧分支合并到新分支中,导致一些冲突。

现在我有一个合并提交位于新分支的顶部,看起来像这样:

commit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Merge: xxxxxxxx xxxxxxxxx
Author: xxxxxxxxxxxxxxxxxxxxx
Date:   Fri Sep 12 16:04:23 2014 -0400
Merge branch 'master' of xxxxxx:xxxxxxx/xxxxxxx into working-branch
Conflicts:
Gemfile.lock
config/xxxx.rb
config/xxxx.yml
public/.xxx/xxxxxxx.xml

这里到底发生了什么?这是一次失败的合并吗?如果是这样,为什么它会出现在遥控器上?我如何知道是否有其他文件被更改?我需要尝试还原此更改还是简单地回滚这四个文件?

有人建议我在合并之前对提交进行硬重置,然后强制将其推回存储库。如果我这样做,我应该注意什么奇怪的影响?

这不是一个失败的合并,这是一个成功的合并…或者至少,从git的角度来看是"成功"。某人(我们称他为Moe,代表进行合并的人)进行了一次"git合并",结果在四个文件上发生了冲突。然后Moe告诉git,他(Moe)已经解决了git发现的冲突,创建了成功的合并提交。(也许Moe正确地解决了,也可能没有:git不知道,我们也不知道。)

创建合并提交后,Moe继续让它对你可用(它现在是"发布";假设您使用带有中央服务器的推送模型,这意味着Moe推送了合并)。这就是你如何得到它,以及"为什么它会出现在遥控器上?"的答案。(所有这些都假设您从服务器获得合并;如果你自己做了,你就是"Moe",你可以使用它,因为这是你的存储库。

让我们一次吃一部分剩下的……

"这里到底发生了什么?">

几乎可以肯定,Moe在分支working-branch上做了一个git pull remote-name master。从本质上讲,pull命令只是git fetch后面跟着git merge:从命名的远程获取,然后从命名的分支(es)中合并(到当前分支中,这是working-branch),因为它们在获取时在远程上看到。1然后他修复了冲突,或者至少告诉git他这么做了,并提交了。

"我怎么知道是否有其他文件被更改?">

合并提交是一个包含两个或多个父提交的提交。"第一个"父级是在其上进行合并的分支(上面的working-branch),其余的父级是被合并的分支(上面的master of xxx...)。

与任何提交一样,要查看"之前的某个提交"(通常是"直接父提交")与该提交之间的更改,您可以使用git diff并告诉它将之前的提交与该提交进行比较。例如,对于普通的(单亲,非合并)提交1234567,您可以这样做:

git diff 1234567^ 1234567

(添加--name-only,--name-status,--stat,或任何git diff选项,您喜欢选择如何显示差异)。如果普通提交是分支b的提示,您可以将其拼写为git diff b^ b,例如:关键是命名父(b^,1234567^或其实际的原始提交id),然后命名提交本身。

(旁注:这个^后缀是"parent"的特殊语法,它是通用gitrevisions语法的一部分。Plain表示^1,第一个父节点;^2表示第二个父节点;如果存在第三个父节点,则^3表示第三个父节点;等等......我们将在下面使用它

有一个简单的方法可以用一个命令显示这个确切的差异(以及日志消息):git show。给git show指定一个普通提交的名称或ID,它将向您显示日志消息以及与该提交的第一个和唯一父级的差异。如果您要求git show合并提交执行此操作,但是,它会做一些不同的事情:它向您显示"组合diff"。

请记住,合并提交是包含至少两个父的提交。这意味着至少要生成两个不同的:第一父级vs合并,第二父级vs合并。(事实上,每个父节点都有一个父节点,但是超过两个父节点的合并是相对罕见的,所以我们可以简单地关注"两个"的情况。)

如果您想查看哪些文件被更改,而没有任何状态或实际差异,则需要git diff --name-only。选择一个或另一个父节点,命名它,并命名合并。假设合并提交可命名为HEAD。然后:

git diff --name-only HEAD^ HEAD

将比较HEADHEAD的第一个父目录,并显示哪些文件在比较中发生了变化。

要查看从第二个父目录更改的文件,请命名第二个父目录:
git diff --name-only HEAD^2 HEAD

等等

当然,也有一种简单的方法来完成这一系列的git diff,使用git show:添加-m标志,它将与每个父级进行比较。(注意,如果您省略-m标志,git show将显示一个"组合diff",它只显示——引用手册——"从所有父文件修改的文件"。这不是你想要的:如果文件foo.txt在一个-mdiff中被修改,但在另一个-mdiff中没有被修改,它不是"从所有父母修改",git show不会显示它。)所以:

git show --name-only -m HEAD

将完成此任务(并且包含每个diff的日志消息和父消息,因此您可以分辨哪个来自哪个父消息)。

"我需要尝试恢复这个更改还是简单地回滚这四个文件?">

Andrew C已经注意到,恢复合并会给您留下一个需要稍后解决的不同问题。然而,它确实避免了"改写历史"所带来的问题。这些问题发生(可以发生)只对那些有"预重写"历史的人发生;你在评论中说:

"我根本不想做合并——这完全是个错误。">

这表明,在上面,当我谈论Moe,合并的家伙时,我实际上是在谈论。你没有从别人那里得到这个合并,你自己做的。

如果是这样的话,这实际上是一个好的的事情,因为也许只有有这个历史,这个合并,你想要删除。这意味着没有人可以看到您从历史记录中删除合并所引起的问题。

执行git reset --hard将更改存储在本地存储库中的分支名称中的分支尖端。这实际上可以"删除合并"…仅在本地存储库中。也就是说,如果历史记录当前看起来像这样:

...- o - o - * - M   <-- HEAD=working-branch
/
...- o - o - o       <-- (other ID, probably stored in FETCH_HEAD)

和你的git reset --hard HEAD^,这告诉git将working-branch从commitM(合并)重新指向标记为*的提交。这是因为git reset调整HEAD命名的分支,使其指向作为参数给出的提交。由于HEAD^表示提交*,HEAD表示提交working-branch,因此reset命令使working-branch指向提交*

(--hard只是简单地使用工作目录,以及索引/暂存区。重置索引是默认的,--hard添加了工作目录。)

CommitM仍然在您的存储库中,但没有指向它的任何内容。2因此似乎要从历史中删除,最终,它真的会被删除。

"在合并之前对提交进行硬重置,然后强制推送…">

你只需要force-push步骤,如果你已经推动了合并(或者因为别人推动了合并,你真的从"Moe"那里得到了合并)。然而,如果推送,其他人可能拥有它(比如Moe自己)。如果你强行推,那些人必须对你的改变负责。

让我们假设,为了便于描述,Larry和Curly也有Moe的合并:他们有自己的存储库副本,并且他们从远程进行了git fetch-ed合并。此外,假设他们的远程版本命名为origin

如果Larry有一个跟踪origin/working-branch的本地分支working-branch,那么:

  • 他"落后于"origin/working-branch,因此在他的本地分支上还没有提交:他所要做的就是git fetch。这将更新他的origin/working-branch,他现在将"少落后",因为origin/working-branch实际上倒退了一个。

  • 他"甚至"origin/working-branch:他必须仍然git fetch,但一旦完成,他还必须重新指向自己的working-branch,放弃合并提交。最简单的方法是输入git checkout working-branch,然后输入git reset --hard origin/working-branch

  • 他"领先"origin/working-branch:现在他要做的工作最多。他仍然必须像往常一样使用git fetch,但是之后,他必须将合并后的新提交重置到合并前的origin/working-branch上,并删除合并提交。当他的git fetch完成时,他将比以前更加领先,原因与第一个案例变得"落后"相同,但是第一个"领先"提交是不必要的合并,他必须从他的working-branch中丢弃它。

    有几种方法可以做到这一点;Git 2.0让它变得更简单;请参阅其他stackoverflow问题和答案了解更多详细信息。

科里必须做拉里必须做的事情,但如果拉里"落后",拉里就轻松了,科里可能仍然"领先",不得不相对努力地工作。


1更具体地说,git fetch的工作方式,拟人化,3是这样的:

  1. 呼叫远程。问他:"嘿,你得到了什么?"你有哪些分支和标签,它们映射到哪些提交id ?"
  2. 把名字(分支和标签,或者更一般地说,"引用")的列表拿出来,弄清楚什么要带过来,什么要忽略。
  3. 进一步交谈以找出需要哪些额外对象(如果有的话)来获得所有必要的提交、树、blobs等。把这些都拿过来。
  4. 最后,以某种方式存储生成的提交id和引用名称。

步骤4有一些特殊的魔力:当git fetchgit merge的方式运行时,id存储在FETCH_HEAD中。当以其他方式运行时,或者使用新版本的git时,它们被存储为"远程跟踪分支"(通过与远程相关的fetch =行)。(git的新版本总是更新远程跟踪分支,即使使用FETCH_HEAD,所以在这种特殊情况下,你可能会得到两个操作。)

系统最容易用更新的远程跟踪分支来考虑:你的git调用他们的git,找出他们有什么,然后更新你的远程跟踪分支。当你"git push"时,你的git也会更新这些,这是你的git在互联网电话上调用他们的git的时候。因此,您的远程跟踪分支只是"上次git检查时它们拥有什么"。

pull脚本早于此,所以它以稍微复杂的方式使用FETCH_HEAD,但它最终的工作方式与非常现代的git和单独的git fetch步骤相同:您的get获得git的分支或分支,将它们隐藏在FETCH_HEAD中,然后使用FETCH_HEAD合并。

2实际上,有一个refflog条目指向它。您可以通过git reflog或使用name@{history-specifier}语法找到这些。然而,refflogs最终会过期:它们是用来修复各种"哎呀"类型错误的,而不是用于永久存储。

3"不要把电脑拟人化,它们讨厌这样。"来源未知的

使用git reset取消合并,然后重新进行合并

步骤1)撤销合并:

git reset --hard xxxxxxx~ # xxxxxxx is the name of working branch

步骤2)再次合并

git merge xxxxxxxx/xxxxxxxx # e.g.: origin/master

From this point, isNOT建议做一个强制推动,你能做的最好的事情就是解决冲突,然后承诺。如果再次发现问题,请返回步骤1

步骤2b)(替代步骤2)

git rebase xxxxxxxx/xxxxxxxx  e.g.: origin/master

如果你做rebase,你将能够合并提交的提交或你的工作在xxxxxx:xxxxxxxx/xxxxxxxx (rebase是相同的樱桃选择你所有的本地非push提交在远程分支),这听起来很繁琐,但它将允许你有更多的控制如何合并每个原子更改

步骤3)做新的修改

即使你需要放弃来自远程分支的更改(就我个人而言,我会在这样做之前与我的同事协调),也要避免使用强制推送,相反,你可以使用checkout with path将旧内容带到当前版本,并在分支的末端再次提交(这也将允许你和你的队友清楚地看到你在做什么)。示例

git checkout xxxxxx~ . # e.g. xxxxxx could be master
git checkout xxxxxx@{2} .
git checkout XXXX4f42134 .
etc...

注意:使用git logandgit reflog在本地历史记录

中找到包含所需内容的提交步骤4)Push你的更改(不强制Push)

git push

如果你的推送再次被拒绝(由于仓库的新变化),执行git fetchA回到步骤2

假设错误的合并仍然在分支提示锁定您的repo,使其不接受任何更改

你有两个选择。1)销毁发生过这种事的历史2)恢复合并。

第一个结果是更干净的历史记录,但可能会给其他下游开发人员带来问题(他们需要"从上游重基中恢复",如git rebase帮助页面中所描述的那样)。另外,第一个技巧只有在合并提交是分支提示时才有效。

第二种通常更安全,但如果您需要重新合并分支,则会导致问题。

选项1

git checkout working-branch
git reset --hard HEAD^1
git push -f 

现在帮助你所有的同事从上游重基中恢复

选项2

git checkout working-branch
git revert -m 1
git push

关于你的问题

如果不查看更改,我们无法判断合并是否糟糕。有人进行了合并并强行执行,我们只知道这些。您可以看到使用git diff BAD_MERGE_SHA^1..BAD_MERGE_SHA

更改了哪些文件

如果您还没有推动合并,您可以删除您的repo并重新克隆它。服务器上不会更改任何内容。如果您已经推入合并,那么您可以恢复更改并推入更改以撤销合并。

最新更新