Git:从重写历史中恢复。合并冲突

时间:2014-02-20 20:19:32

标签: git

我知道,我知道,重写历史是可怕的,但是......

我不小心检查了一个大的sql文件来git我需要删除所有历史记录。

我显然没有做正确的事情,但我按照以下方式运行:

git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch dump.sql' \
  --prune-empty --tag-name-filter cat -- --all

它成功删除了文件,但导致事情处于一种奇怪的状态:

  • 我在branch_a
  • 上运行了上述命令
  • branch_b没有动过,我
  • 尝试将branch_a合并到branch_b
  • 我遇到了很多冲突,比我想要的还要多。但我修好了他们 提交。
  • 然后我在branch_a中进行了更改,然后尝试合并 回到branch_b,但是第一次遇到了所有相同的冲突, 甚至以为我已经将它们合并了。

知道如何解决这个问题吗?是否有些日期因提交导致这种奇怪的行为而被更改?

1 个答案:

答案 0 :(得分:0)

好的,根据评论,我想我现在可以正确描述问题了。

(这种情况发生在任何类型的历史记录重写中,但如果它是非共享存储库,或者它的所有用户都愿意更新,则可以在此成功使用git filter-branch 。)

您需要知道的起点是git 永远不会更改提交,不会更改git rebase,不会更改git filter-branch,甚至不会更改git commit --amend }。为了使事情看起来发生变化,这些命令的作用是基于旧的提交 new 提交,但改变了一些东西。

然后,他们移动标签,以便您看到新提交而不是旧提交。


这是一个包含两个分支和五个总提交的存储库的简单示例:

    B <- C   <-- br1
  /
A
  \
    D <- E   <-- br2

提交A是初始(根)提交,然后有两个分支结构,包括提交BC(标记为br1)和{{1 }和D(标记为E)。 (这里的每个字母代表一些大毛茸茸的SHA-1,它是&#34;提交的真实姓名&#34;例如,br2实际上可能是A。)

每个提交都指回其祖先,因此004257f...指向CB指向B。没有&#34;前进指针&#34;:A不指向A BDB都是D的后代的事实必须是计算,从终点开始并向后工作。 (这是在不更改A的情况下添加提交D的方式。)

接下来,您需要知道&#34;分支&#34;在git中有两个不同的含义:我称之为&#34;结构分支&#34;,你可以看到A是一个分支而A <- B <- C <-- br1是另一个分支。 (这两个分支在A <- B <- C <-- br2分享一个共同点,因此您可以说A仅包含br1CB仅包含br2 {1}}和E。但是那个分支是D&#34;&#34;?它真的&#34; on&#34; 两者,从某种意义上说,如果在A之前有任何提交,那么他们也会在#34;两者之间#34;有时候更清楚地谈论分支&#34;包含&# 34;提交,并且有A来显示包含给定提交的分支的标签。)

Git的标签 - 分支和标签名称之类的东西,虽然&#34;远程分支&#34;也只是标签,就像git branch --contains的{​​{1}}引用之类的东西一样,开始了:标签都是按名称查找的,每个标签都给出了原始的SHA- 1表示提交 1 。因此stash具有提交git stash的原始SHA-1。从那里开始,git也会找到br1,然后找到C。分支标签B找到A,其前往br2,然后发送到E

分支标签也称为&#34;分支&#34;,但从技术上讲,它们只识别分支的 tip 。也就是说,D使A成为&#34;结束&#34;分支。

(请注意,如果我们添加一个指向br1的标签 - 比如说,添加C - 我们只是添加了另一个分支。这适用于两种意义:那里有一个新的分支标签B和一个新的&#34;结构分支&#34;,包含提交mastermaster。这与此无关重叠B;它使A成为分支提示。或者,我们可以br1指向B,以便分支master和{ {1}}完全重叠。如果我们然后向C添加提交br1,则分支提示master向前移动以指向F但是分支提示{ {1}}仍然落后,指向br1。这个分支尖端的自动推进是指在分支上的意思#34;你只能在一个分支标签上,甚至如果许多标签都指向一个提交,那么那就是前进的分支标签。)

现在让我们看一下&#34;历史重写的简单案例&#34;,我们进入分支br1并执行F。这里的想法是选取当前未与master共享的C部分 - 即提交br2git rebase br1 - 以及&#34;移动&# 34;他们,导致:

br2

(我已经从内部提交链接中删除了箭头,因为它们很难绘制。)但这是一张不准确的图片。 Git实际上并没有移动提交;这需要更改br1。原始提交D记录其父提交(或提交,但这里只有一个),以及E。 Git无法更改 B - C <-- br1 / \ A D - E <-- br2 - 但它可以复制到新的提交D,除了D说&#外,所有内容都相同34;我的父母是A&#34;:

D

一旦这样做,git可以将D'复制到D',其中C的所有内容都相同,除了(再次)其父级: B - C <-- br1 / \ A D' \ D - E & #39;父母是E

E'

现在所有git需要移动标签E'。标签用于指向E';如果它现在指向D'没有指向 B - C <-- br1 / \ A D' - E' \ D - E ,则提交br2E成为&#34;被放弃&#34;并且可以在以后进行垃圾收集:

E'

所以,E基本上是&#34; git rebase on steriods&#34;原样。它不是仅复制少数提交,而是复制所有提交,在每次新提交之前应用提供的过滤器。鉴于我们原始的简单D - 通过 - E,如果您执行 B - C <-- br1 / \ A D' - E' <-- br2 \ D - E [abandoned] ,则会复制每次提交。

如果你的过滤器没有做任何更改,那么新复制的提交都是完全相同的(每个文件是相同的,每个树是相同的,每个提交是相同的,所以它们都结束了使用相同的原始SHA-1 ID并且没有任何标签移动)。但实际上你的过滤器必须做出一些改变,可能在历史上已经相当长了。在这种情况下,我们假设它改变了提交git filter-branch的内容,因此git必须改为复制A。 filter-branch的结果如下:

E

(而不是仅仅放弃以前的分支,过滤分支的叶子标签拼写为git filter-branch --all,如果结果符合您的要求,您可以将其删除。)

请注意,它会从变更点向前复制所有内容。但它只在您的本地存储库中完成此操作。你提到你有一个&#34;远程拷贝&#34; (大概是A)。 他们只有原件,没有副本。

让我们绘制一个更真实的提交图,其中A'个节点用于不感兴趣的点和更复杂的历史记录。让我们进一步假设要删除的 B - C <-- refs/original/heads/br1 / A \ D - E <-- refs/original/heads/br2 B'- C' <-- br1 / A' \ D'- E' <-- br2 文件发生在分支refs/original/...origin分割之前:

o

此处dump.sql是第一个包含不需要的文件的提交。 (我们很快就会看到为什么我很快就标记了branch_abranch_b。)因此,filter-branch命令必须将其复制到--o--W--X--Y--o--o--A <-- branch_a \ o--o--B <-- branch_b ,以及所有后续提交,包括两个分支提示XW

Y

现在,你说你已经删除了X'&#34;的远程副本。也就是说,你做了A

这里有点复杂。 : - )

B仅为--o--W--X--Y--o--o--A <-- [some label(s), incl refs/original/...] \ \ \ o--o--B <-- [some label(s)] \ X'-Y'-o'-o'-A' <-- branch_a \ o'-o'-B' <-- branch_b ,后跟branch_bgit checkout branch_a; git pull origin branch_b步骤转到指定的远程并带来任何新提交。可能没有新的提交 - 你已经拥有所有旧的提交,你从中复制了 - 但 merge 步骤在这里出错了。

要考虑此抓取和/或较早的抓取,其中包含&#34;某些标签&#34;以上,我们也添加git pull标签。有git fetch指向提交git mergegit fetch指向origin/。同样,这些是提交。

因此,origin/branch_a找到提交Aorigin/branch_b的合并基础。这就是他们的历史分叉的地方,即B之前的提交,即提交git merge。然后,它会合并A'与合并基础B和&#34;他们的&#34;分支X,查看WA'之间的变化,以及WB之间的变化。

无论它做了什么,这都会带回不需要的W,因为这是从BW添加的,而不是从A'开始添加的到dump.sql。它还会选择下面的两个提交,我将W替换为B。因此,此时合并的树中包含新的W,您将获得将A'绑定到o的合并提交:

*

现在让我们说你要求dump.sql合并到branch_a

origin/branch_b

Git再次需要找到合并基础。 (或者,如果--o--W--X--Y--o--o--A <-- origin/branch_a \ \ \ *--*--B <-- origin/branch_b \ `--. X'-Y'-o'-o'-A'-M <-- branch_a \ *'-*'-B' <-- branch_b branch_a之间存在纵横交错,使用递归策略,它必须创建一个&#34;虚拟合并基础&#34;通过组合同样好的祖先,但是让我们保持简单。)在这种情况下,合并基础是branch_b。我们将从git checkout branch_b; git merge branch_a branch_a获取更改,即branch_b提交的副本,再加上Y'。但我们已经进行了这些更改,因为我们合并了Y',它具有相同的更改(加上branch_b)。

通常情况下,这只会工作&#34;但可能在你的情况下,事情变得更加复杂,你会遇到一堆冲突。

在任何情况下,事情都会出错,因为你正在复活旧的&#34; (预先&#34;重写&#34;)使用*上的分支进行分支。

这就是为什么很难重写历史记录&#34;成功:你可以很容易地重写自己的,但是如果你已经在其他地方发布了它(例如,在B'上),其他人就有了早期的历史,你就不能强迫他们摆脱它。你必须让他们自愿放弃旧的&#34;错误&#34;历史并切换到新的&#34;权利&#34;历史。


1 或其他git对象,如带注释的标签;或标签可以是间接引用,即包含另一个标签的名称。通常只有特殊的origin/branch_b标签是间接引用,包含您所依赖的分支的名称。