[英]efficiently rewriting (rebase -i) a lot of history with git
我有一个git存储库,在最新版本中有大约3500个提交和30,000个不同的文件。 它代表了来自多个人的大约3年的工作,我们已经获得了使其全部开源的许可。 我正在努力发布整个历史记录,而不仅仅是最新版本。 为此,我感兴趣的是“回到过去”并在创建文件时在文件顶部插入许可证标题。 我实际上有这个工作,但完全用ramdisk运行大约需要3天,但仍然需要一些手动干预。 我知道它可以快得多,但我的git-fu不能完成任务。
问题是:如何更快地完成同样的事情?
我目前做什么(在脚本中自动化,但请耐心等待......):
确定将新文件添加到存储库的所有提交(其中只有500个,fwiw):
git whatchanged --diff-filter=A --format=oneline
将环境变量GIT_EDITOR定义为我自己的脚本,在文件的第一行只用一次edit
替换pick
(你很快就会明白为什么)。 这是该操作的核心:
perl -pi -e 's/pick/edit/ if $. == 1' $1
对于上面git whatchanged
以上输出的每个提交,在添加文件的提交之前调用交互式rebase:
git rebase -i decafbad001badc0da0000~1
我的自定义GIT_EDITOR(perl one-liner)更改pick
进行edit
,我们将被删除到shell以更改新文件。 另一个简单的header-inserter
脚本在我试图插入的标题中查找已知的唯一模式(仅在已知文件类型中(*。[chS] for me))。 如果它不存在,则插入它,然后git add
文件。 这种天真的技术不知道在当前提交期间实际添加了哪些文件,但它最终做正确的事情并且是幂等的(对同一文件多次运行是安全的),并且不是这整个过程瓶颈的地方无论如何。
在这一点上,我们很高兴我们已经更新了当前的提交,并调用:
git commit --amend
git rebase --continue
rebase --continue
是昂贵的部分。 因为我们为whatchanged
的输出中的每个修订调用一次git rebase -i
,所以这是很多的重新定位。 这个脚本运行的几乎所有时间都花在观看“Rebasing(2345/2733)”计数器增量上。
它也不仅仅是缓慢的。 必须解决定期发生的冲突。 至少在这些情况下(但可能更多)会发生这种情况:(1)当“新”文件实际上是现有文件的副本时,对其第一行进行了一些更改(例如, #include
语句)。 这是一个真正的冲突,但在大多数情况下可以自动解决(是的,有一个处理它的脚本)。 (2)删除文件时。 通过确认我们想要用git rm
删除它,这是可以轻易解决的。 (3)有些地方似乎diff
只是表现不好,例如,改变只是添加一个空行。 其他更合理的冲突需要人工干预,但总的来说它们不是最大的瓶颈。 最大的瓶颈绝对只是坐在那里盯着“Rebasing(xxxx / yyyy)”。
现在,单个rebase是从较新的提交启动到较旧的提交,即从git whatchanged
输出的顶部开始。 这意味着第一个rebase影响了昨天的提交,最终我们将从3年前重新定位提交。 从正在进行的“新”,以“老”,似乎违反直觉的,但到目前为止,我不相信它的问题,除非我们改变不止一个pick
一个edit
调用重订时。 我害怕这样做是因为冲突确实到来了,而且我不想处理冲突的浪潮,试图一次性改变一切。 也许有人知道避免这种情况的方法吗? 我无法想出一个。
我开始研究git对象1的内部工作原理! 看起来似乎应该有一种更有效的方法来遍历对象图并只进行我想要进行的更改。
请注意,这个存储库来自一个SVN存储库,我们实际上没有使用标签或分支(我已经git filter-branch
ed away),所以我们确实有直线历史的便利。 没有git分支或合并。
我确定我已经遗漏了一些关键信息,但是这个帖子似乎已经过了很长时间。 我会尽力按要求提供更多信息。 最后,我可能需要发布我的各种脚本,这是一种可能性。 我的目标是弄清楚如何在git存储库中重写历史; 不要讨论其他可行的许可和代码发布方法。
谢谢!
更新2012-06-17: 博客文章包含所有血腥细节。
运用
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 );
}
}
blob是内容可寻址的。 您无法在不更改其散列的情况下单独修改单个文件,这会更改包含它的任何提交所引用的目录blob,从而更改从其中下载的任何提交。 基本上你必须改写世界,因为我理解这个问题。 我想我可以想象一个算法以反向DAG顺序完成所有这些工作,带有一个原始到修改过的对象哈希的大哈希表,它只重写了每个对象一次。
如果您已经有一个正确的解决方案(即使需要三天),是否真的值得尝试优化它? 我无法想象实际上已经调试了这段代码并且工作正常,以便在不到三天的天真解决方案中发布结果。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.