6

我有一个 git 存储库,在最新版本中有大约 3500 个提交和 30,000 个不同的文件。它代表了多人大约 3 年的工作,我们已获得将其全部开源的许可。我正在努力发布整个历史,而不仅仅是最新版本。为此,我对“回到过去”感兴趣,并在创建文件时在文件顶部插入许可证标题。我实际上有这个工作,但它需要大约 3 天的时间完全用完一个 ramdisk,并且仍然需要一点手动干预。我知道它可以快很多,但我的 git-fu 并不能胜任这项任务。

问题:我怎样才能更快地完成同样的事情?

我目前在做什么(在脚本中自动执行,但请耐心等待......):

  1. 确定将新文件添加到存储库的所有提交(其中只有 500 个,fwiw):

    git whatchanged --diff-filter=A --format=oneline
    
  2. 将环境变量 GIT_EDITOR 定义为我自己的脚本,在文件的第一行只替换pick一次edit(你很快就会明白为什么)。这是操作的核心:

    perl -pi -e 's/pick/edit/ if $. == 1' $1
    
  3. 对于上述输出中的每个提交,在git whatchanged添加文件的提交之前调用一个交互式变基:

    git rebase -i decafbad001badc0da0000~1
    

我的自定义 GIT_EDITOR(即 perl 单行)更改pickedit,我们被放到 shell 以对新文件进行更改。另一个简单header-inserter的脚本在我尝试插入的标头中查找已知的唯一模式(仅在已知文件类型中(*.[chS] 对我来说))。如果它不存在,它会插入它,并且git add是文件。这种天真的技术不知道在当前提交期间实际添加了哪些文件,但它最终会做正确的事情并且是幂等的(对同一个文件多次运行是安全的),并且无论如何都不是整个过程的瓶颈.

在这一点上,我们很高兴我们已经更新了当前提交,并调用:

    git commit --amend
    git rebase --continue

rebase --continue是昂贵的部分。git rebase -i由于我们为 的输出中的每个修订都调用一次whatchanged,所以这是很多变基。该脚本运行的几乎所有时间都花在观察“变基 (2345/2733)”计数器的增量上。

它也不只是慢。有必须解决的周期性冲突。至少在以下情况下会发生这种情况(但可能更多): (1) 当“新”文件实际上是现有文件的副本时,它的第一行(例如,#include语句)做了一些更改。这是一个真正的冲突,但在大多数情况下可以自动解决(是的,有一个处理该问题的脚本)。(2) 当一个文件被删除时。这可以通过确认我们要删除它来轻松解决git rm。(3) 有些地方看起来像diff只是表现不佳,例如,更改只是添加了一个空白行。其他更合理的冲突需要人工干预,但总的来说它们并不是最大的瓶颈。最大的瓶颈绝对是坐在那里盯着“Rebasing (xxxx/yyyy)”。

现在,各个变基是从较新的提交到较旧的提交启动的,即从git whatchanged. 这意味着第一个 rebase 会影响昨天的提交,最终我们将 rebase 3 年前的提交。从“较新”到“较旧”似乎违反直觉,但到目前为止,我不相信这很重要,除非我们在调用变基时将多个更改pick为一个。edit我害怕这样做,因为冲突确实会到来,而且我不想处理由于试图一次性重新确定所有内容而引发的冲突浪潮。也许有人知道避免这种情况的方法?我一直想不出一个。

我开始研究 git objects 1的内部工作原理!似乎应该有一种更有效的方法来遍历对象图并进行我想要进行的更改。

请注意,这个存储库来自一个 SVN 存储库,我们实际上没有使用标签或分支(我已经git filter-branch删除了它们),所以我们确实有直线历史的便利。没有 git 分支或合并。

我确定我遗漏了一些关键信息,但这篇文章似乎已经过长了。我将尽我所能根据要求提供更多信息。最后我可能只需要发布我的各种脚本,这是有可能的。我的目标是弄清楚如何在 git 存储库中重写历史;不讨论其他可行的许可和代码发布方法。

谢谢!

2012-06-17 更新:包含所有血腥细节的博客文章。

4

2 回答 2

4

使用

git filter-branch -f --tree-filter '[[ -f README ]] && echo "---FOOTER---" >> README' HEAD

本质上会在文件中添加一个页脚行README,并且历史看起来自文件创建以来就一直存在,我不确定它是否对您来说足够有效,但这是正确的方法。

制作一个自定义脚本,您最终可能会得到一个很好的项目历史记录,做太多的“魔术”(rebase、perl、脚本编辑器等)可能最终会以意想不到的方式丢失或更改项目历史记录。

jon(OP)使用这种基本模式来实现目标,并显着简化和加速。

git filter-branch -d /dev/shm/git --tree-filter \
'perl /path/to/find-add-license.pl' --prune-empty HEAD

一些对性能至关重要的观察。

  • 使用-d <directory>指向 ramdisk 目录的参数(如/dev/shm/foo)将显着提高速度。

  • 使用其内置的语言功能从单个脚本进行所有更改,在使用小型实用程序(如find)时完成的分叉将多次减慢该过程。避免这种情况:

    git filter-branch -d /dev/shm/git --tree-filter \
    'find . -name "*.[chS]" -exec perl /path/to/just-add-license.pl \{\} \;' \
    --prune-empty HEAD
    

这是 OP 使用的 perl 脚本的净化版本:

#!/usr/bin/perl -w
use File::Slurp;
use File::Find;

my @dirs = qw(aDir anotherDir nested/DIR);
my $header = "Please put me at the top of each file.";

foreach my $dir(@dirs) {
  if (-d $dir) {
    find(\&Wanted, $dir);
  }
}

sub Wanted {
  /\.c$|\.h$|\.S$/ or return; # *.[chS]
  my $file = $_;
  my $contents = read_file($file);
  $contents =~ s/\r\n?/\n/g; # convert DOS or old-Mac line endings to Unix
  unless($contents =~ /Please put me at the top of each file\./) {
    write_file( $file, {atomic => 1}, $header, $contents );
  }
}
于 2012-06-06T18:21:01.960 回答
-1

Blob 是内容可寻址的。您不能在不更改其哈希值的情况下单独修改单个文件,这会更改包含它的任何提交所引用的目录 blob,从而更改从它派生的任何提交。基本上你必须重写世界,因为我理解这个问题。我想我可以想象一个算法以反向 DAG 顺序完成所有这些工作,具有原始到修改对象哈希的大哈希表,每个对象只重写一次。

如果您已经有了一个正确的解决方案(即使是一个需要三天的解决方案),那么尝试优化它真的值得吗?我无法想象实际调试此代码并正常工作以在不到天真的解决方案所需的三天内发布结果。

于 2012-06-06T18:13:40.330 回答