是否可以从git存储库中删除两个旧提交?
例如,采用此时间表: [成千上万的提交]> A> B> C> D> [成千上万的提交]> HEAD
我想删除" B"和" C",但不改变以" D"
开头的任何历史记录一些注意事项:
如果可以纠正,我会喜欢这样做,只因为它有效地打破了任何责备。通过在此之前隐藏任何提交来起作用。不太重要的是,它也打破了很多GitHub" graph"函数,因为这两个大规模的提交会使得缩减程度大大降低。
我已经考虑恢复两个提交,但它并没有真正帮助任何"责备"函数(它只是将每一行的责任从" C"移动到新的恢复提交)。这听起来像是我正在寻找的一个基础,但这将如何影响分支结束附近的任何活跃工作?
答案 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可能出错,请继续阅读。
虽然git rebase
适用于较小的情况,但在您的情况下,&#34; D&gt; [成千上万的提交]&gt; HEAD&#34;部分是有问题的。
原因是rebase通常只是完全剥离合并。据推测,在&#34;成千上万的提交中有分支和合并序列。部分。
Rebase有一个-p
或--preserve-merges
标志,但从严格意义上讲,这并不是保留合并。相反,它重新执行合并。由于rebase的性质,这在某些情况下是非常必要的 - 但是由于您处理的情况更具体,因此对于您的特定问题, 试图重新进行合并可能是灾难性的。
这意味着你可能根本不想要变装。您可能想要git filter-branch
。
任何操作都会删除两个&#34;坏&#34;提交B
和C
意味着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
。
如果使用提交过滤器,您只需跳过提交B
和C
。我们可以直接从文档开始这个例子:
删除由&#34; Darl McBribe撰写的提交&#34;来自历史:
git filter-branch --commit-filter ' if [ "$GIT_AUTHOR_NAME" = "Darl McBribe" ]; then skip_commit "$@"; else git commit-tree "$@"; fi' HEAD
然后我们需要至少两次更改,可能还需要三次:
我们希望过滤HEAD
(拼写--all
,而不是过滤-- --all
,因为我们需要将--all
传递给git rev-list
而不是git filter-branch
尝试解释它。)
我们不想测试提交的作者以获取特定名称,而是要测试提交的 ID 以查看它是否& #39; s我们想要跳过的提交B
的ID,或者我们也想跳过的提交C
的ID。
我们(可能)希望确保更新过去指向旧提交的任何标记,以便它们指向新副本。这意味着我们需要一个--tag-name-filter
,我们想要的标签名称过滤器只是cat
(即,原始标签名称不变)。
由于我没有B
和C
的原始提交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。在这种情况下,大概你没有引用指向提交B
或C
- 你将跳过的两个 - 所以这只是一个有趣的理论注释。但是,如果你 的引用指向B
或C
,那么剥离它们的效果就是重写过滤后的正引用以指向A
。 (请注意,必须在filter-branch
引用表达式中提及它们,通常是--all
。)
真正永久删除提交B
和C
的任何方法的缺点是它重新编号(重新散列) 删除后的每次提交。 Git的所有分布式存储库魔法都通过这些哈希来实现。这意味着,一旦您重写了历史记录,每个拥有克隆或分支的用户都必须采取行动以适应新的重写历史记录。 (通常这意味着&#34;将当前工作/回购保存到一边,重新克隆,然后挑选或以其他方式将当前工作重新提取到新克隆中。&#34;)
无论 如何获得更改的历史记录,无论是git rebase
还是git filter-branch
还是使用类似BFG的内容都无关紧要。 &#34;改变过去&#34;重新编号这些加密签名的Merkle树ID。现在使用这些的所有人都必须适应。
使用git replace
时,我们会留下B
和C
,告诉Git在查看提交D
时,应该看一下一些改动的副本D'
。更改后的版本D'
只是D
的副本,其父级设置为A
,这意味着只要Git将其小小的眼睛滑到D'
而不是,它会&#34;看&#34;链从提交E
到D'
再到A
,而不是&#34;请参阅&#34;原始&#34; D
导致C
导致B
导致A
&#34;序列
替换方法还要求所有客户故意接受这个新的替代D'
(他们不会自动看到它),就像filter-branch
一样&# #39;不完美。然而,它的破坏性要小得多:客户可以随时启动(或停止!)更换,而不会影响他们现在正在做的事情。只有正在查看替换的客户才会看到更改的历史记录。