如何使用index-filter等工具从Git仓库中提取带有提交历史记录的单个文件?

54

我有一个从SVN转换到Mercurial再到Git的Git仓库,我想要提取其中一个源文件。此外,我的文件名中有空格和奇怪的字符,如(编码不匹配导致Unicode ä损坏)。

我该如何从仓库中提取一个文件并将其放置在新仓库的根目录下?


1
这就是我需要的。顺便说一下,https://dev59.com/R2025IYBdhLWcg3wclq- 不是任何子目录过滤问题的克隆。提取文件需要同时使用 --subdirectory-filter 步骤和 --index-filter 或 --tree-filter。 - peterhil
1
我想要的是一个单文件包,其中提供了一棵 trie 树。我希望在其他项目中也能使用它,并在 Github 上发布。但是我的存储库中有一些代码,我不想公开开源(至少现在还不想)。 - peterhil
1
截至2.24(2019年3月7日),git-filter-repo是git-filter-branch的推荐替代品。 - Stephen
相关:如何将 Git 存储库中的单个文件拆分为新存储库?。但不涵盖 Unicode 细节。 - idbrii
6个回答

59

一个更快,更易于理解的过滤器,可以实现相同的功能:

git filter-branch --index-filter '
                        git read-tree --empty
                        git reset $GIT_COMMIT -- $your $files $here
                ' \
        -- --all -- $your $files $here

2
这对我来说完美地运作了。我添加了一个 --prune-empty 参数来删除任何空提交。 - Aaron Jensen
1
@AaronJensen 最后一行的 --all -- $your $files $here 会传递给 git rev-list,这个命令会在 filter-branch 运行之前对提交记录进行修剪。这比让 filter-branch 无意义地加载索引、运行过滤器、创建新树和提交再将其全部丢弃要快得多,因为它只处理那些与指定文件有关的提交。尽管如此,添加它也不会有任何影响。 - jthill
3
我如何将此应用于单个分支?将“-- --all”替换为“-- branchname”吗? - Mr_and_Mrs_D
3
对我而言,这使得涉及到该文件的提交都被保留了,但它们全部是空的。文件本身在创建该文件的提交中以其当前状态添加(即不是实际上当时的状态)。 - Mahmoud Al-Qudsi
2
我不确定你为什么认为我使用了 cmd。实际上,这是在 Linux 下的 fish 中进行的。 - Mahmoud Al-Qudsi
显示剩余7条评论

13

看起来并不是特别容易,这就是为什么尽管有许多类似于git [index-filter|subdirectory-filter|filter-tree]的问题,我仍然会回答自己的问题,因为我需要使用所有之前的方法才能实现这个!

首先,一个快速提醒,即使在将Git存储库中的一组文件拆分为它们自己的存储库,保留相关历史记录的评论中,也有类似的拼写错误。

SPELL='git ls-tree -r --name-only --full-tree "$GIT_COMMIT" | grep -v "trie.lisp" | tr "\n" "\0" | xargs -0 git rm --cached -r --ignore-unmatch'
git filter-branch --prune-empty --index-filter "$SPELL" -- --all

无法处理像 imaging/DrinkkejaI<0300>$'\302\210'.txt_74x2032.gif 这样命名的文件。

aI<0300>$'\302\210' 部分曾经是一个单独的字母:ä

因此,为了提取单个文件,除了使用 filter-branch,我还需要执行以下操作:

git filter-branch -f --subdirectory-filter lisp/source/model HEAD

或者,您可以使用 --tree-filter:

(这个测试很有必要,因为文件先前在另一个目录中,请参见:如何将Git存储库中的目录移动到所有提交中?
MV_FILTER='test -f source/model/trie.lisp && mv ./source/model/trie.lisp . || echo "Nothing to do."'
git filter-branch --tree-filter $MV_FILTER HEAD --all

要查看文件曾用过的所有名称,请使用:

git log --pretty=oneline --follow --name-only git-path/to/file | grep -v ' ' | sort -u

根据http://whileimautomaton.net/2010/04/03012432所述:

之后按照以下步骤进行:

$ git reset --hard
$ git gc --aggressive
$ git prune
$ git remote rm origin # Otherwise changes will be pushed to where the repo was cloned from

5
我不确定如何遵循这些指示,这个答案的文本似乎提出了几条可能的路线。我没有看到步骤。 - ThorSummoner
也许你应该查看关于 filter-branch 命令和重写历史的 git 文档:- http://git-scm.com/docs/git-filter-branch - http://git-scm.com/book/en/v2/Git-Tools-Rewriting-History - peterhil

12

请注意,如果您将此步骤与将所需文件移动到新目录的附加步骤相结合,事情会变得更加容易。

这可能是一个非常常见的用例(例如,将所需的单个文件移动到根目录)。
我使用git 1.9这样做(先移动文件,然后删除旧树):

git filter-branch -f --tree-filter 'mkdir -p new_path && git mv -k -f old_path/to/file new_path/'
git filter-branch -f --prune-empty --index-filter 'git rm -r --cached --ignore-unmatch old_path'

您甚至可以轻松使用通配符来获取所需文件(而不必使用grep -v等工具)。

我认为('mv'和'rm')也可以在一个filter-branch中完成,但对我没用。

我没有尝试过使用奇怪的字符,但我希望这样能有所帮助。让事情变得更加简单似乎总是一个好主意。

提示:
对于大型仓库,这是一项耗时的操作。因此,如果您想执行多个操作(例如获取一堆文件,然后将它们重新排列在“new_path/subdirs”中),那么尽早执行“rm”部分是一个很好的主意,以便获得更小、更快的树。


我也在Ubuntu 12.04和Git 1.7.x上尝试了它,并得到以下结果: *权限被拒绝的问题也出现在Ubuntu上
  • Git 1.7.x在执行我上面提到的命令时表现不佳(因为只有一个文件匹配,它总是被重命名为应该移动到的目录)。因此,我建议使用Git 1.9.x,我正在我的Windows机器上运行它。
- Roman
重新修改了我的帖子,因为我大部分的问题似乎是由于我缺乏bash技能所导致的 -> 现在使用' && '代替'|'来组合命令 - Roman
在 Git 2.2.1 中,第一步对我无效。存储库没有任何更改。 - xixixao
我有时也会遇到这个问题,即第一步没有改变任何东西。结果总是mv命令没有移动任何文件,因为路径没有匹配任何文件(请注意,git不保留有关空目录的任何信息)。 - Roman
1
第二步为我删除了所有文件(旧路径为),为什么不使用 git filter-branch -f --subdirectory-filter new_path -- --all 呢? - jan-glx
显示剩余2条评论

9

我在这里找到了一种使用git log和git am的优雅解决方案:

https://www.pixelite.co.nz/article/extracting-file-folder-from-git-repository-with-full-git-history/

如果链接失效,以下是操作步骤:

  1. in the original repo,

    git log --pretty=email --patch-with-stat --reverse --full-index --binary -- path/to/file_or_folder > /tmp/patch
    
  2. if the file was in a subdirectory, or if you want to rename it

    sed -i -e 's/deep\/path\/that\/you\/want\/shorter/short\/path/g' /tmp/patch
    
  3. in a new, empty repo

    git am < /tmp/patch
    

4
以下内容将重写历史记录,仅保留与您提供的文件列表相关的提交。您可能希望在克隆存储库中执行此操作,以避免丢失原始历史记录。
FILES='path/to/file1 other-path/to/file2 file3'
git filter-branch --prune-empty --index-filter "
                        git read-tree --empty
                        git reset \$GIT_COMMIT -- $FILES
                " \
        -- --all -- $FILES

然后您可以通过常规的合并(merge)或者变基(rebase)命令,将新分支合并到目标代码库中,具体取决于您的用例。


2
现在有一个新的命令git filter-repo。它具有更多的可能性和更好的性能。

有关详细信息,请参见手册页面项目页面进行安装。

删除除src/README.md之外的所有内容,并将其移动到根目录:

git filter-repo --path src/README.md
git filter-repo --subdirectory-filter src/

--path 选择单个文件,--subdirectory-filter 移动该目录的内容到根目录。


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