用git有效地重写(rebase -i)很多历史

时间:2012-06-06 15:59:53

标签: perl git git-rebase

我有一个git存储库,在最新版本中有大约3500个提交和30,000个不同的文件。它代表了来自多个人的大约3年的工作,我们已经获得了使其全部开源的许可。我正在努力发布整个历史记录,而不仅仅是最新版本。为此,我感兴趣的是“回到过去”并在创建文件时在文件顶部插入许可证标题。我实际上有这个工作,但完全用ramdisk运行大约需要3天,但仍然需要一些手动干预。我知道它可以更快,但我的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输出的每个提交,在添加文件的提交之前调用一个交互式rebase:

    git rebase -i decafbad001badc0da0000~1
    
  4. 我的自定义GIT_EDITOR(perl one-liner)将pick更改为edit,我们将被删除到shell以更改新文件。另一个简单的header-inserter脚本在我试图插入的标题中查找已知的唯一模式(仅在已知文件类型中(*。[chS] for me))。如果它不存在,则插入它,git add是文件。这种天真的技术不知道在当前提交期间实际添加了哪些文件,但它最终做了正确的事情并且是幂等的(对同一文件多次运行是安全的),并且不是这整个过程瓶颈的地方无论如何

    此时我们很高兴我们已更新当前提交,并调用:

        git commit --amend
        git rebase --continue
    

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

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

    现在,单个rebase是从较新的提交启动到较旧的提交,即从git whatchanged的输出的顶部开始。这意味着第一个rebase影响了昨天的提交,最终我们将从3年前重新定位提交。从“较新”到“较旧”似乎是违反直觉的,但到目前为止,我并不认为重要,除非我们在调用rebase时将多个pick更改为edit。我害怕这样做是因为冲突确实到来了,而且我不想处理冲突的浪潮,试图一次性改变一切。也许有人知道避免这种情况的方法吗?我无法想出一个。

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

    请注意,此存储库来自SVN存储库,我们实际上没有使用标记或分支(我已经git filter-branch编辑它们),因此我们确实有直线历史记录的便利。没有git分支或合并。

    我确定我已经遗漏了一些重要信息,但是这个帖子似乎已经过长了。我会尽力按要求提供更多信息。最后,我可能需要发布我的各种脚本,这是一种可能性。我的目标是弄清楚如何在git存储库中重写历史;不要讨论其他可行的许可和代码发布方法。

    谢谢!

    更新2012-06-17:Blog post并附上所有血腥细节。

2 个答案:

答案 0 :(得分: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

一些性能关键的观察结果。

  • 使用指向ramdisk目录的-d <directory>参数(如/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 );
  }
}

答案 1 :(得分:-1)

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

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