如何重新将git仓库的根目录更改为父文件夹且保留历史记录?

53

我在/foo/bar/baz中有一个Git仓库,其中包含大量提交历史记录和多个分支。

现在我希望/foo/qux/foo/bar/baz在同一个仓库中,这意味着它们都需要位于以/foo为根的仓库中。但是,我想保留对/foo/bar/baz所做更改的历史记录。

一开始我考虑使用git format-patch并接着使用apply命令,但是这样会导致提交信息丢失。

因此,

我需要重新设置仓库根目录

(1) 到任意更高的祖先目录 (2) 同时通过使其看起来一直在提交到/foo/bar/baz来保留我的提交历史记录


谢谢大家,我正在尝试你们的建议。我不能移动 /foo/baz,但其他两个看起来很有希望。 - masonk
1
我不认为这个问题是重复的,因为这两个问题详细说明了4个不同的问题和答案:1a. 如何将我的git repo向下移动一个目录?1b. 如何将我的git repo向下移动一个目录并使其看起来一直都是这样?2a. 如何将我的git repo向上移动一个目录?2b. 如何将我的git repo向上移动一个目录并使其看起来一直都是这样?1a和2a的答案基本相同,但1b和2b完全不同,提供了3个不同有价值的答案。 - TTT
正如原帖和其他人所指出的那样,将此问题标记为已有其他答案是不正确的;链接到的问题实际上是这个问题的相反。此外,这里给出的被接受的答案既独特又优于任何其他候选答案。 - cueedee
我投票支持重新开放。 - masonk
1
@DanielW:如果我可以自由地重新构建文件布局,你的方法是可行的。但在原帖中这不是一个选项。 - masonk
显示剩余4条评论
8个回答

25

你需要的是git filter-branch,可以将整个仓库移动到一个子目录中,并通过使其看起来一直如此来保留历史记录。在使用之前备份你的仓库!

以下是具体步骤,在/foo/bar中运行:

git filter-branch --commit-filter '
    TREE="$1";
    shift;
    SUBTREE=`echo -e 040000 tree $TREE"\tbar" | git mktree`
    git commit-tree $SUBTREE "$@"' -- --all
这将使得名为/foo/bar的代码库具有另一个名为“bar”的子目录,并包含其整个历史内容。然后,您可以将整个代码库移至foo级别并添加baz代码。

更新:

好的,这里是正在发生的事情。提交是指向“树”(将其视为代表整个文件系统子目录内容的SHA)的链接,以及一些“父”SHA和一些元数据链接作者/消息等。git commit-tree命令是将所有这些内容组合在一起的低级位。 --commit-filter参数在过滤器过程中被视为shell函数并在git commit-tree位置运行,必须像它那样运作。

我所做的是获取第一个参数,即要提交的原始树,并使用git mktree来构建一个新的“树对象”,该对象通过子文件夹进行描述,而另一个低级别的git命令则用于在其中执行操作。为此,我必须将某些看起来像git树的东西进入管道,即一组(mode SP type SP SHA TAB filename)行;因此echo命令。 mktree的输出然后替代了第一次链接到真正的commit-tree时的第一个参数;"$@"是一种传递所有其他参数不变的方式,已经使用shift除去了第一个参数。有关信息,请参见git帮助mktreegit help commit-tree

因此,如果您需要多个级别,则必须嵌套几个额外级别的树对象(尚未经过测试,但这是一般想法):

git filter-branch --commit-filter '
    TREE="$1"
    shift
    SUBTREE1=`echo -e 040000 tree $TREE"\tbar" | git mktree`
    SUBTREE2=`echo -e 040000 tree $SUBTREE1"\tb" | git mktree`
    SUBTREE3=`echo -e 040000 tree $SUBTREE2"\ta" | git mktree`
    git commit-tree $SUBTREE3 "$@"' -- --all

这应该会将真正的内容下移至 a/b/bar(注意顺序已颠倒)。

更新:整合了Matthew Alpert下面的改进。如果没有--all,这只能在当前已检出的分支上工作,但由于问题是关于整个存储库的,所以这种方式更加合理,而不是逐个分支操作。


1
好的,我已经添加了一个部分来解释正在发生的事情,并提供一种更或多或少实现你真正想要的方式。 - Walter Mundt
这个有点麻烦。不知道为什么mktree会连同echo输出的-e参数一起获取? - getWeberForStackExchange
weberwithoneb:你在运行什么操作系统?GNU和bash echo都接受-e,但来自旧Unix系统(也许是MacOSX?)的shell和/bin/echo二进制文件可能不支持,此时您将获得-e和文字反斜杠-T而不是制表符。您可以通过删除-e并使用控制-V TAB在echo命令中插入文字制表符来解决此问题。(空格无效,mktree对其空白字符很挑剔。) - Walter Mundt
2
应该使用/bin/echo,否则echo -e可能无法工作。还可以使用--tag-name-filter cat来避免"WARNING: You said to rewrite tagged commits, but not the corresponding tag."的警告吗? - Julien Carsique
2
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Screenack
显示剩余7条评论

16

不要创建一个新的代码库,将当前代码库中的内容移动到正确的位置:在当前目录下创建一个名为bar的新目录,并将当前内容移动到其中(这样你的代码就在/foo/bar/bar中)。然后在新的bar目录旁边创建一个baz目录(/foo/bar/baz)。运行命令mv /foo /foo2; mv /foo2/bar /foo; rmdir /foo2,就完成了 :)

Git 的重命名跟踪功能意味着您的历史记录仍将起作用,而 Git 对内容的哈希处理意味着即使您移动了文件,仍然可以在代码库中引用相同的对象。


1
我不能随意移动东西。我的存储库初始化在错误的位置,但文件层次结构是正确的。尽管如此,谢谢你的建议。 - masonk
1
我并不是建议最终结果与起始状态有任何区别,只是将事物移动作为中间步骤可能会帮助你获得所需的结果。如果我的回答没有表达清楚,我很抱歉。请注意,filter-branch 的答案最终也会使应在 /foo 中的存储库位于 /foo/bar 中并需要移动。 - Andrew Aylett
我很难理解你的描述,你能否重新格式化一下呢?它相当密集,很难看清楚你到底在做什么。 - Profpatsch
foo2 是从哪里来的? - Profpatsch
3
哦,我明白了。也许做个图形会简化这个过程。我认为这个方法并没有接受答案那么“hacky”,因为它不涉及破坏整个代码库的内部结构的风险。 - Profpatsch

8
这里是针对“如何将我的git repo上移一个或多个目录并使其看起来一直是这样?”的具体解答。随着git >= 2.22.0的出现,git filter-repo可以被利用来重写历史记录以使其看起来该父目录一直都是其中的一部分。这与@Walter-Mundt's answer使用git filter-branch所实现的相同,但更简单且执行起来不那么脆弱。请注意,现在git filter-repogit filter-branch本身宣传为更安全的替代方案

因此,假设您的存储库位于/foo/bar/baz并且您想将其移动到/foo

首先,为了在重写历史记录时防止对工作区中的文件进行任何更改,请将存储库暂时转换为所谓的"裸" 存储库,如下所示:

cd /foo/bar/baz
git config --local --bool core.bare true

实际的历史重写现在可以直接在.git目录中完成:
cd ./.git
git filter-repo --path-rename :bar/baz/

这将重写存储库的完整历史记录,就好像每个路径始终都已经加上了bar/baz/(如果存储库的根目录向上两级,则会这样做)。实际文件不会受此操作影响,因为现在这是一个裸仓库。
最后,取消裸仓库状态,将.git目录移动到指定位置并进行重置:
git config --local --bool core.bare false
cd ..
mv ./.git ../..
cd ../..
git reset

我认为git reset可以取消仓库被改变为裸仓库后又被转换回来的影响。在执行git reset之前,试一下git status以理解我的意思。
最终的git status应该证明一切都正常,除了需要处理/foo/qux中的一些新的未跟踪文件。
注意:如果你尝试对未克隆的仓库进行上述操作,git filter-repo将拒绝完成这一操作,除非你使用--force选项强制执行。请备份好数据并做好心理准备。

6

最常见的解决方案

在大多数正常情况下,git会相对于其位置(即.git目录)查看所有文件,而不是使用绝对文件路径。

因此,如果您不介意在历史记录中显示已将所有内容移动到上一层的提交,则有一个非常简单的解决方案,即将git目录移动。唯一稍微棘手的是确保git理解文件是相同的,它们只是相对于它移动了:

# Create sub-directory with the same name in /foo/bar
mkdir bar

# Move everything down, notifying git :
git mv file1 file2 file3 bar/

# Then move everything up one level :
mv .git ../.git
mv bar/* .
mv .gitignore ../

# Here, take care to move untracked files

# Then delete unused directory
rmdir bar

# and commit
cd ../
git commit

唯一需要注意的是,在移动到新目录时正确更新 .gitignore,以避免暂存不想要的文件或忘记某些文件。

奖励方案

在某些情况下,当 git 看到与已删除文件完全相同的新文件时,它能够自行判断文件已被移动。在这种情况下,解决方案甚至更简单:

mv .git ../.git
mv .gitignore ../.gitignore

cd ../
git commit

再次提醒,小心你的.gitignore文件


我已经成功地完成了添加应用程序的第二部分,谢谢。 - bcag2

6
我有一个解决方案,似乎没有人提过:
我需要在我的代码库中包含来自父目录的文件(实际上是将代码库上移一个目录)。
我通过以下方式实现:
1. 将所有文件(除了 .git 文件夹)移动到一个与原目录同名的新子目录下,并使用 git mv 命令告诉 git 有这个变化。
2. 将父目录中的所有文件移动到现在空的(除了 .git/)当前目录下,并使用 git add 命令告诉 git 有这个变化。
3. 提交整个更改到代码库中。代码库本身没有移动,使用 git commit 命令提交所有更改。
4. 使用命令行操作将当前目录向上移动一个级别。
希望这可以帮助下一个遇到同样问题的人。对于我而言,上面的答案看起来过于复杂和可怕了。尽管和 Andrew Aylett 的答案类似,但我的情况有点不同,我想要一个更通用的方法。

1
天啊,只需将.git目录移动到上一级目录,并在其中提交-a,这样会容易得多。我必须停止害怕git! - Jon Carter
2
我简直不敢相信这是那么简单 - 感谢你!另外,小心你的.gitignore文件。其中的文件夹路径也是相对的,第一次尝试时,因为它在.gitignore中列出,我错过了想要的目录。 - Roy Pardee
1
这个比被接受的答案更好。应该有更多的赞成票。 - Dr. Ehsan Ali
2
已经有一段时间没有做这个了,我的第一个评论是在我意识到几乎相同的事情有更简单的方法之后发出的。试试这个:只需将 .git 文件夹向上移动一个目录,然后使用 git add .git commit 命令告诉 git 关于所有内容。你确实可以使用图形界面窗口移动目录,如何执行 git 操作取决于你如何使用 git。我给出的命令是在终端窗口中执行的(如果你使用的是 Windows,则可能是 Git Bash)。 - Jon Carter
1
@JonCarter,谢谢你快速回复并提供精确的解答。我认为你的第一种解决方案最适合我,因为第二种解决方案可能会导致 git 不考虑文件已经被移动而把它们全部视作删除并创建了很多新文件。我总是必须使用 git mv 否则就会遇到这个问题。所以我想我会使用你的第一种解决方案。非常感谢! - Vic Seedoubleyew
显示剩余4条评论

5
这是对 Walter Mundt 的回答的补充。我本来想在他的回答下评论,但是我没有足够的声望。
所以 Walter Mundt 的方法很好,但它只适用于一次一个分支。而且在第一个分支之后,可能会出现需要使用-f强制执行操作的警告。因此,要同时为所有分支执行此操作,只需在结尾处添加“-- --all”即可:
git filter-branch --commit-filter '
    tree="$1";
    shift;
    subtree=`echo -e 040000 tree $tree"\tsrc" | git mktree`
    git commit-tree $subtree "$@"' -- --all

若要对特定分支执行此操作,请将它们的名称添加到结尾处,但我无法想象为什么您要更改仅部分分支的目录结构。

在 git filter-branch 的 man 页面中可以了解更多信息。但是,请注意关于使用此命令后可能出现推送困难的警告。请确保您知道自己在做什么。

如果有任何潜在问题,我会感激更多的意见。


3
这个方法对我最有效。然而我的bash(Ubuntu Server 12.04)无法正确解析echo -e参数(我得到了致命错误:“...输入格式错误:-e 040000 tree ...”),所以我创建了别名echo ='echo -e',并从命令中删除参数使其正常工作。 - tishma

2
除了已接受的答案能帮我使其工作之外,还有一个小提示: 当我将列出的内容放在 shell 脚本中时,由于某种原因,-e 仍然存在。 (很可能是因为我不太擅长处理 shell 脚本) 当我删除 -e 并将引号移至包括所有内容时,它就可以正常工作了。 SUBTREE2=echo "040000 tree $SUBTREE1 modules" | git mktree 请注意 $SUBTREE1 和 modules 之间有一个制表符,这就是 -e 应该解释的相同 \t。

2
您可以使用 /bin/echo 代替 echo 并保留 -e - Julien Carsique
1
为了进一步解释Julien在这里的评论,echo命令通常在各种shell中作为内置实现。根据如何实现它,选项可能会有不同的工作方式。正如Julien建议的那样,可以通过调用/bin/echo来跳过使用哪个echo的问题,它是一个独立的可执行文件,具有标准行为,依赖于用户可能正在运行的任何特定shell的细节。 - Jon Carter

0

你可以在 foo 中创建一个 git 仓库,并通过 git 子模块 引用 bazbar

这样,barbaz 就可以共存,并且它们的完整历史记录都会被保留。


如果你确实只想要一个存储库(foo),同时又需要bar和baz的历史记录,那么就需要使用一些接枝技术或子树合并策略


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