让git format-patch像git log --follow一样跟踪重命名

6

我正在尝试将一个本地git仓库中的文件移动到另一个不同项目的本地git仓库中,同时保留原始仓库中的历史记录。目前为止,如果在源代码库中从未移动或重命名过该文件,则下面的代码可以正常工作

# Executed from a directory in the target repository
( cd $SOURCE_REPOSITORY_DIRECTORY && git format-patch -B -M --stdout --root $SOURCE_FILENAME) | git am --committer-date-is-author-date

这能够奏效是因为两个仓库的目录结构相同。如果它们不同,我就必须创建补丁文件并使用sed或其他工具来修复目录名称。
无论如何,当我遇到一个被重命名的文件时,情况就会变得有些棘手。即使我指定了-B -M(并且使用-B -M -C --find-copies-harder也得到了相同的结果),我也无法获得移动前的补丁,尽管文件已经被干净地移动(相似度指数为100%)。
这特别奇怪,因为git log --follow显示了所有提交,而git log --follow -p提供了所有差异。除了它们以相反的顺序提供,所以我无法将它们输入git am
还要注意的是,git log --follow -p 文件名会输出以下“补丁”以显示重命名:
diff --git a/old_dir_name/dir1/dir2/filename b/new_dir_name/dir0/dir1/dir2/filename
similarity index 100%
rename from old_dir_name/dir1/dir2/filename
rename to new_dir_name/dir0/dir1/dir2/filename

现在,如果git log能够以正确的格式和顺序显示补丁,让git am应用它们,那我就可以直接使用它了,但事实并非如此。使用git log --reverse --follow -p 文件名仅输出名称更改补丁,没有其他内容。
那么,我该如何让git format-patch按照帮助文件/手册所说的真正遵循重命名,并同时仅输出单个文件的补丁?或者,我该如何让git log -p以一种可以将其馈送到git am中以重新创建带有历史记录的文件的方式生成补丁?
我正在使用git版本1.8.4.3。

(Cc @OldPro)这个问题现在已经相当老了,因为它可以追溯到2013年 :) 但我认为下面的答案完全回答了你的问题,如果你同意的话,能否将其标记为接受的答案?这将非常有用,正如@jjd最近在这个其他问题(现在被标记为重复...)中指出的那样。 - ErikMD
@ErikMD,我给你的答案点了赞,但我不想把脚本/插件作为答案接受。即使我想这么做,我也没有尝试过你的脚本,并且我也不能验证它是否有效,所以我不想担保它能够正常工作。此外,我认为一个合适的答案应该作为一个单一的标准git命令提供,并带有适当的标记。因此,我不会将你宝贵的贡献标记为采纳答案。 - Old Pro
好的,我完全明白了,谢谢@OldPro的回复! - ErikMD
3个回答

3
我最近遇到了与此问题相同的用例,并使用Bash实现了一个解决方案,因此我想分享这个代码,因为它可能对其他人有用。
它由一个名为git-format-patch-follow的脚本组成,可在https://github.com/erikmd/git-scripts上找到,可按以下方式用于OP的问题:
( cd "$SOURCE_REPOSITORY_DIRECTORY" && git format-patch-follow -B -M --stdout --root --follow -- "$SOURCE_FILENAME" ) | git am --committer-date-is-author-date

更普遍地说,语法如下:

更一般的情况是:

git format-patch-follow <options/revisions...> --follow -- <paths...>

这个Bash脚本可以看作是自动运行@OldPro所列出算法的一种方式,我特别注意处理一些边角情况,如文件名中包含空格、多个文件通过CLI传递或从Git源代码库的子目录运行脚本。
最后,正如这篇博客文章指出的那样,只需将这样的脚本放在一个人的PATH中,Git就会像git子命令git format-patch-follow一样集成该脚本。
免责声明: git format-patch,因此git-format-patch-follow无法应用于非线性历史(涉及合并提交)。

2
我已经取得了一些进展,但现在需要更多的手动操作。
  • 对于每个文件,使用带有和不带有--follow参数的log命令来查看哪些文件已被重命名/移动/复制(为简单起见,将所有文件都称为“重命名”)。
  • 对于已重命名的文件,从log输出中提取之前的完整路径和文件名。
  • 然后在format-patch命令中,除了当前名称外,还要在命令行上给出所有旧名称

现在我的操作步骤如下:

 git format-patch -B -M -o /tmp/patches --root -- old_dir_name/dir1/dir2/filename new_dir_name/dir0/dir1/dir2/filename

该功能创建补丁以创建旧文件,将其重命名为新名称,然后继续修补文件。当然,对我来说问题在于旧目录在新仓库中不存在,并且目录级别已更改,因此还需要进行一些操作以使目录名称正常工作。

这应该更容易一些....


这是一个非常好的方法。我通常运行命令 grep "rename from" /tmp/patches/* 来验证在格式化大量文件时是否忽略了任何重命名。如果一些文件从 grepping 中输出,但不在我的命令中,我就知道需要添加它们并重新运行 format-patch。 - Paul

1

噢,我认为问题出在一些错误的逻辑上。特别是,当你结合--reverse--follow使用时,必须指定文件名:

[rename foo to bar]
$ git log --follow bar  # works
$ git log --follow --reverse -- foo # requires "--" because foo is not in HEAD

这个有点奏效。但是,当它遇到重命名时,它会将文件视为已删除,然后一切都停在那里。
tree-diff.c 包含此函数:
static void try_to_follow_renames(...)
{
        ...
        /* Remove the file creation entry from the diff queue, and remember it */
        choice = q->queue[0];
        q->nr = 0;

如果 diff_might_be_rename 返回 true,则调用此函数:

static inline int diff_might_be_rename(void)
{
        return diff_queued_diff.nr == 1 &&
                !DIFF_FILE_VALID(diff_queued_diff.queue[0]->one);
}

...
int diff_tree_sha1(...)
{
        ...
        if (!*base && DIFF_OPT_TST(opt, FOLLOW_RENAMES) && diff_might_be_rename()) {

我在这里做出了一些大胆的假设,但如果你按照相反的顺序进行,而不是“文件bar刚刚被创建,让我们看看是否可以找到一个foo,从中它被重命名”,如果日志被颠倒,您需要“文件foo已删除,让我们看看是否可以找到一个bar,将其重命名”,这就是...缺失的,如果注释是准确的。

如果你有很多这样的工作要做,我建议尝试在此处添加一些内容来记住差异是否被反转(对于format-patchlog --reverse都是如此),并根据需要更改diff_might_be_rename()try_to_follow_renames()代码。

如果你只有一个,那么手动修改一些差异可能会更容易。 :-)


恶心是对的。我已经很难使用git了,我绝不会尝试打补丁。不过,非常感谢你深入研究了这个问题,并希望你能激励其他人来解决它。 - Old Pro
(附注)注意:git log --follow 与 git 2.9 有所改进:https://dev59.com/vG025IYBdhLWcg3w4aCu#36615639 - VonC
@VonC:看起来这主要是修复在git 2.0中引入的一些故障(当父目录和最终文件名都更改时)。 - torek
@torek 是的,考虑到 Git 2.0 是从2014年5月开始的,这可能涉及到相当大量的用户。 - VonC

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