git-subtree无法保留历史记录,因此我无法推送子树更改,如何修复此问题/避免此问题在未来出现?

18
我是一位有用的助手,可以为您翻译文本。

我一直在使用git-subtree扩展(https://github.com/apenwarr/git-subtree)来管理我们主项目中的子项目。它做的很好,除了当我尝试从我们的主项目中分离出对子项目所做的更改时失败。

例如,之前我已经完成了以下工作:

git subtree add -P Some/Sub/Dir --squash git@gitserver:lib.git master

将库代码引入到我们主项目的Some/Sub/Dir中。一切都进行得很顺利,然后我将更改推送到我们中央主项目裸git存储库。然后我决定对我在Some/Sub/Dir中本地版本的库进行更改,提交它,然后将其分离以将其推回lib.git存储库。
git subtree split -P Some/Sub/Dir -b some_branch

一切都按预期工作。不再需要本地副本,我将其删除。

从中央仓库克隆一个新的副本后,我对Some/Sub/Dir中的lib进行了一些更改,并决定将这些更改拆分出来并推回lib.git存储库。我尝试使用与以前相同的子树拆分命令,但是这次我得到以下输出:

1/      3 (0)
2/      3 (1)
3/      3 (1)
fatal: bad object d76a03f0ec7e20724bcfa253e6a03683211a7bb1

d76a03f0ec7e20724bcfa253e6a03683211a7bb1来自我添加子树时的操作:

commit 43b3eb7d69d5eb64241eddb12e5bd74fd0215083
Author: Ian Bond <ibond@onezero.com>
Date:   Fri Apr 22 15:06:50 2011 -0400

    Squashed 'Subtree/librepoLib/' content from commit d76a03f

    git-subtree-dir: Subtree/librepoLib
    git-subtree-split: d76a03f0ec7e20724bcfa253e6a03683211a7bb1

这实际上是指lib.git仓库中的一个提交。


我所了解的是(我是一个git新手,所以可能会有错误、忽略某些东西或者使用不正确的术语),'git subtree add --squash'命令将远程lib.git repo的整个历史记录引入到当前repo中,将其压缩成一个单独的提交(commit),然后将该提交添加到工作分支中。虽然lib.git提交历史记录保留在当前repo中,但它们是悬挂提交,因为除了通过压缩提交的文本之外,它们实际上没有被引用。只要这些悬挂提交存在,git-subtree就可以使用它们来执行拆分操作,但是由于push或pull不包含悬挂对象(或者如果我运行gc并完全修剪悬挂对象),这些悬挂提交就会丢失,git-subtree将不再具备执行拆分所需的必要信息。
我已经添加了一个脚本,可以完全重现我遇到的问题。

我的问题是:

1) 我现在有一些子树,想将它们合并到它们原始的仓库中,但是它们之间没有任何历史联系。我现在的想法是执行以下操作:

git subtree split -P Some/Sub/Dir 43b3eb7^.. --ignore-joins -b splitBranch

我希望将自“git subtree add”以来的所有历史记录拆分出来,并合并回原始仓库(幸运的是,自添加以来未进行任何更改)。这是最好的方法吗?您对如何执行合并有何建议?

2) 有没有办法让git-subtree按预期工作?我相信如果我在“git subtree add”上省略--squash参数,那么一切都会正常工作,但这会导致我的仓库注入大量不相关的历史记录。有没有某种方法可以保留所需的提交记录(最好不保留整个库的历史记录)?


感谢您抽出时间编写并发布了一个脚本来重现您的情况。您可以考虑直接将其包含在您的帖子中。这将增加更多的“垂直空间”(尽管有一个最大值,超过后会添加滚动条),但它会使您的问题完全“自足”。 - Chris Johnsen
就我个人而言,我更喜欢链接,因为它可以节省空间。不过我建议这应该是一个真正的链接,我已经编辑过了。我猜测楼主在提问时可能没有足够的声望来包含链接。然而,如果你要在谈论Git时使用Pastebin,那么肯定应该使用Gist - Tom Anderson
6
是的,我不想在帖子中间放一个大脚本。我其实很惊讶Stack Overflow没有任何粘贴板或文件上传支持。这将是一种保证帖子始终完整的好方法。当你找到某个答案时,所有链接都失效时,这总是很痛苦的。 - Screndib
2个回答

15
git subtree split的目的是在子树的原始历史记录上创建一些新的提交(代表最初在子树本地目录中进行的“本地”更改)。由于它直接涉及子树的原始历史记录(作为第一个重新编写的与子树有关的本地提交的父提交),因此只有在子树的原始历史记录存在的情况下才能执行分割操作。
考虑一下使用git subtree split生成的历史记录要做什么。您可能希望将其推送到存储库,在该存储库中将其合并到“上游”历史记录的其余部分中。为了使此合并操作有意义,拆分历史记录需要基于原始历史记录本身1
可能安排用户具有子树的原始历史记录的最可靠方法是在文档中发布子树的上游存储库的URL,并让他们为其定义一个远程(在单个存储库中具有“不相关”的远程是完全可以的)。例如:
如果需要处理 Some/Sub/Dir 的“上游”(以拉入外部更改或推出本地更改),请在使用 git subtree 之前定义和更新库的存储库的远程。
git remote add lib git@host:the-lib-repository &&
git fetch lib
即使您不使用--squash,也需要执行类似以下操作,因为用户需要知道从哪里获取新的上游提交(以及推送新的拆分生成提交的最终位置)。
使用--squash可以在您的主项目中获得“干净”的历史记录,并且仅那些需要处理子树“上游”的用户才必须将其对象放在其存储库中。
看起来您对对象模型有很好的理解。您是正确的,git subtree add --squash拉取的历史记录会变成悬空状态,但是git subtree split仍然可以使用它,直到它被修剪掉。
(针对您的复制脚本)您能够在repoMainClone中成功拆分,仅因为本地克隆会自动硬链接(或复制).git/objects/中的所有文件(从而获取访问repoLib中悬挂(或几乎悬挂2)对象的repoMain 的副本),而不是使用常规的“pack协议”传输(这将限制传输的对象仅限于传输的引用所需的对象;即省略任何来自repoLib的内容)。您的repoMainPull等效于克隆file://"$(pwd)"/repoMain repoMainCloneFilefile:// URL会强制本地克隆使用基于打包的传输,而不仅仅是链接/复制所有内容)。
实际上,您可以直接合并不相关的历史记录,但是您失去了进行三路合并的能力(因为没有共同的祖先)。这将是一个相当大的牺牲。您提出的git subtree split -P Some/Sub/Dir 43b3eb7^.. --ignore-joins …(其中43b3eb7是由git subtree add --squash …生成的合成提交)将生成不相关的历史记录(除非它需要是43b3eb7 ..,因为43b3eb7 ^表示“43b3eb7的第一个父级”,而43b3eb7没有父级)。我不确定git subtree split是否设计用于处理此类范围。 git subtree split的文档只是说<commit>,但从未真正提到其目的。阅读代码表明,它默认为HEAD,这可能表明它旨在是指定应该用于拆分的历史记录“尖端”的单个提交。此外,打开调试输出将显示一个消息incorrect order:,这可能表明使用范围参数将拆分操作置于意外情况中(它正在预计在处理提交本身之前已经处理了提交的所有父项,但范围确保43b3eb7(它是子树合并提交的父项)永远不会被处理)。如果要生成“不相关”的历史记录并尝试以某种方式使用它,则可以只使用--ignore-splits,并省略范围:git subtree split -P Some/Sub/Dir --ignore-joins …
它们实际上并不会立即悬挂在git subtree add --squash之后,因为它们仍然由FETCH_HEAD引用。但是,一旦进行了不相关的提取,它们就会变成真正的悬挂状态

感谢您详细的回复,这解决了一些问题,特别是关于克隆与推送/拉取的区别。 - Screndib
另一个潜在的选项是您可以添加引用对象的标签。有了标签,可以确保垃圾收集器不会删除对象,并且标签将自动传播到克隆存储库的其他人。当然,这对于从上游获取东西没有帮助,但在某些情况下可能很有用。 - benno

1

Chris Johnsen的答案非常正确,解释了为什么克隆情况下拆分可行而拉取不可行。

对于问题中提供的解决方法,以及Chris Johnsen答案中脚注2所解释的内容,我可以确认git subtree split -P Some/Sub/Dir -b splitBranch --ignore-joinsgit subtree split -P Some/Sub/Dir -b splitBranch 43b3eb7..实际上产生了相同的提交和相同的分支,可以反映本地仓库中所做的修改,但不能推送到原始repoLib仓库,因为它们没有共同的祖先,即使git diff显示d76a03f0ec7e243b3eb7d69d是相同的。

因此,为了使子树推送在拉取情况下工作,必须添加并获取原始repoLib远程仓库,以便存在d76a03f0ec7e2以产生具有与原始repoLib共同祖先的分支。

原来的脚本在Linux下无法顺利运行,这里提供一个新的:http://pastebin.com/3NAQKEz9


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