仅复制当前跟踪文件历史的新存储库

38
我们当前的代码库有成千上万个提交记录,新克隆一个库需要传输近1GB的数据(历史中有很多已删除的jar文件)。我们希望通过创建一个新的库来缩小这个大小,该库仅保留当前在库中活动的文件的完整历史记录,或者可能只修改当前的库以清除已删除文件的历史记录。但我不确定如何实现这一点。我尝试了Remove deleted files from git history中的脚本:
for del in `cat deleted.txt`
do
    git filter-branch --index-filter "git rm --cached --ignore-unmatch $del" --prune-empty -- --all
    # The following seems to be necessary every time
    # because otherwise git won't overwrite refs/original
    git reset --hard
    git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
    git reflog expire --expire=now --all
    git gc --aggressive --prune=now
done;

但是考虑到我们的历史记录中有数以万计的已删除文件和提交记录,运行这个脚本需要花费很长时间。两个小时前,我只为一个被删除的文件开始运行此操作,filter-branch命令仍在运行,它正在逐个检查40000多个提交记录,而且这是在一台带有SSD磁盘的新Macbook Pro上进行的。

我也阅读了页面https://help.github.com/articles/remove-sensitive-data,但它仅适用于移除单个文件。

是否有人能够做到这一点?我真的想保留当前跟踪文件的历史记录,如果我们无法保留历史记录,不确定节省空间的好处是否值得创建一个新的库。


1
你可以使用 git filter-branch --prune-empty --tree-filter 命令,并编写一个脚本,将树中的每个文件与你想要保留的文件列表(即当前跟踪的文件)进行比较,然后在任何你不想要的文件上执行 git rm -f 命令。这将在历史记录的每个提交中删除不需要的文件。 - Jonathan Wakely
3
如果你有成千上万个已删除的文件——你正在使用的脚本将运行数以万计次的git filter-branch。如果你还有数万个提交——这意味着你目前正在尝试重新处理许多百万次提交。 - AD7six
@Cupcake 是的,那就是我正在运行的脚本,我已经更新了我的问题以包括它。filter-branch命令仍在运行我的第一个删除文件,自我开始以来已经超过2小时了。我使用一台新的Macbook Pro和SSD。鉴于该命令逐个遍历存储库中的每个提交,我不知道可以期望它运行得有多快。 - Brent Sowers
1
正如AD7six指出的,您正在bash脚本中多次运行filter-branch。这可能是为什么它花费了这么长时间的原因。如果您只执行一次并传入一个命令来一次性删除您不想要的JAR文件,它可能会更快地运行。与您当前的操作相比,使用--tree-filter选项可能会更好。 - user456814
1
@Cupcake 是的,我们现在在项目中使用SBT来管理这个,所以绝大部分的JAR包都是从中央仓库下载的。这也带来了一些问题,但我不想偏离手头的话题,因为我们之前没有使用SBT,所以现在只能使用这个大型仓库。 - Brent Sowers
显示剩余6条评论
5个回答

48

删除所有内容并恢复您想要的内容

与其逐个删除这个文件列表,不如做相反的事情:删除所有内容,只恢复您想要保留的文件。

操作步骤如下:

# for unix

$ git checkout master
$ git ls-files > keep-these.txt
$ git filter-branch --force --index-filter \
  "git rm  --ignore-unmatch --cached -qr . ; \
  cat $PWD/keep-these.txt | tr '\n' '\0' | xargs -d '\0' git reset -q \$GIT_COMMIT --" \
  --prune-empty --tag-name-filter cat -- --all

# for macOS

$ git checkout master
$ git ls-files > keep-these.txt
$ git filter-branch --force --index-filter \
  "git rm  --ignore-unmatch --cached -qr . ; \
  cat $PWD/keep-these.txt | tr '\n' '\0' | xargs -0 git reset -q \$GIT_COMMIT --" \
  --prune-empty --tag-name-filter cat -- --all

这可能会更快地执行。

清理步骤

整个过程完成后,然后进行清理:

$ rm -rf .git/refs/original/
$ git reflog expire --expire=now --all
$ git gc --prune=now

# optional extra gc. Slow and may not further-reduce the repo size
$ git gc --aggressive --prune=now

比较操作前后的存储库大小,应该表明有相当大的减少,当然只有涉及到保留文件的提交,以及合并提交 - 即使是空的(因为这就是--prune-empty的工作原理),也会在历史记录中。

$GIT_COMMIT?

使用$GIT_COMMIT似乎引起了一些混淆,来自git filter-branch文档(强调已添加):

参数始终使用eval命令在shell上下文中进行评估(出于技术原因,在提交过滤器之前除外)。在那之前,$GIT_COMMIT环境变量将被设置为包含正在重写的提交的ID

这意味着 git filter-branch 将在运行时提供变量,而不是提前由您提供。如果存在任何疑问,可以使用此no-op过滤分支命令进行演示:

$ git filter-branch --index-filter "echo current commit is \$GIT_COMMIT"
Rewrite d832800a85be9ef4ee6fda2fe4b3b6715c8bb860 (1/xxxxx)current commit is d832800a85be9ef4ee6fda2fe4b3b6715c8bb860
Rewrite cd86555549ac17aeaa28abecaf450b49ce5ae663 (2/xxxxx)current commit is cd86555549ac17aeaa28abecaf450b49ce5ae663
...

1
你可能想使用 xargs 而不是逐行循环。它会尝试在每次运行中尽可能多地适应参数。 - Hasturkun
1
@AD7six,它奏效了!感谢您的所有帮助。运行大约花了10个小时。对于那些有兴趣从远程(或任何其他远程)拉取新提交而不增加存储库大小的人,您将不得不挑选它们,运行“git fetch origin master”,然后为每个新提交运行“git cherry-pick commitid”,然后再次运行上面列出的git gc命令。 - Brent Sowers
1
那么,如果一个文件被重命名前有历史记录呢?由于它的名称已经改变,我想它会被删除。或者有什么办法可以保留它吗? - SeB.Fr
我并没有看到任何复杂性 - 保留现有文件并将其重命名为单独的提交。如果这不是你想听到的,请提出另一个问题:)。 - AD7six
1
@SeB.Fr 记录重命名文件历史的答案是在第二个和第三个命令之间添加 git ls-files | while read -r line; do (git log --follow --raw --diff-filter=R --pretty=format:%H "$line" | while true; do if ! read hash; then break; fi; IFS=$'\t' read mode_etc oldname newname; read blankline; echo $oldname; done); done >> keep-these.txt - Cœur
显示剩余8条评论

21
基于AD7six,保留重命名文件的历史记录。(您可以跳过初步的可选部分)
可选:
删除所有远程:
git remote | while read -r line; do (git remote rm "$line"); done

去除所有标签:

git tag | xargs git tag -d

删除所有其他分支:

git branch | grep -v \* | xargs git branch -D

删除所有的暂存区

git stash clear

删除所有子模块的配置和缓存:

git config --local -l | grep submodule | sed -e 's/^\(submodule\.[^.]*\)\(.*\)/\1/g' | while read -r line; do (git config --local --remove-section "$line"); done
rm -rf .git/modules/

修剪未跟踪文件历史记录,保留已跟踪文件历史记录和重命名。
git ls-files | sed -e 's/^/"/g' -e 's/$/"/g' > keep-these.txt
git ls-files | while read -r line; do (git log --follow --raw --diff-filter=R --pretty=format:%H "$line" | while true; do if ! read hash; then break; fi; IFS=$'\t' read mode_etc oldname newname; read blankline; echo $oldname; done); done | sed -e 's/^/"/g' -e 's/$/"/g' >> keep-these.txt
git filter-branch --force --index-filter "git rm --ignore-unmatch --cached -qr .; cat \"$PWD/keep-these.txt\" | xargs git reset -q \$GIT_COMMIT --" --prune-empty --tag-name-filter cat -- --all
rm keep-these.txt
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now
  • 前两个命令用于列出被跟踪的文件和跟踪的文件旧名称,使用引号以保留具有空格路径的文件。
  • 第三个命令用于仅重写这些文件的提交记录。
  • 后续命令用于清除历史记录。

可选项(不建议使用)

repack (来自the-woes-of-git-gc-aggressive):

git repack -a -d --depth=250 --window=250

2
感谢您考虑文件名中的空格。 - rpyzh
你为什么不在git gc命令后添加--aggressive参数呢? - Jazaret
1
@Jazaret,这距离我记不清有多久了。但是如果你按照帖子末尾的链接(the-woes-of-git-gc-aggressive)进行操作,似乎有理由反对使用“--aggressive”。也许3.5年前我受到了它的影响。 - Cœur
不要紧,但是那些 sed 命令中的 g 标志是无害的,但并非必需的(它表示“全局”,即在该行上替换 所有,但当您替换 ^$ 时并没有任何区别,因为每行上只会有一个)。 - Silas S. Brown

14
截至2020年4月,使用git filter-branch命令时,git会产生以下警告:
WARNING: git-filter-branch has a glut of gotchas generating mangled history
         rewrites.  Hit Ctrl-C before proceeding to abort, then use an
         alternative filtering tool such as 'git filter-repo'
         (https://github.com/newren/git-filter-repo/) instead.  See the
         filter-branch manual page for more details; to squelch this warning,
         set FILTER_BRANCH_SQUELCH_WARNING=1.

我相信有一种安全的方法可以使用git filter-branch,但对于那些(像我自己一样)不知道如何避免上述陷阱的人来说,git-filter-repo可以很容易地保留仅当前跟踪文件的历史记录:

$ git checkout master
$ git ls-files > /tmp/keep-these.txt
$ git filter-repo --paths-from-file /tmp/keep-these.txt

git filter-branch 在我的代码库上运行大约需要5分钟的时间,而 git filter-repo 则在不到一秒钟的时间内运行并重新打包了代码库!

你可以按照它的 GitHub 页面上的说明来安装它。或者,在 Mac 上,你只需运行 brew install git-filter-repo 即可。


1
不错。现在这可能是被接受的答案。 - TTT

6

只运行一次git filter branch

问题中的脚本将会处理成千上万个提交,而且会执行各种(非常慢的)操作,每次迭代都要执行一遍,而这些操作通常只需要在最后执行一次。这确实会花费很长时间。

相反,可以一次性运行脚本,一次性删除所有文件:

del=`cat deleted.txt`
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch $del" \
  --prune-empty --tag-name-filter cat -- --all

一旦流程结束,那么进行清理:

rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now

# optional extra gc. Slow and may not further-reduce the repo size
git gc --aggressive --prune=now 

如果由于文件数量而导致上述方法失败

如果deleted.txt中的文件足够多,以至于上面的命令过于庞大而无法运行,则可以改写为以下方式:

git filter-branch --force --index-filter \
  'cat /abs/path/to/deleted.txt | xargs git rm --cached --ignore-unmatch' \
  --prune-empty --tag-name-filter cat -- --all

(清理步骤相同)

这与上面的版本完全相同,但是删除文件的命令是逐个进行而不是一次性全部删除。


在标题“无法工作”的第二部分中,您没有将Git命令传递给--index-filter,那能正常工作吗?您不需要改用--tree-filter吗? - user456814
它期望一个命令 - 它可以是任何命令。如果tree-filter合适,我不能确定 - 它只与从存储库中剪切dir-slice有关,我不认为OP正在这样做。 - AD7six
git gc --aggressive 真的不必要吗?或者说这是个好主意吗?如果你想要摆脱悬空提交,只需要 git gc --prune=now 就足够了。我曾经读到过一些关于使用 git gc --aggressive 的坏处(1),也看到过一些关于它的讨论(2) - user456814
我对它的操作并不是很了解,但在删除敏感或庞大的内容时,这是标准做法。我不知道过去5年中git发生了什么变化 - 但该命令仍然存在。我会添加一个注释。 - AD7six
1
@BrentSowers 命令 git ls-files 可以用来获取索引中文件的列表,而不必迭代外部文件。也许如果你传递类似于 --index-filter 的东西,会更快一些:git ls-files | grep .jar | xargs git rm --cached。但是这个命令将从历史记录中删除所有JAR文件...也许你可以在之后再提交你仍然需要的那些。 - user456814
显示剩余5条评论

0
补充AD7six所接受的答案(因为我没有足够的声望来评论答案):
如果你想保留不止一个master,可以:
1.删除不再需要的标签和分支。
2.然后创建一个引用所有那些你想要保留的分支和标签的文件列表。
for tag in `git for-each-ref refs/tags --format='%(refname)' | cut -d / -f 3`
do
    echo $tag; sleep 3 # sleep to avoid: fatal: Unable to create '.git/index.lock': File exists.
    git checkout "$tag"
    git ls-files > ../keep_files_tag_$tag.txt
    git ls-files >> ../keep_files_all.txt
done
for branch in `git for-each-ref refs/heads --format='%(refname)' | cut -d / -f 3`
do
    echo $branch; sleep 3 # sleep to avoid: fatal: Unable to create '.git/index.lock': File exists.
    git checkout "$branch"
    git ls-files > ../keep_files_branch_$branch.txt
    git ls-files >> ../keep_files_all.txt
done
sort ../keep_files_all.txt | uniq > keep_files_uniqe.txt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接