何时使用git子树?

126
< p > git subtree 解决了什么问题?何时以及为什么应该使用该功能? < p > 我已经阅读过它用于 存储库分离 。但是,我为什么不创建两个独立的存储库,而不是将两个不相关的存储库粘在一起呢? < p > 这个GitHub教程说明了如何执行Git子树合并。 < p > 我有点了解如何使用它,但不知道何时(用例)和为什么以及它如何与 git submodule 有关。当我依赖另一个项目或库时,我会使用子模块。
(注:已保留HTML标记)

2
“仓库分离”不等于“无关的仓库”,它考虑到你的仓库中的依赖关系,而你又不想使用子模块(可能是因为你不喜欢它们不透明,或者子模块提交中的路径与主 Git 仓库中的路径不匹配)。 - cyphar
1
@cyphar:您的意思是submodulesubtree都更或多或少实现了相同的目标,即合并相关项目,并且唯一的区别是submodule可能稍微不太透明,更新子模块是一个两步操作,而subtree的缺点是提交消息将在两个项目之间混合吗? - Lernkurve
1
在某些情况下,这并不是一个缺点。例如,如果您需要对包含子树的存储库进行二分,并且在依赖项中引入了错误,则可以在子树中找到引入错误的确切提交。使用子模块,您只会发现导致子模块版本更新的提交引起了错误,如果您想快速找到在主项目中导致错误的子模块提交,则有点棘手。 - cyphar
1
这是一篇文章,通过实际示例比较了git子树和git子模块的区别:https://nering.dev/2016/git-submodules-vs-subtrees/ - 8ctopus
6个回答

93
请注意,在使用术语“子树”时,应明确注明您正在讨论的内容。在git的上下文中,实际上有两个不同但相关的主题:git-subtreegit subtree merge strategy。这两个与子树相关的概念都有效地允许您在一个仓库中管理多个仓库。与git-submodule相比,后者只在根仓库中以.gitmodules的形式存储元数据,并且必须单独管理外部仓库。
更多细节方面,git subtree merge strategy基本上是更手动的方法,需要使用您提到的命令。

git-subtree 是一个包装的 shell 脚本,用于简化语法。实际上,它仍然是 contrib 的一部分,并未完全集成到 git 中,也没有通常的 man 页面。文档 存储在与脚本相同的位置。

以下是使用信息:

NAME
----
git-subtree - Merge subtrees together and split repository into subtrees


SYNOPSIS
--------
[verse]
'git subtree' add   -P <prefix> <commit>
'git subtree' add   -P <prefix> <repository> <ref>
'git subtree' pull  -P <prefix> <repository> <ref>
'git subtree' push  -P <prefix> <repository> <ref>
'git subtree' merge -P <prefix> <commit>
'git subtree' split -P <prefix> [OPTIONS] [<commit>]

我曾经查阅了许多关于子树的资源,因为我计划写一篇博客文章。如果我写了,我会更新这篇文章,但现在这里有一些与问题相关的信息:

你所寻找的很多内容可以在Atlassian博客上找到,由Nicola Paolucci撰写,下面是相关部分:

为什么使用子树而不是子模块?

以下是你可能会发现subtree更好用的几个原因:

  • 管理简单工作流程容易。
  • 支持旧版本的git(甚至在v1.5.2之前)。
  • 在超级项目完成clone后,子项目的代码就可以使用了。
  • subtree不需要你的存储库的用户学习任何新知识,他们可以忽略你正在使用subtree来管理依赖项的事实。
  • subtree不像submodules那样添加新的元数据文件(即.gitmodule)。
  • 模块的内容可以被修改而无需在其他地方拥有一个单独的存储库副本。

我认为这些缺点是可以接受的:

  • 你必须学习一种新的合并策略(即subtree)。
  • 将代码贡献回子项目的上游稍微有些复杂。
  • 确保提交中没有混合超级和子项目代码的责任在于你自己。

我也大部分同意这个观点。建议阅读这篇文章,因为它涵盖了一些常见的用法。

你可能已经注意到他还写了一个这里的跟进文章,在那里他提到了这种方法遗漏的重要细节...

git-subtree目前未能包含远程仓库!

这种短视可能是因为人们在处理子树时经常手动添加远程仓库,但这也没有存储在git中。作者详细介绍了他编写的补丁,将此元数据添加到git-subtree已经生成的提交中。在此被合并到官方git主线之前,您可以通过修改提交消息或将其存储在另一个提交中来执行类似操作。

I also find this blog post very informative as well. The author adds a third subtree method he calls git-streeto the mix. The article is worth a read as he does a pretty good job of comparing the three approaches. He gives his personal opinion of what he does and doesn't like and explains why he created the third approach.

附加内容

结束语

这个主题展示了git的强大和当一个功能不能完全满足需求时可能会出现的细分。

我个人对git-submodule并不感冒,因为我认为它更加令贡献者困惑。我也更喜欢在我的项目中管理所有的依赖项,以便于创建一个易于重现的环境,而不需要管理多个存储库。然而,git-submodule目前更为广泛地使用,所以了解它显然是有好处的,具体是否使用取决于你的受众群体。

2
git-stree 的作者在同一篇博客中表示,他自 2016 年以来就支持 git-subrepo。 - legends2k

17

首先:我认为您的问题很可能会得到强烈的主观答案,并且在这里可能被认为是不相关的。但是我不喜欢SO政策,想要将其边界向外推一点,所以我愿意回答,并希望其他人也这样做。

在您指出的GitHub教程中,有一个链接如何使用子树合并策略,其中提供了优缺点的观点:

比较子树合并和子模块

使用子树合并的好处是,它需要用户的管理负担较少。它可以与旧版本(Git v1.5.2之前)客户端一起使用,并且在克隆后就可以获得代码。

然而,如果您使用子模块,则可以选择不传输子模块对象。这可能是子树合并的问题。

此外,如果您对其他项目进行更改,如果只使用子模块,则更容易提交更改。

以下是我的观点:
我经常与一些不常用 git 的人(即 committers)一起工作,其中一些人仍然(并将永远)在版本控制方面挣扎。教他们如何使用子模块合并策略基本上是不可能的。它涉及到额外的远程概念、合并、分支,然后将所有这些东西混合成一个工作流。从上游拉取和推送上游是一个两个阶段的过程。由于分支对他们来说很难理解,所以这一切都是没有希望的。
对于子模块来说,对他们来说还是太复杂了(叹气),但它更容易理解:它只是一个嵌套在一个 repo 中的 repo(他们熟悉层次结构),你可以像往常一样进行推拉操作。
为子模块工作流提供简单的包装脚本在我看来更容易。
对于具有许多子 repo 的大型超级 repo,选择不克隆某些子 repo 的数据点是子模块的一个重要优点。我们可以根据工作要求和磁盘空间使用量来限制这一点。
访问控制可能会有所不同。我还没有遇到过这个问题,但如果不同的 repo 需要不同的访问控制,有效地禁止一些用户访问某些子 repo,我想这是否使用子模块方法更容易实现。
就我个人而言,我还没有决定自己要使用什么。所以我分享了你的困惑:o]

4
尽管存在矛盾,但这个回答是我见过最强烈的主观意见之一,因为它是唯一的答案,也具有自我实现预言的特点。这个回答中的叹气和对别人学习能力的末日预言态度非常傲慢。您对政策的看法可能更适合 Meta,那里可能会更有帮助。除了自我吹嘘的内容外,这个答案本身还是相当不错的。 - vgoff
3
您的批评是正确的。对于似乎傲慢,我感到抱歉-这只是15年以上的工作经验,许多受过不同人在不同版本控制系统中培训的人仍然将文本文件复制到许多.backup.<timestamp>。我认为我在开始时已经表明了这是主观看法。希望其他人能够提供更具事实性的见解,我很惊讶还没有人这样做。 - cfi
我还是不明白。你是说 submodule 是过时的旧方法,用于合并已使用的库,而 subtree 是新的闪亮方式吗? - Lernkurve
不,至少文档中没有提到这两种方法中的任何一种已被弃用。对我来说,文档是最终的依据(除了Bug)。这只是两种不同的工作流程,用于实现类似的功能。两者都有优缺点。对我来说,没有一个Git大师回答,这表明对专家来说这些差异微不足道。很可能大多数人使用子树合并策略,因为它是早期实施的策略,并且人们熟悉read-tree(以及分支/合并/远程等操作)。submodules是后来添加的。 - cfi

9
基本上,Git-subtree是Git-submodule方法的替代品: 使用git-submodules时存在许多缺点,或者我应该说,在使用git-submodules时需要非常小心。例如,当您在“one”中添加名为“two”的另一个存储库时,需要注意以下事项:
- 当您更改“two”中的内容时,您需要在“two”内提交和推送,如果您在顶级目录(即在“one”中),则不会突出显示您的更改。 - 当未知用户尝试克隆您的“one”存储库时,在克隆“one”之后,该用户需要更新子模块以获取“two”存储库。
这些是其中一些要点,为了更好地理解,我建议您观看此视频:https://www.youtube.com/watch?v=UQvXst5I41I 为了克服这些问题,发明了子树方法。要了解有关git-subtree的基础知识,请查看以下内容:https://www.youtube.com/watch?v=t3Qhon7burE
我发现与子模块相比,子树方法更可靠和实用 :)(我很菜,不能做出这样的评价)
干杯!

8

我们公司有一个真实的使用案例,其中git subtree是救星:

我们公司的主要产品是高度模块化的,并在单独的存储库中开发了几个项目。所有模块都有其单独的路线图。整个产品由具体版本的所有模块组成。

与此同时,每个客户的整个产品的具体版本都进行了定制-每个模块都有单独的分支。有时需要在多个项目中同时进行自定义(跨模块自定义)。

为了为定制产品提供单独的生命周期(维护,功能分支),我们引入了git subtree。我们有一个git-subtree存储库用于所有定制模块。我们每天将自定义内容“git subtree push”回所有原始存储库以进行自定义分支。

这样我们就避免了管理许多存储库和许多分支。 git-subtree将我们的生产力提高了数倍!

更新

有关已发布评论的解决方案的更多详细信息:

我们创建了一个全新的存储库。然后,我们将具有客户分支的每个项目作为子树添加到该新存储库中。我们有一个Jenkins工作,定期将主分支更改推回原始存储库以进行客户分支。我们只使用“客户端”存储库,使用典型的git流程进行功能和维护分支。

我们的“客户端”存储库还具有构建脚本,我们还为该特定客户适应了这些脚本。

但是,这种解决方案存在一个陷阱。

随着我们越来越远离产品主要核心开发,对于该特定客户的可能升级变得越来越困难。在我们的情况下,这是可以接受的,因为在子树之前项目的状态已经远离了主路径,因此子树引入了至少一定的顺序和引入默认git流的可能性。


Marek,我面临着听起来与你相似的情况,而我对Git还比较新,对可能性感到困惑。我想了解更多关于你的设置。 - goug
我创建了一个全新的代码库。然后,我将每个具有客户分支的项目作为子树添加到该代码库中。我们有一个Jenkins作业,将更改推送回原始代码库的客户分支。 在我们的客户端代码库上,我们通常使用主要功能和维护分支进行工作。 - Marek Jagielski
陷阱在于我们越来越远离产品的主要核心开发。因此,针对该特定客户的可能升级变得越来越困难。在我们的情况下,由于子树之前的项目状态已经远离了主路径,因此引入子树至少可以带来秩序和引入默认的git流程。 - Marek Jagielski
我们的“客户端”代码库还有构建脚本,我们也在为这个特定的客户进行适应。 - Marek Jagielski
太棒了!我希望我能给你更多的赞。 - Jan Warchoł
2
我想建议您将评论中的附加信息纳入您的答案中;它们肯定会使这个答案更好。 - James Skemp

4
除了上述答案中提到的缺点,使用子树的另一个缺点是与子模块相比增加了仓库的大小。
我没有任何真实的度量数据,但考虑到每次在模块上进行推送时,使用该模块的所有位置都会得到父模块相同更改的副本(然后在这些仓库上更新)。
因此,如果代码库被大量模块化,这将很快累积起来。
然而,考虑到存储价格一直在降低,这可能不是一个重要因素。

2
存储不是问题。熵才是问题!例如,您有1000个工具,每个工具的大小为10KB到100KB,共享一个包含来自不同来源的大量模块的代码库,大小为35GB。使用子模块,您可以为所有工具传输约36GB,但使用git子树可能超过1TB!此外,请注意,如果涉及到git gc和ZFS dedup(对象包),子模块具有明显的不公平优势。因此,就存储库大小而言,较小的代码库(而不是存储库计数)应该使用子模块,而较大的代码库应该使用单一存储库。我还没有发现子树的任何用途。 - Tino
@tino Git可以很好地去重具有相同代码的子树。我刚刚进行了一些实验来确认。对于检出的代码,您需要运行类似ZFS的东西。但是子模块并没有什么不同。 - Matthias

1
我不得不停止使用子模块,因为一旦出错,你就会把你自己和其他人的代码库搞砸,而且很难修复(“天哪,哪个子模块提交与主代码匹配,我在哪里忘记检查它?”)。我得出的结论是,如果git默认始终检出整个代码库,包括任何子模块,并且提交和推送整个代码库,包括子模块,除非明确告知不这样做,那么使用子模块将会非常容易。默认将整个嵌套仓库集合视为一个大的虚拟仓库(这应该是默认的,如果子模块对项目不是必需的,为什么要包含它呢),除非你明确需要做一些不同的事情,这通常是个例外。在这个世界上,一个TB的硬盘空间只需10美元,很多人都有千兆的互联网,所以优化大小,这是git目前所做的,有点过时了。

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