如何在Git仓库中移动一个目录并保留所有提交记录?

75

假设我有一个包含以下目录结构的代码仓库:

repo/
  blog/
    _posts/
      some-post.html
  another-file.txt

我想将_posts移动到仓库的顶层,使结构看起来像这样:

repo/
  _posts/
    some-post.html
  another-file.txt

使用 git mv 很容易实现,但我想让历史记录看起来好像 _posts 一直存在于仓库的根目录中,并且我希望能够通过 git log -- _posts/some-post.html 获得 some-post.html 的完整历史记录。我认为可以使用 git filter-branch 进行一些魔法来实现这一点,但我还没有弄清楚具体如何操作。有什么想法吗?

4个回答

105
您可以使用子目录过滤器来实现此功能。
 $ git filter-branch --subdirectory-filter blog/ -- --all

编辑1: 如果你不想有效地将_posts作为根目录,可以使用tree-filter代替:

 $ git filter-branch --tree-filter 'mv blog/_posts .' HEAD

编辑2: 如果某些提交中不存在blog/_posts,上面的方法将失败。请改用以下方法:

 $ git filter-branch --tree-filter 'test -d blog/_posts && mv blog/_posts . || echo "Nothing to do"' HEAD

4
使用 --index-filter 更快,因为它不需要检出树形结构。 - Cascabel
7
索引过滤器确实更快,但是它不起作用,因为所示命令 不会 影响索引。如果您想使用索引过滤器,则只需要进行索引操作(例如,使用 git rm --cached 而不是 rm)。 - sehe
我能保留标签吗?在我的情况下,它们似乎已经消失了。 - Michael
1
如果您的标签未签名并且想要移动它们/使旧标签无效,请添加“--tag-name-filter cat”。命令为:git filter-branch --index-filter 'git read-tree --prefix=/ $GIT_COMMIT:_posts; git rm -r --cached _posts' -- --all - jthill
对于那些只是为了复制粘贴而来的人:请记得对所有分支执行 git push --force --all 命令。否则你可能会遇到一些有趣的情况。 - SiliconMind
不需要让最后一个命令变得那么复杂。要将现有文件夹移动到其他地方,无论它是否存在于所有提交中,只需运行git filter-branch --tree-filter 'git mv -k blog/_posts .'在这种情况下,索引过滤器将不起作用,因为您还修改了树对象(并且需要将它们签出),而不仅仅是索引。 -k开关将处理某些提交中的“不存在”文件。 - petrpulc

39

尽管Ramkumar的回答非常有价值和有益,但在许多情况下它都无法工作。 例如,当您想将一个包含其他子目录的目录移动到新位置时。

对于这种情况,man页面包含了完美的命令:

git filter-branch --index-filter \
  'git ls-files -s | sed "s-\t\"*-&NEWSUBDIR/-" |
   GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
   git update-index --index-info &&
   mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' HEAD

只需将NEWSUBDIR替换为您想要的新目录即可。 您还可以使用嵌套目录,例如dir1/dir2/dir3/-


12
由于从该命令或结果错误中立即看不出来,\t 在 os x 版本的 sed 上不起作用。有很多解决方法,但可能最快的方法是删除 \t 并通过键入 ctrl-v,<tab> 来替换为字面制表符。 - Jeremy Huiskamp
4
怎样指定原始文件夹,或者这只是移动整个分支?当我尝试从我想要移动的文件夹中运行此命令时,我收到“您需要从工作树的顶层运行此命令”的提示。 - joshcomley
1
太棒了。但是是的,sed命令确实有点让人困惑,所以试试只运行第二行来测试一下。也就是说,为了移除不必要的顶级目录,我使用了一个简单的 git ls-files -s | sed "s-\tdir1/dir2/dir3/-\t-" 命令。 - KCD
2
如果您筛选的提交实际上删除了所有文件,则git update-index不会创建文件“$GIT_INDEX_FILE.new”,因此mv命令会失败。 我最终在filter-branch脚本中使用以下内容: test -f \$GIT_INDEX_FILE.new && mv \$GIT_INDEX_FILE.new \$GIT_INDEX_FILE || touch \$GIT_INDEX_FILE - Knut Forkalsrud
1
@askb:您可以通过附加“; /bin/true”来跳过错误提交。请参阅这篇优秀的文章 - Roi Danton
显示剩余12条评论

2
创建了一个通用的脚本,用于任意移动/重命名:https://gist.github.com/xkr47/f766f4082112c086af63ef8d378c4304 示例:
git filter-mv 's!^!subdir/!'

➜ 将当前分支的所有提交中的所有文件移动到子目录 "subdir/" 中

git filter-mv 's!^foo/bar.txt$!foo/barbar.txt!'

➜ 将当前分支所有提交中的foo/bar.txt重命名为foo/barbar.txt


1

git filter-branch 已经不建议使用。你可以使用更方便的 git filter-repo:

git filter-repo --path-rename blog/_posts:_posts

请看这里
另外要注意的是安装方式有些问题,需要从pip安装并将git-filter-repo添加到$PATH中。请参考这里

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