Git子模块和获取

6

我很难理解子模块

它们似乎非常复杂。通常情况下,我会完全避免使用它们,但是现在的项目却让我不得不使用。

所以...

我在我们开发服务器上有一个带有子模块的git仓库。

/myproject
          /.git
          /files ...
          /other
               /submodule
                         /.git

现在,因为我们运行一个开发/生产环境,我们在所能做的事情方面受到了很大限制。
我该怎么做呢?
1.将repo克隆到生产环境,以便使生产服务器完全填充父git和所有检出的子模块?
2.然后如何……
- 更新其他/子模块中的文件 - 提交 - 获取到克隆 repo - 然后在克隆的 repo 中合并
我们传统上使用获取然后合并的策略,而不是单个拉取。由于团队非常小,因此我们也不使用裸库。
我尝试多种不同的方法来实现以上内容,但似乎都不对。这其中涉及到非常多的步骤,所以肯定出了一些问题。
另外,我不想在prod服务器上从子模块远程repo进行获取操作。
让您知道,我正在处理一个Drupal 8项目,直接在生产环境上进行开发是完全不适当的,我们甚至不会安装composer或drush。

对于那些遇到相同问题的人来说,问题似乎出在我使用了过时的git版本进行测试。如果你的版本低于1.8,请更新它,这可以节省你数小时的头疼。 - DeveloperChris
对于那些跟进的人,提供更多信息。在使用 git submodule add url path 时,请始终确保子模块已经存在于 git 超级项目中,且 url 和 path 都是相同的,例如 git submodule add ./path/to/submodule ./path/to/submodule,这样 git 就会理解子模块已经存在,否则它将尝试将其克隆到自己。 - DeveloperChris
1个回答

31
【子模块】看起来过于复杂了......

可能是真的。然而,子模块也是必要的复杂性。 :-) 我还要指出,在Git 2.x中,子模块支持明显比Git 1.5或1.6等早期版本要好得多,这也是我学习为什么人们称它们为"哭泣"模块的时候。其中一些历史可能就是这里的某些复杂性存在的原因。
在我深入回答之前,这里有一个简短的入门方式:使用git clone --recurse-submodules,或者在克隆后运行git submodule update --init --recursive。(如果子模块有自己的子模块,则需要第二个--recursive选项。)将--recurse-submodules选项添加到git clone只是告诉git clone在其正常操作序列之后执行git submodule update --init --recursive。请注意,这不会帮助您处理子模块内部的工作过程。

我该如何......

Git是一种工具,而不是解决方案(似乎在建筑业中很常见,但通常适用于大多数技术)。与大多数工具一样,有多种使用方法。
关于子模块需要知道的是,每个子模块只是另一个Git仓库。使Git仓库成为“子模块”的唯一条件是有某个“外部”仓库以某种方式控制内部仓库。在内部仓库中,我们将外部仓库称为“超级项目”。
在任何你需要进行工作的Git仓库中,都有一个“工作树”。工作树保存了文件的普通日常形式,在其中你(和计算机上的其他程序)可以使用它们。每个Git仓库还有一个“索引”,这是你构建下一个提交的地方。索引也称为“暂存区”或“缓存区”,反映了它极其重要的角色,或者原始名称“索引”选择不佳(或两者兼而有之)。当然,每个Git仓库都有一组提交,具有各种“分支名称”和/或“标签名称”,用一些人类可读的名称标识特定的提交哈希值。
如果那个Git仓库是独立的,那些名称——分支和标签名称——对我们人类来说是有用的,因为我们在该仓库中工作。但我们刚刚宣布,这个仓库是一个子模块,它存在(或消失)取决于某个其他仓库——超级项目的命令。我们自己的分支和标签名称几乎没有用。只有当我们将这个仓库视为常规仓库而不是某个超级项目的附属物时,它们才变得有用。当我们将这个仓库视为受控实体时,我们希望这个仓库有一个“分离的HEAD”来代替。超级项目,而不是其中的子模块,指定要检出的提交哈希值,而不是通过某种人类可读的名称,而是通过原始哈希ID。
这反映到所有“如何做”的答案中。超级项目在其索引中记录了应该在子模块中检出的特定提交的原始哈希ID。
克隆
像任何克隆一样,可以通过git clone url [dir]进行克隆,这实际上包括大约六个步骤:
  1. 创建一个新的空目录dir并切换到该目录(cd),或者如果有指定现有的空目录,则使用该目录:([ -d dir ] || mkdir dir) && cd dir。(如果失败,请停止执行,不要继续执行后面的步骤。如果后续步骤失败,请删除新目录(如果我们创建了它)和所有文件,不留下任何部分克隆的痕迹)。如果我们没有给出git clone的目录名称,则从url参数计算一个。
  2. 创建一个新的空仓库:git init。这将创建.git目录和一个初始配置。
  3. 根据git clone之后给出的-c选项进行任何必需的附加配置。
  4. 添加一个给定urlremotegit remote add remote url。通常远程的名称为origin,但您可以使用-o选项来控制此名称。
  5. 从远程获取提交记录:git fetch remote
  6. 检出某个分支或标签名称:git checkout name。如果这是一个分支名称,该分支尚不存在,因此这将创建分支,就像git checkout一样。如果这是一个标签名称,则将提交作为独立的HEAD检出。这里的name是您使用-b选项给出的名称。如果您没有给出名称,则通过询问git fetch操作另一端的Git来获取名称推荐的分支,这通常是main。如果那也失败了-如果另一个Git没有名称推荐-则使用的名称是main
最后一步,第6步,检出特定的提交,通常通过获取“在”分支(如main)来创建该分支名称,这是在第5步中获得的名称(git fetch使其成为origin/main)。检出此特定提交的行为填充了存储库的索引和工作树,因此现在您的工作树中有所有所需的文件。
子模块和gitlinks 如果您刚刚检出的提交具有子模块,则它具有名为.gitmodules的文件,并且在您刚刚检出的提交中,每个都称为gitlink的一个或多个特殊条目。 gitlink条目看起来很像文件(blob)条目或tree条目,但类型代码为160000而不是100644(常规文件)或100755(可执行文件)或004000(树)。 这些gitlink条目进入您的索引,并且您的Git在由gitlink给出的路径处创建一个空目录,就像您的Git会为tree创建子目录或为blob创建文件一样。这些gitlink条目关联的哈希ID-每个索引条目都有一个哈希ID-是子模块中一个特定提交的哈希ID,Git可以但现在不会将其作为分离的HEAD检出。
请注意我这里所说的如果您刚才检出的提交包含子模块。这是另一个关键认识:子模块的“子模块性”由超级项目中的特定提交控制。该提交需要有一个gitlink条目,以在子模块中提供要检出的哈希ID和一个.gitmodules文件。但是这个.gitmodules文件是用来做什么的?

1还有一种索引类型码,120000,用于符号链接。这些处理方式与blob对象几乎完全相同,只是只要启用符号链接,Git就会将内容写成符号链接而不是文件。如果禁用符号链接,Git会将内容写成常规文件,以便您可以使用git update-index将其编辑并重新添加为符号链接,如果您知道处理索引条目的所有技巧。

2Git将创建一个空目录作为tree对象的事实,导致人们尝试使用Git的semi-secret empty tree存储空目录。不幸的是,索引本身在这里有奇怪的角落案例,Git在各种条件下将空树转换为gitlink条目。然后它充当了一个损坏的子模块——没有.gitmodules条目的gitlink,这使得Git的行为稍微有些糟糕。


.gitmodules文件

如上所述,git clone至少需要一个参数:要克隆的存储库的url。超级项目将所需的提交哈希ID存储在gitlink中,但它如何知道要使用什么url?答案是查看.gitmodules文件。

.gitmodules的内容格式与.git/config$HOME/.gitconfig或任何其他Git配置文件相同,实际上,Git使用git config来读取它们:

git config -f .gitmodules --get submodule.path/to/x.url

这是在寻找{{某个东西}}。

[submodule "path/to/x"]
    url = <whatever you put here>

.gitmodules文件中,当我们找到它时,{{它}}提供URL。
事实上,内容将会是:
[submodule "path/to/x"]
    path = path/to/x
    url = <whatever you put here>

也许还包括以下一个或两个方面:

    branch = <name>
    update = <control>

{{path}}必须对应于超级项目中子模块的相对路径,而子模块的名称必须是超级项目中子模块的相对路径。(如果其中一个或两者不匹配会发生什么,我不太确定。Git的子模块命令通常会确保它们匹配,因此问题从未出现。)
这使得git submodule可以找到URL以进行克隆。 这个过程很复杂。当你运行git submodule initgit submodule update --init时,Git将会把.gitmodules中的url设置复制到.git/config中。 如果有一个update = control设置,则也会复制该设置,除非在.git/config中已经有一个设置。(尽管我认为这是那些“不必要的复杂性”之一,但我认为这是为了纠正历史错误。)
没有使用--init选项,git submodule update命令只会查看.git/config中的条目,而不是.gitmodules中的条目。这意味着您可以使用两步序列git submodule init && git submodule update来完成相同的操作,但是git submodule update --init更容易输入。更重要的是,git submodule init 没有--recursive选项,而git submodule update有。这实际上是明智的,因为git submodule init .gitmodules复制到.git/config(有关此内容的更多信息,请参见下文)。git submodule update操作实际上创建了克隆,使用了上面概述的六个步骤。

将HEAD分离到子模块中的正确提交

我们看到超级项目列出了子模块的正确哈希ID,作为一个gitlink条目。这意味着Git需要在超级项目中开始,从索引中读取gitlink条目,然后切换到子模块(cd path)并通过其哈希ID使用git checkout检出正确的提交。这将导致一个分离的HEAD,其中检出了正确的提交。
执行此操作的命令是git submodule update。通常我们想要的是:通过其哈希ID检出特定提交,作为分离的HEAD。现在我们已经获得了子模块中想要的内容,我们完成了...或者说我们完成了吗?如果这个Git存储库 - 记住,每个子模块都是一个独立的Git存储库 - 如果这个Git存储库有自己的子模块呢?
子模块可以有子模块
如果这个子模块有它自己的子模块,那么我们现在希望这个子Git检出正确的提交,运行git submodule init来初始化其子模块的.git/config,并运行git submodule update使其自己的子模块被检出到正确的提交。这正是git submodule update已经代表我们的超级项目在做的事情,所以我们只需要让这个git submodule update递归地操作子模块的子模块。这意味着git submodule update需要能够递归进入子模块并对其进行--init
因此,git submodule update --init --recursive之所以存在,就是它是工作马,从超级项目进入每个子模块,如果需要设置其.git/config,则检出正确的分离式HEAD哈希,然后在子模块的子模块上进行递归。 git clone可以调用git submodule update
如果我们现在回到git clone,我们可以看到在第6步之后我们需要第7步:git submodule update --init --recursive,进入超级项目中列出的每个子模块并初始化它,并检出正确的分离HEAD,如果该子模块是其他子模块的超级项目,则递归处理它们。最终,我们将拥有超级项目及其特定提交,控制其所有子模块,这些子模块作为分离的HEAD处于正确的提交状态,对于其中任何一个作为子模块的超级项目具有子模块的情况,子模块作为超级项目的提交将递归地控制子模块作为超级项目的子模块。
如果您没有递归子模块,所有递归都不会产生任何效果:这需要一点额外的工作,但是是无害的。因此,通常采用这种方式:只需运行git clone --recurse-submodules,就可以创建克隆,并将其子模块作为分离的HEAD存储库检出,然后您就完成了。
在子模块内部工作
您几乎提出了另一个问题:
“那么我如何更新其他/子模块中的文件?”
我们上面看到,超级项目控制/使用子模块的方式是通过绝对哈希ID指定子模块被锁定到的提交,作为分离HEAD。这非常适用于控制和使用子模块,但当我们想要将子模块更新到某个更新的提交时,就会出现问题。
传统的答案可以追溯到Git 1.5时代,因为子模块本身就是一个Git存储库,所以只需进入子模块并使用git checkout <branchname>开始工作。这仍然有效!但是它有一个明显的缺点:你如何知道要使用哪个分支名?
在某些情况下,你只是知道。那很好,继续使用它们。但是,如果你希望超级项目知道,这就是超级项目的branch =设置的作用,也是git submodule update和/或超级项目中的submodule.name.update设置的作用。请记住,这些设置来自超级项目中的.git/config文件,而不是子模块本身,通常也不是.gitmodules文件,但是.gitmodules文件内容设置了默认的.git/config设置。因此,有很多方法可以控制此配置。
接下来,问题是每个配置都做了什么,以及如何根据自己的目的进行设置。这些在git submodule文档中列举得不够好。这里是我自己对其摘要的总结,并附加评论。
  • checkout:在分离的 HEAD 上检出超级项目中记录的提交,该提交将在子模块中检出。

    这是默认设置,就像我们上面看到的那样。

  • rebase:将子模块的当前分支变基到超级项目中记录的提交上。

    除非您已经进入子模块并在其中执行了某些操作,否则此选项无用。然而,在文档的后面还有一个描述为 --remote 的选项,它使其更加有用。

  • merge:将超级项目中记录的提交合并到子模块中的当前分支中。

    rebase 一样,这本身并不有用:您需要使用 --remote 或在子模块中自行完成工作才能执行此操作。

  • 自定义命令:执行一个接受单个参数(超级项目中记录的提交的 SHA1)的任意 shell 命令。

    这个选项本身是有用的,但需要您在超级项目中进行一些前期工作,以设置配置并定义命令。

  • none:不更新子模块。

    这主要用于标记一个不会在特定超级项目的所有其他子模块更新时得到更新的子模块。如果您只有一个子模块,则此设置根本没有作用。

到目前为止,我们还没有看到从.gitmodules复制到.git/configbranch设置有任何用处。在同一文档下面进一步描述了这个设置是如何使用的:

...不使用超级项目记录的SHA-1来更新子模块,而是使用子模块远程跟踪分支的状态。

也就是说,超级项目有一个gitlink条目,其中包含“使用哈希a1b2c3d…”或其他内容,但是当超级项目git submodule update命令在包含子模块的Git存储库中探索时,超级项目命令将在子模块中查找,例如origin/main。 这里的main名称来自该分支设置,因此将submodule.name.branch设置为develop,超级项目将使用origin/develop而不是origin/main4 为了让这个过程有用,超级项目Git在开始任何操作之前会在子模块中运行git fetch。这将导致子模块从其origin Git获取任何新的提交,更新其origin/mainorigin/develop等内容。这里的假设是你自己没有在子模块中进行任何工作!你只是在获取其他人在子模块仓库的origin代码库中所做的工作(哇)。
如果在.git/config中没有设置且命令行上没有覆盖设置,则会使用.gitmodules中的设置。我认为这是另一个向后兼容的项目。这假定origin/develop是与子模块存储库中的develop分支相关联的远程跟踪名称,即一切都是按照正常方式设置的。

准备更新子模块

如果你要在自己的子模块中进行自己的工作,那么这些内容对你毫无帮助。相反,你应该cd进入子模块仓库并运行git checkout branchname。这将使你从分离的HEAD状态下退出,并将你放在给定的分支上,现在你可以正常地工作了。像平常一样编写代码、git addgit commit。当子模块准备好后,返回到超级项目的目录。你会发现你的子模块是位于一个分支上(而不是处于分离的HEAD模式),处于某个特定的提交状态。

如果你只是在接手别人的工作,那么这个git submodule update --remote --checkout或其他操作将执行git fetch,然后在子模块中执行适当的git checkout origin/main或其他操作。这将使你的子模块处于没有分支的detached HEAD模式,处于某个特定的提交状态。这可能是你想要的。

在超级项目中使用已更新的子模块

总之,从超级项目的角度来看,发生的事情是子模块现在位于不同的提交。超级项目并不关心子模块的HEAD状态是否已经连接,重要的是子模块的当前提交

现在子模块已经处于所需的提交状态,您可以在超级项目中进行其他更改——例如,可能有一些文件应该使用子模块的新功能。完成所需更改后,git add任何更新的文件,并且还要在子模块路径上运行git add(不带斜杠)
git add features.ext   # updated to use feature F of submodule sub/S
git add sub/S          # record the new gitlink for sub/S!

这将更新超级项目的索引,因此我们不仅拥有更新的文件(features.ext),而且还有子模块的新正确哈希ID——更新的gitlink。现在我们可以像往常一样在超级项目中运行git commit

git commit

这使得我们的新提交包含一个gitlink,记录了子模块sub/S应该使用分离的HEAD在提交f37c219...或者实际上是sub/S当前提交。这个新提交将被放置在超级项目的任何一个已检出的分支上,无论是main、develop还是其他分支。
推送:
假设我们在sub/S上进行了自己的工作,在其devel分支上创建了提交f37c219...。然后我们在超级项目的主分支main上创建了新提交;由于某种奇怪的机会,它的哈希ID是abcdef1...。现在我们有了两个更新的存储库,我们可以git push它们。但是有一个顺序约束!假设我们现在推送超级项目:
git push origin main

我们的新提交{{abcdef1}}已经发送到上游仓库,并且Git的{{main}}现在将我们的新提交命名为{{abcdef1}}。我们的新提交指示子模块{{sub/S}}应该在提交{{f37c219}}处检出。因此,Fred在他的电脑上运行{{git clone}}或{{git fetch}}或其他命令,获取我们的提交{{abcdef1}},并告诉他“在使用{{sub/S}}时使用提交{{f37c219}}”。Fred运行{{git submodule update}},他的Git进入{{sub/S}}并尝试检出{{f37c219}},但是糟糕的是,Fred没有{{f37c219}}。实际上,只有我们拥有{{f37c219}},因为我们刚刚创建它!
我们最好很快地cd sub/S并运行git push origin develop。(记住,我们在子模块的develop上做了我们的f37c219。)这样,当Fred尝试访问f37c219时,它至少在某个地方可用。最好先git push那个,然后在超级项目中git push origin main,以推送引用f37c219abcdef1。因此,这导致更新规则#2:首先按最深的子模块顺序推送子模块。这样,每个超级项目都引用一个Fred或其他人可以访问的提交。
对于Fred仍有一个小问题:我们在上面介绍了Fred作为获取(合并或重新设置基础等)引用新子项目提交的超级项目提交的第一人。然而,在这里,Fred代表已克隆我们的超级项目的任何人。他们都有我们的超级项目,并且他们都运行了git submodule update --init --recursive,可能是作为得到超级项目的克隆命令的一部分,因此他们已经拥有了所有子模块。
但是他们的子模块中没有任何新的提交。当他们更新超级项目并告诉Git执行“git submodule update”命令时,他们的Git将进入子模块并找不到正确的提交哈希值。幸运的是,“git submodule update”足够智能,可以为您(或Fred)运行“git fetch”。
然而,为了使其正常工作,更新者必须在线。这意味着在连接时必须运行“git submodule update”。如果您始终处于连接状态,则没有问题,但如果没有,应该有一种简单的方法来提前获取所有子模块。
没有“git submodule fetch”,但有一个可以完成此操作的命令:
git submodule foreach --recursive git fetch

这将在每个子模块中运行git fetch,以更新它们。这样,稍后使用超级项目中的任何提交进行的git submodule update将能够正常工作,即使您处于离线状态且子模块需要更新。


1
谢谢您的非常详细的回复。对我来说太多了,我还没有吸收其中很大一部分。但是关于您的简短回复,我尝试了多个命令,git clone --recurse-submodules 没有填充子模块的工作目录,它在那里,但是是一个空文件夹。 - DeveloperChris
还没有到那一步。我进入了子模块(超级项目内部),删除了一个文件。在子模块中提交了该文件更改,再次将子模块添加到超级项目中,并提交了更改。切换到克隆,获取并合并源超级项目。运行git ls...,它说子模块处于正确的提交状态。在超级项目中运行git submodule update,得到一个错误“引用不是树”。 - DeveloperChris
git ls?这是一个别名吗?如果是,它是用来做什么的?(git: 'ls' 不是一个 git 命令。请参阅 'git --help'。最相似的命令是...) 这个错误信息也很奇怪,因为子模块引用不应该是树,它们是 git 链接,而 git 链接标识的是提交哈希,而不是树哈希。 - torek
非常抱歉我的缩写不太好,我是指 git ls-files --stage submodule - DeveloperChris
git版本1.7.10.4。我认为我的一些文件可能已经损坏了,所以我将完全删除子模块并重新添加它。如果情况有所改善,我会通知您的。 - DeveloperChris
显示剩余12条评论

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