如何从repo中删除大文件而不丢失历史记录

时间:2018-05-07 06:52:01

标签: git rebase

当我试图将其推送到github并且被拒绝时,我的文件夹中有一个大型mp3文件。我已经从文件夹中删除了该文件。我在删除文件后尝试再次按下代码,并收到此消息:

client/build/audio/Celtic Music - Ancient Forest _ 3 hours of celtic fantasy music (192  kbps) (TubeMp3Convert.com).mp3 is 252.71 MB; this exceeds GitHub's file size limit of 100.00 MB

然后我尝试运行此命令:

$ git reset HEAD 'client/build/audio/Celtic Music - Ancient Forest _ 3 hours of celtic fantasy music (192  kbps) (TubeMp3Convert.com).mp3'

之后我试图再次将它推上去,但仍然像以前一样得到了同样的错误。我不确定如何取消暂存此文件。我不想做一个回复,因为我不想失去自那时以来所做的所有工作。请帮忙,我不知道该怎么办。在这个问题得到解决之前,我无法将任何工作推向github。

1 个答案:

答案 0 :(得分:0)

Ritwick Dey的回答可能有效,也可能不行,具体取决于自首次添加大文件以来您执行的git次操作。让我们退一步看看为什么一个命令或其他命令可能有用。

git中有三种类型的存储空间。当你考虑它们如何在物理上存储时,边界起初看起来有点软弱,但从概念上讲它们是三个不同的存储区域:

首先是工作树。回购可能有也可能没有这些中的一个或多个。工作树由硬盘驱动器(或任何存储介质)上的常规文件组成,代表您正在进行的工作"。您的编辑器和其他工具与文件进行交互的位置,因此它看起来就像没有git的情况一样,除了可能有一些git使用的额外文件。

当您从文件夹"中删除文件时,您将其从工作树中删除。

其次,有索引。这是在提交之前进行更改的地方。它由git对象和一个将它们连接在一起的文件(.git/index)组成。它的确切用法取决于你正在做什么,但一般来说,项目的暂定版本 - 即你可能会承诺进入历史的版本 - 就在这里。

当您运行git reset HEAD ...时,您正在更新索引。我假设您打算从索引中删除文件,并在某些情况下 - 特别是当文件在当前检出的提交中不存在时 - 它会产生这种效果。在您的情况下,我认为当前签出的提交中的文件,因此该命令可能什么也没做。

您可以使用

更可靠地从索引中删除文件
git rm path/to/file

如果要在将副本保留在工作树中的同时从索引中删除文件,则可以说

git rm --cached path/to/file

哪个好;事实上,如果你不希望文件爬回到历史记录中,你需要将它放在索引之外,你需要将它放在.gitignore中或者从工作树中删除它好。

但是工作树和索引都不与其他repos共享。也就是说,push仅处理第三个存储区域,我们还没有谈过这个区域。由于GitHub已拒绝push,我们知道您的文件位于此最终存储区域。

第三个存储区域包含您的git历史记录 - 提交和其他描述项目的对象。我经常看到它称之为(并称之为)数据库"但我已经认为这可能是不精确的(因为直觉上"数据库"应该表示git对象的存储区域,它构成了历史记录和索引的大部分。

所以似乎真正定义了第三个区域是refs。分支是一个参考 - 可能是最重要的一种。 (还有标签,注释,替换,远程跟踪引用,"备份"引用等)ref指向数据库中的对象,通过该对象可以到达其他对象。对象的集合"可达"以这种方式是与其他存储库共享的候选者。在分支的情况下,它构成了项目的历史记录。

按设计,很难修改这些数据。向它添加新数据很容易,但从中删除数据却不那么容易。因为git的工作是保留历史。

问题是,你的巨大文件位于第三个存储区域。删除文件有多难取决于历史记录和#34;以及#34;它。为了说明一些可能的场景,我将绘制一些图表,其中提交由单个字母表示。对于每个场景中包含大文件的提交,我将使用大写字母。

以下所有技术构成历史重写" - 他们从历史中删除提交,替换为新提交。 (新提交的内容与原始内容不同,因为它们排除了巨大的文件。)因为您还没有成功推送受影响的分支,这很好,但要注意重写任何部分已经分享的历史可能会导致其他问题。

如果分支上的最新提交"知道"文件,然后你可以使用

git commit --amend
从索引中删除文件后

。最简单的情况是

a -- b -- C <--(master)

从索引中删除文件然后运行git commit --amend会给你

a -- b -- C
      \
       c <--(master)

其中c替换历史记录中的C,并且&#34;相同,但没有巨大的文件&#34;。原始提交C目前仍在您的本地存储库中,可以使用git reflog访问。它最终会被清理干净,如果你需要,你可以采取措施尽快清理它。但是在这一点上它现在不会干扰push,因为它不属于你的分支历史。

可以通过这种方式清除多个分支的其他变体,但它可能会变得更加棘手。

a -- b -- C <--(branch_a)
      \
       D <--(branch_b)

在这种情况下,您可以像修复前一个方案中的master一样修复每个分支。 (查看branch_a;从索引中删除大文件; git commit --amend;查看branch_b;从索引中移除大文件; git commit --amend。)然后您就拥有< / p>

a -- b -- c <--(branch_a)
     |\
     | C
     |\
     | d <--(branch_b)
      \
       D

当两个或多个分支都包含具有大文件的相同提交时,它变得更加棘手。由于我们只假设最近的提交包含该文件,因此仍然不会坏。

a -- b -- C <--(branch_c)(branch_d)

现在您不想为每个分支执行单独的commit --amend命令,因为分支将不再指向一个共享提交。

git checkout branch_c
git commit --amend
git branch -f branch_d

将确保两个分支最终都在同一个重写的提交中。

a -- b -- c <--(branch_c)(branch_d)
      \
       C

无论如何,commit --amend仅适用于最近的提交。如果您需要编辑&#34;较旧的&#34;历史,你可以考虑git rebase。这仍然只适用于相对简单的场景;更多关于此以后。但是例如

a -- b -- C -- D -- E <--(master)

在这种情况下你可以说

git rebase -i master~3 master

其中master~3是一个表达式,在此示例中,表示包含该文件的最后一次提交。如果您不想找出适合您特定情况的表达方式,并且历史记录不是太大,您可以说

git rebase -i --root master

这将为您提供TODO列表中的额外条目(您可以忽略它们,但它们是&#34;噪音&#34;要完成),这可能会使rebase花费更长时间。< / p>

无论如何,无论您使用哪种命令,都会获得TODO列表。在列表中找到提交C的条目,并将其第一个单词从pick更改为edit。退出编辑器时,rebase将开始,最终它将停止并提示您编辑下一个提交(commit C)。从工作树中删除大文件,然后按照提示的指示continue rebase。

a -- b -- C -- D -- E
      \
       c -- d -- e <--(master)

commit --amend一样,这一次只会移动一个分支,如果任何提交已经共享,则必须特别注意&#34;由多个分支机构。

a -- b -- C -- D <--(branch_a)
           \
            E <--(branch_b)

因为C是&#34;共享&#34;,您可以执行类似

的操作
git rebase -i branch_a~2 branch_a
# ... rebase steps as outlined above ...
git rebase --onto branch_a^ branch_b^ branch_b

branch_a^之类的表达式会因具体情况而异。如果文件是在C添加的,并且在DE中保持不变,则第二个rebase不需要是交互式的。

&#34;多个分支&#34;但是,案件可能会很快变得复杂。更糟糕的是,如果rebase必须遍历合并提交,例如

a -- b -- C -- D -- M <--(master)
           \       /
            E --- F

做一个rebase没有任何问题,因为更难。所以在这种情况下,你可以回到git filter-branch

git filter-branch --index-filter 'rm --cached --ignore-unmatch path/to/file' -- --all

这适用于任何git repo,但如果repo很大(很多提交),它可能会很慢。它还需要一些特殊的清理,因为它创建了备份参考&#34;在重写之前保留状态。最安全的&#34;对于每个被重写的分支my_branch,清理的方法是

git update-ref -d refs/original/refs/heads/my_branch

作为一种快捷方式,它只能用于rm -r .git/refs/original,但这不太安全&#34;并且它假定您在任何可能导致引用被打包之前执行此操作。

如果清理过程似乎太麻烦,或者回购太大而无法在可接受的时间范围内工作,最后一个选择是使用第三方工具,如BFG Repo Cleaner。