删除一对旧的git提交

时间:2016-09-30 19:05:19

标签: git github

是否可以从git存储库中删除两个旧提交?

例如,采用此时间表: [成千上万的提交]> A> B> C> D> [成千上万的提交]> HEAD

我想删除" B"和" C",但不改变以" D"

开头的任何历史记录

一些注意事项:

  • " A"和" C"功能相同
  • " B"基本上是"删除存储库中的每个文件"
  • " C"基本上是#34;添加存储库中的每个文件"
  • 从A到D没有分支或备用路径(我们的存储库的这一部分都是从另一个不支持分支的源控制系统转换而来的,所以它对于成千上万的提交非常线性(方向)
  • 我们的存储库现在托管在GitHub上,并且有无数的分支,拉取请求和此存储库的本地克隆,所有这些都可以追溯到" D"
  • 之后的数千次提交

如果可以纠正,我会喜欢这样做,只因为它有效地打破了任何责备。通过在此之前隐藏任何提交来起作用。不太重要的是,它也打破了很多GitHub" graph"函数,因为这两个大规模的提交会使得缩减程度大大降低。

我已经考虑恢复两个提交,但它并没有真正帮助任何"责备"函数(它只是将每一行的责任从" C"移动到新的恢复提交)。这听起来像是我正在寻找的一个基础,但这将如何影响分支结束附近的任何活跃工作?

3 个答案:

答案 0 :(得分:1)

你可以。如何操作取决于您是否已将这些更改推送到远程。

未发布的更改

如果尚未将更改推送到远程,您可以在良好的基础提交之上简单地重新提交好的提交( D 或其祖先,如果有的话)( ),不包括上次错误提交的任何祖先( C ):

git rebase --onto <commit-sha-of-A> <commit-sha-of-C> <commit-sha-of-D>

在违规分支上,使用--onto告诉它哪个分支或提交到rebase。然后引用C的分支或提交来告诉它要排除的祖先。最后,引用D或其祖先的分支或提交来告诉它什么血统的基因。

你来自:

-> A -> B -> C -> D

为:

-> A -> D
   \--> B -> C

推送变更

如果您已经分享了这些更改,那么您将重写分支的历史记录,并可能为您的队友带来额外的工作。您想要通知人们即将发生的变化。首先,使用上面相同的方法在本地仓库上修复问题。当你准备好了,你将不得不强迫这种分歧到遥控器:

git push --force <remote> <branch>

受此更改影响的任何人如果对自己进行了更改,都会遇到合并问题。您希望他们使用上述相同的方法获取更改并在固定分支的顶部修改其良好的更改(如果有)。

希望这有帮助!

答案 1 :(得分:1)

正如Matt Meng所写,您可以使用git rebase删除历史记录中的提交。这具有从commit C向前创建全新版本历史的不良副作用。如果您自己正在开展此项目,则其副作用很小。如果您正在组建团队,重写版本历史记录可能会导致严重问题,因为他们需要将自己的工作重新绑定到新树上。

或者,您可以使用git revert来创建新的提交,然后撤消&#34;给定提交中的更改。

答案 2 :(得分:0)

考虑使用git replace

我不会把这个放到答案中,因为git replace字面上并没有删除两个提交,但它可能是正确的解决方案。它允许您假装它们已经消失,并且可以转移到其他存储库,并且不会重新编号每个复制的提交。在任何情况下都有现有的SO答案,例如How do git grafts and replace differ? (Are grafts now deprecated?)

要了解您可能选择git replace的原因,以及为什么rebase可能出错,请继续阅读。

不是rebase而是filter-branch

虽然git rebase适用于较小的情况,但在您的情况下,&#34; D&gt; [成千上万的提交]&gt; HEAD&#34;部分是有问题的。

原因是rebase通常只是完全剥离合并。据推测,在&#34;成千上万的提交中有分支和合并序列。部分。

Rebase有一个-p--preserve-merges标志,但从严格意义上讲,这并不是保留合并。相反,它重新执行合并。由于rebase的性质,这在某些情况下是非常必要的 - 但是由于您处理的情况更具体,因此对于您的特定问题, 试图重新进行合并可能是灾难性的。

这意味着你可能根本不想要变装。您可能想要git filter-branch

你无法得到你想要的东西

任何操作都会删除两个&#34;坏&#34;提交BC意味着Git必须将原始提交D复制到更改的提交D'。新提交D'将存储与提交D相同的源树。它可以(并且应该)具有相同的作者提交者及其时间戳。它也可以并且应该具有相同的提交消息。但是,作为其父提交,它将提交A而不是提交C

这意味着与原始D'相比,新提交D'至少会改变一件事。因此它将具有不同的SHA-1 ID。

现在,在&#34; [数千次提交]&#34;部分,我们说下一个提交是E。您希望尽可能多地保留E,但提交E列表D作为其父级,D的SHA-1 ID。

我们必须 D复制到D',以便我们可以更改D的父级。新副本D'具有不同的SHA-1 ID。这意味着我们现在被迫将E复制到E'E'E完全相同,只是作为其父级,它列出了提交D'

E复制到E'会强制我们将E之后的任何提交(例如F)复制到F'。这迫使我们复制其后续提交,这些提交一直持续到每个分支的提示,最终会回到D(现在为D')。

git filter-branch

这就是git filter-branch所做的事情:对于你要告诉它要检查的每个提交,它会提取该提交,应用每个过滤器,然后创建一个的新提交尽可能精确的副本< / em>(但没有比这更精确)。如果您通过过滤器管理进行逐位相同的提交,例如,如果您应用过滤器在&#34中提交A; A来自B&#34;链的一部分,并且不会改变关于A的任何 - 然后新提交与原始提交具有相同的ID,即原始提交承诺。否则 - 如果提交中的任何数据已更改,无论是父ID,树ID还是提交消息中的单个位,您都会得到一个新的不同提交:A',如它是。

虽然filter-branch正在创建所有这些副本,但它会写一个映射文件,对于每个复制的提交,它表示&#34;旧提交ID X 变为新的提交ID X' &#34;。并且,filter-branch允许您故意跳过提交。 1

因此,您要在此处执行git filter-branch --commit-filter--parent-filter

如果使用提交过滤器,您只需跳过提交BC。我们可以直接从文档开始这个例子:

  

删除由&#34; Darl McBribe撰写的提交&#34;来自历史:

git filter-branch --commit-filter '
    if [ "$GIT_AUTHOR_NAME" = "Darl McBribe" ];
    then
            skip_commit "$@";
    else
            git commit-tree "$@";
    fi' HEAD

然后我们需要至少两次更改,可能还需要三次:

  1. 我们希望过滤HEAD(拼写--all,而不是过滤-- --all,因为我们需要将--all传递给git rev-list而不是git filter-branch尝试解释它。)

  2. 我们不想测试提交的作者以获取特定名称,而是要测试提交的 ID 以查看它是否& #39; s我们想要跳过的提交B的ID,或者我们也想跳过的提交C的ID。

  3. 我们(可能)希望确保更新过去指向旧提交的任何标记,以便它们指向新副本。这意味着我们需要一个--tag-name-filter,我们想要的标签名称过滤器只是cat(即,原始标签名称不变)。

  4. 由于我没有BC的原始提交ID,因此我无法在此处显示,但最终可以解决:

    git filter-branch --commit-filter '
        if [ $GIT_COMMIT = id-of-B -o $GIT_COMMIT = id-of-C ];
        then
                skip_commit "$@";
        else
                git commit-tree "$@";
        fi' HEAD
    

    使用--parent-filter稍微简单一些。同样,直接来自文档,我们有:

    git filter-branch --parent-filter \
        'test $GIT_COMMIT = <commit-id> && echo "-p <graft-id>" || cat' HEAD
    
         

    甚至更简单:

    echo "$commit-id $graft-id" >> .git/info/grafts
    git filter-branch $graft-id..HEAD
    

    此处$commit-id有效代表提交D,而$graft-id代表提交A

    同样,在我们的案例中,我们并不真正想要HEAD,我们想要-- --all。我们可能也想要--tag-name-filter cat。我们可以使用负引用(例如^<id-of-C>)来跳过复制提交 - C - 更早(这是$graft-id..HEAD的左侧所做的)。由于复制提交很慢,这将跳过C之前的数千次提交,从而加快了过滤速度。

    请注意,移植物不是很稳定:它们被git replace取代,后者更加强大。如果您确实使用这样的移植物,您几乎肯定会立即运行git filter-branch以使移植物永久化。 (您也可以使用git replace运行git filter-branch以使替换永久化。)

    1 当您跳过提交时,它会在映射文件中写入一个条目,告诉它旧的提交ID已消失。更确切地说,它将旧ID映射到最近的祖先新ID&#34;。请参阅remap to ancestor section of the filter-branch documentation。在这种情况下,大概你没有引用指向提交BC - 你将跳过的两个 - 所以这只是一个有趣的理论注释。但是,如果你 的引用指向BC,那么剥离它们的效果就是重写过滤后的正引用以指向A 。 (请注意,必须在filter-branch引用表达式中提及它们,通常是--all。)

    复制提交的后果

    真正永久删除提交BC任何方法的缺点是它重新编号(重新散列) 删除后的每次提交。 Git的所有分布式存储库魔法都通过这些哈希来实现。这意味着,一旦您重写了历史记录,每个拥有克隆或分支的用户都必须采取行动以适应新的重写历史记录。 (通常这意味着&#34;将当前工作/回购保存到一边,重新克隆,然后挑选或以其他方式将当前工作重新提取到新克隆中。&#34;)

    无论 如何获得更改的历史记录,无论是git rebase还是git filter-branch还是使用类似BFG的内容都无关紧要。 &#34;改变过去&#34;重新编号这些加密签名的Merkle树ID。现在使用这些的所有人都必须适应。

    使用git replace时,我们会留下BC,告诉Git在查看提交D时,应该看一下一些改动的副本D'。更改后的版本D'只是D的副本,其父级设置为A,这意味着只要Git将其小小的眼睛滑到D'而不是,它会&#34;看&#34;链从提交ED'再到A,而不是&#34;请参阅&#34;原始&#34; D导致C导致B导致A&#34;序列

    替换方法要求所有客户故意接受这个新的替代D'(他们不会自动看到它),就像filter-branch一样&# #39;不完美。然而,它的破坏性要小得多:客户可以随时启动(或停止!)更换,而不会影响他们现在正在做的事情。只有正在查看替换的客户才会看到更改的历史记录。