使用 git filter-branch 更改文件名大小写

3
我有一个Git仓库,在分支中有些文件名只是大小写不同。举个简单的例子,在主分支中有一个文件alpha/beta/foo.cpp,在分支bar中有一个文件alpha/beta/Foo.cpp。
问题是,当我尝试切换分支时,Git不允许我这么做。会出现一条错误消息,具体内容可能如下:
changes to file alpha/beta/Foo.cpp would be overwritten -- aborting
即使随后执行`git status`也显示工作目录是干净的。
由于该仓库还没有分享(实际上它是一个我正在迁移的大型Perforce仓库的镜像),所以我认为可以使用`git filter-branch`来重写历史,但这样做会丢失任何大小写敏感的更改。
当我使用:
git filter-branch -f -d /tmp/tmpfs/filter-it \
--tree-filter path/to/script \
--tag-name-filter cat --prune-empty -- --all

脚本看起来像这样

#!/bin/bash
if [ -e alpha/beta/foo.cpp ] ; then
    mv alpha/beta/foo.cpp alpha/beta/Foo.cpp
fi

最终结果是重写了引用(预期的),但文件本身并没有在两个分支中进行重命名,这让我感到困惑。你有什么建议吗?

你使用的操作系统是Windows、OS X还是Linux?你正在使用哪个shell来运行它?此外,使用git mv与index-filter一起可能会更快(只要重命名有效),因为你不需要使用它来检出工作副本,而不是tree-filter。 - user456814
一些性能改进肯定会受到欢迎。如上所示,对整个存储库(约29K次提交)进行排序需要大约4小时。 - escouten
OS X具有不区分大小写的文件系统,这可能会导致重命名问题。 Git也有配置设置来控制这一点,所以情况可能会变得有点复杂。 如果您正在像Linux这样区分大小写的操作系统上运行,则使用 git mv foo Foo 和index-filter应该可以解决问题。 在OS X上,有人需要找出如何使其工作。 - user456814
值得一提的是,我在这个仓库上启用了 git config core.ignorecase false 并再次尝试。现在我要睡觉了...明天早上我们就会知道这是否起了作用。 - escouten
事实证明,git mv 无法与 index-filter 一起使用,因为它需要一个可操作的工作副本。这个答案据说可以解决问题,但我自己还没有尝试过。 - user456814
显示剩余2条评论
2个回答

13

简短回答

以下解决方案是从多个来源修改而来的:

  1. 使用“fatal: bad source”错误的filter-branch --index-filter

  2. 使用Git重命名过去

这里是一个filter-branch调用,它使用索引过滤器在没有工作副本的情况下重写提交,因此应该运行非常快。请注意,作为示例,我将文件alpha/beta/foo.cpp重命名为alpha/beta/Foo.cpp

与任何可能具有破坏性的Git操作一样,在使用之前强烈建议您备份存储库的克隆

git filter-branch --index-filter '
git ls-files --stage | \
sed "s:alpha/beta/foo.cpp:alpha/beta/Foo.cpp:" | \
GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
git update-index --index-info && \
mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD

请注意,HEAD是可选的,因为它应该是filter-branch的默认值。它将重写从根提交到指向HEAD的提交的所有提交。如果您想进一步提高filter-branch的速度,可以传递一系列提交而不是HEAD,例如:
HEAD~20..HEAD

重新编写最近的20个提交。范围的开头是排除在外的,也就是说,它不会被重写,只有它的子代会被重写,而结束点HEAD是可选的,因为它是默认的。

验证

做一些快速的检查来验证过滤分支是否按照预期进行了操作是一个好主意。首先,将当前历史记录与之前的历史记录进行比较:

git diff --name-status refs/original/refs/heads/master
D       foo.cpp
A       Foo.cpp

请注意,相对于当前历史记录而言,之前的历史记录中不再包含foo.cpp(它被删除了),而是添加了Foo.cpp
现在确认一下foo.cppFoo.cpp包含完全相同的内容:
git diff refs/original/refs/heads/master:foo.cpp Foo.cpp

输出应为空,意味着两个版本之间没有差异。

详细说明

下面的细节还可以从博客文章“使用Git重命名过去”中获取更详细的解释。我在这里进行总结。脚本的基本思想是创建一个新的索引文件,其中包含文件foo的新名称(即foo变成Foo),然后用新的索引替换旧的索引。

第一步:获取索引文件内容

首先,当前的索引文件内容以可以被输入到git update-index中的形式输出,使用--stage选项:

git ls-files --stage
100644 195ff081f7d0d37a60181de790ae1c6b9f177be8 0       alpha/beta/foo.cpp
100644 0504de8997941bf10bcfb5af9a0bf472d6c061d3 0       LICENSE
100644 6293167f0eb7389b2f6f6b73e838d3a547787cbf 0       README.md
...etc...

步骤2:重命名文件

由于我们想要将foo.cpp重命名为Foo.cpp,因此我们使用带有正则表达式的sed来替换字符串fooFoo

"s:alpha/beta/foo.cpp:alpha/beta/Foo.cpp:"

在上述命令中,我使用冒号:来分隔sed命令中的正则表达式,但您也可以使用其他字符作为分隔符,如pipe|。我选择使用冒号而不是更常见的正斜杠/作为分隔符,以便无需转义文件路径中使用的正斜杠。
通过管道将git ls-files --stage传递给sed后,您应该会得到以下结果:
git ls-files --stage | sed "s:alpha/beta/foo.cpp:alpha/beta/Foo.cpp:"
100644 195ff081f7d0d37a60181de790ae1c6b9f177be8 0       alpha/beta/Foo.cpp
100644 0504de8997941bf10bcfb5af9a0bf472d6c061d3 0       LICENSE
100644 6293167f0eb7389b2f6f6b73e838d3a547787cbf 0       README.md
...etc...

步骤三:使用重命名后的文件创建新索引

现在,修改过的git ls-files --stage输出可以通过管道符号传递给git update-index --index-info,以在索引中重命名该文件。因为我们想要创建一个全新的索引来替换旧的索引,所以在调用git update-index命令之前需要设置一些环境变量,以指定索引文件的路径:

GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info

步骤四:替换旧索引

现在我们只需用新的索引替换旧的索引,这实际上就像是“重命名”了文件:

mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"

摘要

当所有内容组合在一起时,以下是完整的命令:

git filter-branch --index-filter '
git ls-files --stage | \
sed "s:alpha/beta/foo.cpp:alpha/beta/Foo.cpp:" | \
GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
git update-index --index-info && \
mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD

文档

  1. git filter-branch

  2. git ls-files

  3. git update-index

  4. Git环境变量


@escouten 很酷,很高兴它对你有用。顺便说一下,如果您想在过滤器后验证结果,只需执行 git diff --name-status refs/original/refs/heads/master,您将看到原始文件已被删除,新文件已添加。要确认这两个文件包含相同的内容,只需执行 git diff refs/original/refs/heads/master:foo.cpp :Foo.cpp。稍后我会将所有这些添加到我的答案中。 - user456814

0

我的.profile别名基于@cupcake的答案,修复了如何扩展变量的问题。

示例用法:

mvidx src/myfile.cs src/myfolder/myfile.cs origin/develop..feature/myfeature

~/.profile bash配置文件

alias mvidx=rewriteIndexToMoveFile

red="\e[0;31m"
green="\e[0;32m"

rewriteIndexToMoveFile() {
    if [ $# -ne 3 ] ; then        
        echo -e "Rewrite index to move a file in a range of commits."
        echo -e "Args: <from file path> <to file path> <range of commits>"
        echo -e "${green}Examples:"
        echo -e "mvidx src/myproject/myfile.cs src/myproject/subfolder/myfile.cs origin/develop..feature/myfeature"
        return
    fi

  fromFilePath=$1
  toFilePath=$2
  revisionRange=$3

  echo -e "Renaming ${red}$fromFilePath${nc} to ${red}$toFilePath${nc}."

git filter-branch --index-filter 'git ls-files -s \
    | sed "s|\t'"$fromFilePath"'|\t'"$toFilePath"'|" \
    | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info \
        && mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' \
  $revisionRange
}

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