将文件和目录移动到子目录中并保留提交历史记录

98

我该如何将目录和文件与提交历史一起移动到子目录中?

例如:

  • 源目录结构:[项目]/x/[文件和子目录]

  • 目标目录结构:[项目]/x/p/q/[文件和子目录]


1
我不理解这个问题的大部分答案,我尝试了所有的方法,但总是要么出现错误,要么什么也没有发生,所以我只好剪切.git文件夹并将其粘贴到子文件夹中...猜猜怎么着?它奏效了! - Ayyash
请参考相关/重复问题 https://dev59.com/DFwY5IYBdhLWcg3wHkqK。我的答案 https://dev59.com/DFwY5IYBdhLWcg3wHkqK#50547331 涵盖了Windows。 - Garret Wilson
有人知道如何反过来做吗?在子文件夹中有一个可重现的问题,然后将一些父文件/文件夹添加到此可重现的问题中? - Martin Krung
6个回答

103

补充 bmargulies评论,完整的顺序是:

mkdir -p x/p/q      # make sure the parent directories exist first
git mv x/* x/p/q    # move folder, with history preserved
git commit -m "changed the foldername x into x/p/q"

先试一下,看看移动的预览效果:

git mv -n x/* x/p/q

Wolfgang 评论:

如果你正在使用bash,你可以通过使用扩展的glob表达式(使用shopt内置命令)避免尝试将一个文件夹移动到自己内部的问题:

shopt -s extglob; git mv !(folder) folder

Captain Man评论中报告 需要执行以下操作:

mkdir temp 
git mv x/* temp
mkdir -p x/p/q
git mv temp x/p/q
rmdir temp;

背景:

我正在使用带有Cygwin的Windows。
我刚意识到我做了shopt -s extglob的示例错误,所以我的方法可能并不必要,但我通常使用zsh而不是bash,并且它没有shopt -s extglob命令(虽然我确信有替代品),因此这种方法应该适用于各种shell(如果特别陌生,则使用您的shell的mkdirrmdir替换)。


作为替代方案,spanky评论中提到了git mv-k选项

跳过会导致错误条件的移动或重命名操作。

git mv -k * target/

这样就可以避免“无法将目录移到其本身”的错误。


2
无法将 x 移动到其子目录。 git mv ... 可以完成此任务,如果没有进一步更改,则不需要 git add ... 任何内容。 - vonbrand
如何在这里使用git mv?它抱怨说我不能将一个目录移动到其自身的子目录中。 - Abhinav Manchanda
2
如果您正在使用bash,可以通过使用扩展的glob来避免尝试将文件夹移动到自身的问题,方法如下:shopt -s extglob; git mv !(folder) folder - Wolfgang
7
我刚学会了可以使用 git mv -k * target/ 命令来避免“无法将目录移动到自身”的错误。 - Spanky
1
@Spanky 很有趣。我已经将您的评论包含在答案中以增加可见性。 - VonC
显示剩余11条评论

52

即使文件移动位置,Git仍然能够很好地跟踪内容,所以git mv是移动文件的正确方式,因为它们曾经属于x,但现在属于x/p/q,因为项目发展到了这个阶段。

然而,有时候在项目的历史记录中将文件移动到子目录也是有原因的。例如,如果文件不小心被放错位置,并且有更多提交已经完成,但是这些提交与错误放置的文件毫不相关。如果所有这些操作都在本地分支上进行,我们希望在推送之前清理掉混乱。

问题中提到“包括提交历史”,我会理解为以这种方式重写历史记录。可以使用以下命令完成此操作:

git filter-branch --tree-filter "cd x; mkdir -p p/q; mv [files & sub-dirs] p/q" HEAD

文件随后会在整个历史记录中出现在 p/q 子目录中。

树过滤器非常适合小型项目,它的优点是命令简单易懂。但对于大型项目而言,这种解决方案并不具备良好的可扩展性,如果性能很重要,可以考虑使用索引过滤器,如此答案所述。

请注意,如果重写触及到已经推送过的提交,则不应将结果推送到公共服务器上。


8
谢谢您,让我今天过得愉快!我想把一棵树下的所有文件都“移动”到一个新的子目录中,我用了您的方法mv * ./the-subdir 但出现了错误:“无法将 the-subdir 移动到其自身的子目录中。”我的解决方案是在tree-filter命令中使用 mv the-subdir /tmp; mv * /tmp/the-subdir mv /tmp/the-subdir ./。非常好用。 - Alexander
除非我使用 git mv [文件和子目录] p/q 而不是调用原始的 mv,否则我无法让它正常工作;在 Mac 上运行。但最终它确实对我很有帮助,所以谢谢! - d4vidi
1
我使用了 mv * .* the/subdir/ || true 命令来移动文件和点文件,并忽略错误信息。 - joeytwiddle

14

我可以看到这是一个旧问题,但我仍然觉得有义务回答。我的当前解决方案来自于git book中的一个示例。我使用了一个高效的--index-filter,直接在索引上移动文件,而不是使用低效的--tree-filter

git filter-branch -f --index-filter 'PATHS=`git ls-files -s | sed "s/^<old_location>/<new_location>/"`;GIT_INDEX_FILE=$GIT_INDEX_FILE.new; echo -n "$PATHS" | git update-index --index-info && if [ -e "$GIT_INDEX_FILE.new" ]; then mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"; fi' -- --all

这是书中示例的一个专业化版本,我也曾在某些特殊情况下使用相同的示例来批量重命名提交历史中的文件。如果要移动子目录中的文件:请记得在路径中用 \ 转义 / 字符,以使 sed 命令按预期工作。

示例:

Project Directory
                 |-a
                 |  |-a1.txt
                 |  |-b
                 |  |  |-b1.txt

将b目录移动到项目根目录:
git filter-branch -f --index-filter 'PATHS=`git ls-files -s | sed "s/a\/b\//b\//"`;GIT_INDEX_FILE=$GIT_INDEX_FILE.new; echo -n "$PATHS" | git update-index --index-info && if [ -e "$GIT_INDEX_FILE.new" ]; then mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"; fi' -- --all

结果:

Project Directory
                 |-a
                 |  |-a1.txt
                 |
                 |-b
                 |  |-b1.txt

1
通过您的命令行,我得到了 fatal: malformed index info -n 100644。使用 git filter-branch -f --index-filter 'git ls-files -s | sed "s/<old_location>/<new_location>/" | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' HEAD 在我的情况下可以解决问题。别忘了用 \/ 转义 / - tanguy_k
1
如果您在 sed 中使用 : 作为分隔符,那么您就不必担心需要转义 / - Mateen Ulhaq
另外举个例子,如果你想把仓库根目录 . 下的所有文件/文件夹移动到 ./newfolder1/newfolder2/ 中,你需要输入以下命令:git filter-branch -f --index-filter 'git ls-files -s | sed "s:\t\"*:&newfolder1/newfolder2/:" | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' HEAD。希望这能帮助到其他人! - virtualdj
要举另一个例子,如果你想要将存储库根目录 . 中的所有文件/文件夹移动到 ./newfolder1/newfolder2/ 中,你需要输入以下命令:git filter-branch -f --index-filter 'git ls-files -s | sed "s:\t\"*:&newfolder1/newfolder2/:" | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' HEAD。希望这对其他人有所帮助! - undefined
使用将"b"移动到根目录的第二个示例后,过一段时间后出现以下错误:fatal: 无法创建 'C:/stuff/test_rewrite2/.git-rewrite/t/../index.new.new.new.new.new.new.new.new.new.new.lock':文件名太长。 我使用的是Git for Windows版本2.41.0中的Git Bash,并使用了您答案中的确切命令。 - David Balažic

8

git mv命令是最简单的方法。然而,在我的Linux系统上,我需要提供-k标志以避免收到一个错误,即无法将文件夹移动到自身中。我能够通过使用...

mkdir subdirectory
git mv -k ./* ./subdirectory
# check to make sure everything moved (see below)
git commit

作为一个警告,这将跳过所有会导致错误情况的移动,因此在提交之前和移动之后,您可能需要检查一切是否正常。

注意:-k标志在Mac OS X(Sierra)上不起作用,似乎在Ubuntu上也不行。 - Translunar

1

如果由于某些原因您不想使用git mv命令,这里有一种替代方法:

  • 确保您没有任何未提交的更改。

    git status

  • 只需移动包含.git文件夹的文件夹到新文件夹中:

    mkdir new_folder mv old_folder new_folder

  • 然后将移动后的文件夹中的.git old_folder移回到基础文件夹中:

    mv new_folder/old_folder/.git new_folder/

  • 然后将检测到的所有更改(列出为将文件添加到其新位置并从其旧位置删除)进行暂存和提交。


1

我的建议是:先使用git fast-export导出文件,然后编辑导出的文件,最后使用git fast-import导入。


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