现代软件项目大多依赖于其他项目的工作成果。如果别人已经编写了优秀的解决方案,而你却在代码中重新发明轮子,那将是极大的时间浪费。这就是为什么许多项目使用第三方代码,例如库或模块。
Git,全球最流行的版本控制系统,提供了一种优雅而强大的方法来管理这些依赖项。其“子模块”概念允许我们包含和管理第三方库,同时保持它们与我们自己的代码清晰分离。
本文将阐述Git子模块为何如此有用,它们究竟是什么以及它们的工作原理。
关键要点
保持代码分离
为了清楚地说明Git子模块为何是一种宝贵的结构,让我们来看一个没有子模块的案例。当您需要包含第三方代码(例如开源库)时,您可以选择简单的方法:只需从GitHub下载代码并将其放入项目的某个位置。虽然这种方法很快,但由于以下几个原因,它绝对是不干净的:
软件开发中“将不同的事物分开”的普遍规则并非没有道理。对于在您自己的项目中管理第三方代码,这一点尤其正确。幸运的是,Git的子模块概念正是为这些情况而设计的。
当然,子模块并不是解决此类问题的唯一解决方案。您还可以使用许多现代语言和框架提供的各种“包管理器”系统。这样做并没有错!
但是,您可以认为Git的子模块架构具有一些优势:
Git子模块的本质
Git中的子模块实际上只是标准的Git存储库。没有花哨的创新,只是我们现在都非常熟悉的相同的Git存储库。这也是子模块强大功能的一部分:它们之所以如此强大而直接,是因为它们从技术的角度来看是如此“枯燥”并且经过了充分的测试。
使Git存储库成为子模块的唯一一点是它位于另一个父Git存储库内部。
除此之外,Git子模块仍然是一个功能齐全的存储库:您可以执行您已经从“普通”Git工作中了解到的所有操作——从修改文件到提交、拉取和推送。子模块中的一切都是可能的。
添加子模块
让我们以一个经典的例子为例,假设我们要向项目添加一个第三方库。在我们获取任何代码之前,创建一个单独的文件夹来存放此类内容是有意义的:
<code class="language-bash">$ mkdir lib $ cd lib</code>
现在我们准备以有序的方式使用子模块将一些第三方代码导入我们的项目。假设我们需要一个小的“时区转换器”JavaScript库:
<code class="language-bash">$ git submodule add https://github.com/spencermountain/spacetime.git</code>
当我们运行此命令时,Git会将存储库克隆到我们的项目中,作为一个子模块:
<code>Cloning into 'carparts-website/lib/spacetime'... remote: Enumerating objects: 7768, done. remote: Counting objects: 100% (1066/1066), done. remote: Compressing objects: 100% (445/445), done. remote: Total 7768 (delta 615), reused 975 (delta 588), pack-reused 6702 Receiving objects: 100% (7768/7768), 4.02 MiB | 7.78 MiB/s, done. Resolving deltas: 100% (5159/5159), done.</code>
如果我们查看我们的工作副本文件夹,我们可以看到库文件实际上已经到达了我们的项目中。
您可能会问:“有什么区别呢?”毕竟,第三方库的文件就在这里,就像我们复制粘贴它们一样。关键的区别在于它们包含在它们自己的Git存储库中!如果我们只是下载了一些文件,将它们扔到我们的项目中,然后提交它们——就像我们项目中的其他文件一样——它们将成为同一个Git存储库的一部分。但是,子模块确保库文件不会“泄漏”到我们主项目的存储库中。
让我们看看还发生了什么:在主项目根文件夹中创建了一个新的.gitmodules文件。以下是其内容:
<code class="language-bash">$ mkdir lib $ cd lib</code>
这个.gitmodules文件是Git跟踪项目中子模块的多个位置之一。另一个是.git/config,现在结尾如下:
<code class="language-bash">$ git submodule add https://github.com/spencermountain/spacetime.git</code>
最后,Git还在内部.git/modules文件夹中保留每个子模块的.git存储库的副本。
所有这些都是您不必记住的技术细节。但是,了解Git子模块的内部维护相当复杂可能会有所帮助。这就是为什么记住一件事很重要:不要手动修改Git子模块配置!如果您想移动、删除或以其他方式操作子模块,请帮自己一个忙,不要手动尝试这样做。可以使用适当的Git命令或像“Tower”这样的Git桌面GUI,它会为您处理这些细节。
让我们看看我们添加子模块后主项目的状态:
<code>Cloning into 'carparts-website/lib/spacetime'... remote: Enumerating objects: 7768, done. remote: Counting objects: 100% (1066/1066), done. remote: Compressing objects: 100% (445/445), done. remote: Total 7768 (delta 615), reused 975 (delta 588), pack-reused 6702 Receiving objects: 100% (7768/7768), 4.02 MiB | 7.78 MiB/s, done. Resolving deltas: 100% (5159/5159), done.</code>
如您所见,Git将添加子模块视为与其他更改一样的更改。因此,我们必须像其他任何更改一样提交此更改:
<code>[submodule "lib/spacetime"] path = lib/spacetime url = https://github.com/spencermountain/spacetime.git</code>
克隆包含Git子模块的项目
在我们上面的例子中,我们向现有的Git存储库添加了一个新的子模块。但是,“反过来”呢,当您克隆已经包含子模块的存储库时会发生什么?
如果我们在命令行上执行了普通的git clone <远程URL>,我们将下载主项目——但是我们会发现任何子模块文件夹都是空的!这再次生动地证明了子模块文件是独立的,并且不包含在其父存储库中。
在这种情况下,要在克隆其父存储库后填充子模块,您可以简单地执行git submodule update --init --recursive。更好的方法是在第一次调用git clone时直接添加--recurse-submodules选项。
检出版本
在“普通”Git存储库中,我们通常检出分支。通过使用git checkout <分支名>或更新的git switch <分支名>,我们告诉Git我们当前活动的分支应该是什么。当在这个分支上进行新的提交时,HEAD指针会自动移动到最新的提交。理解这一点很重要——因为Git子模块的工作方式不同!
在子模块中,我们始终检出一个特定的版本——而不是分支!即使您在子模块中执行类似于git checkout main的命令,在后台,也会记录该分支上当前最新的提交——而不是分支本身。
当然,这种行为并非错误。考虑一下:当您包含第三方库时,您希望完全控制在主项目中使用哪些确切的代码。当库的维护者发布新版本时,这很好……但是您不一定希望自动在您的项目中使用这个新版本。因为您不知道这些新更改是否会破坏您的项目!
如果您想找出您的子模块正在使用哪个版本,您可以在主项目中请求此信息:
<code class="language-bash">$ mkdir lib $ cd lib</code>
这将返回我们lib/spacetime子模块当前检出的版本。它还让我们知道这个版本是一个名为“6.16.3”的标签。在使用Git子模块时,大量使用标签是很常见的。
假设您希望您的子模块使用较旧的版本,该版本标记为“6.14.0”。首先,我们必须更改目录,以便我们的Git命令将在子模块的上下文中执行,而不是我们的主项目。然后,我们可以简单地使用标签名运行git checkout:
<code class="language-bash">$ git submodule add https://github.com/spencermountain/spacetime.git</code>
如果我们现在回到我们的主项目并再次执行git submodule status,我们将看到我们的检出结果:
<code>Cloning into 'carparts-website/lib/spacetime'... remote: Enumerating objects: 7768, done. remote: Counting objects: 100% (1066/1066), done. remote: Compressing objects: 100% (445/445), done. remote: Total 7768 (delta 615), reused 975 (delta 588), pack-reused 6702 Receiving objects: 100% (7768/7768), 4.02 MiB | 7.78 MiB/s, done. Resolving deltas: 100% (5159/5159), done.</code>
仔细查看输出:该SHA-1哈希前面的 符号告诉我们子模块的版本与当前存储在父存储库中的版本不同。由于我们刚刚更改了检出的版本,这看起来是正确的。
现在,在我们的主项目中调用git status也会告知我们这一事实:
<code>[submodule "lib/spacetime"] path = lib/spacetime url = https://github.com/spencermountain/spacetime.git</code>
您可以看到Git将移动子模块的指针视为与其他更改一样的更改:如果我们想存储它,我们必须将其提交到存储库:
<code>[submodule "lib/spacetime"] url = https://github.com/spencermountain/spacetime.git active = true</code>
更新Git子模块
在上述步骤中,是我们自己移动了子模块指针:我们是那些选择检出不同版本、提交它并将其推送到我们团队的远程存储库的人。但是,如果我们的同事更改了子模块版本会怎样——也许是因为发布了子模块的有趣的新版本,并且我们的同事决定在我们的项目中使用它(当然,在彻底测试之后……)。
让我们在主项目中执行一个简单的git pull——因为我们可能经常这样做——以从共享的远程存储库获取新的更改:
<code class="language-bash">$ git status On branch master Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: .gitmodules new file: lib/spacetime</file></code>
倒数第二行表示子模块中的某些内容已更改。但是让我们仔细看看:
<code class="language-bash">$ git commit -m "Add timezone converter library as a submodule"</code>
我相信您还记得那个小的 号:这意味着子模块指针已移动!要将我们本地检出的版本更新到我们的队友选择的“官方”版本,我们可以运行update命令:
<code class="language-bash">$ git submodule status ea703a7d557efd90ccae894db96368d750be93b6 lib/spacetime (6.16.3)</code>
好了!我们的子模块现在已检出到记录在我们主项目存储库中的版本!
使用Git子模块
我们已经介绍了使用Git子模块的基本构建块。其他工作流程非常标准!
例如,检查子模块中的新更改就像在任何其他Git存储库中一样:您在子模块存储库中运行git fetch命令,如果确实要使用更新,之后可能会运行类似于git pull origin main的命令。
更改子模块也可能适合您,特别是如果您自己管理库代码(因为它是内部库,而不是来自第三方)。您可以像使用任何其他Git存储库一样使用子模块:您可以进行更改、提交它们、推送它们等等。
充分利用Git的强大功能
Git在幕后拥有强大的功能。但是,许多高级工具(如Git子模块)并不为人所知。许多开发人员错过了很多强大的功能,这实在令人遗憾!
如果您想深入了解一些其他高级Git技术,我强烈推荐“高级Git工具包”:这是一个(免费的!)短视频合集,它将向您介绍Reflog、交互式变基、Cherry-Picking甚至分支策略等主题。
祝您成为更好的开发者!
关于Git子模块的常见问题
什么是Git子模块? Git子模块是一种将另一个Git存储库作为子目录包含到您自己的Git存储库中的方法。它允许您将单独的存储库作为子项目维护在主项目中。
为什么要使用Git子模块? Git子模块对于将外部存储库合并到您的项目中非常有用,尤其是在您希望将它们的开发历史与主项目分开时。这对于管理依赖项或包含外部库非常有益。
主项目中关于子模块存储了哪些信息? 主项目将子模块的URL和提交哈希存储在父存储库中的特殊条目中。这允许任何克隆主项目的人也克隆引用的子模块。
如何克隆包含子模块的Git存储库? 克隆包含子模块的存储库时,您可以使用git clone命令的--recursive标志自动初始化和克隆子模块。或者,您可以在克隆后使用git submodule update --init。
我可以嵌套子模块吗? 是的,Git支持嵌套子模块,这意味着子模块可以包含它自己的子模块。但是,管理嵌套子模块可能会变得复杂,并且必须确保每个子模块都已正确初始化和更新。
以上是理解和与git中的子模型合作的详细内容。更多信息请关注PHP中文网其他相关文章!