将 Git 子模块更新为 origin 上的最新提交

1123

我有一个使用Git子模块的项目。它来自于一个ssh://...的URL,并处于A提交状态。B提交已经被推送到该URL,我想让子模块检出该提交并切换到它。

现在,我的理解是git submodule update应该可以做到这一点,但它没有。它什么也不做(没有输出,成功退出代码)。这是一个例子:

$ mkdir foo
$ cd foo
$ git init .
Initialized empty Git repository in /.../foo/.git/
$ git submodule add ssh://user@host/git/mod mod
Cloning into mod...
user@host's password: hunter2
remote: Counting objects: 131, done.
remote: Compressing objects: 100% (115/115), done.
remote: Total 131 (delta 54), reused 0 (delta 0)
Receiving objects: 100% (131/131), 16.16 KiB, done.
Resolving deltas: 100% (54/54), done.
$ git commit -m "Hello world."
[master (root-commit) 565b235] Hello world.
 2 files changed, 4 insertions(+), 0 deletions(-)
 create mode 100644 .gitmodules
 create mode 160000 mod
# At this point, ssh://user@host/git/mod changes; submodule needs to change too.
$ git submodule init
Submodule 'mod' (ssh://user@host/git/mod) registered for path 'mod'
$ git submodule update
$ git submodule sync
Synchronizing submodule url for 'mod'
$ git submodule update
$ man git-submodule 
$ git submodule update --rebase
$ git submodule update
$ echo $?
0
$ git status
# On branch master
nothing to commit (working directory clean)
$ git submodule update mod
$ ...

我还尝试过git fetch mod,它似乎执行了一次获取(但不可能,因为它没有提示输入密码!),但是git loggit show都否认存在新的提交。到目前为止,我只是通过rm删除模块并重新添加它,但这在原则上是错误的,在实践中也很烦琐。


8
David Z的答案似乎是更好的方法——现在Git已经通过“--remote”选项内置了您需要的功能,也许将其标记为已接受的答案而不是Jason的“手动”方法会更有用? - Mark Amery
3
我非常赞同@MarkAmery的观点。虽然Jason提供了一个可行的解决方案,但它并不是预期的方法,因为它将子模块的提交指针留在错误的提交标识符上。新的 --remote 在目前来说肯定是更好的解决方案,而且由于这个问题已经从关于子模块的Github Gist链接过来,我认为让后来的读者看到新答案会更好。 - MutantOctopus
hunter2 密码很不错哦 :o) - lfarroco
17个回答

1843

git submodule update 命令实际上是告诉 Git 你想让子模块检出超级项目中已指定的提交。如果你想要将子模块更新到其远程可用的最新提交,你需要直接在子模块中执行此操作。

因此,总结一下:

# Get the submodule initially
git submodule add ssh://bla submodule_dir
git submodule init

# Time passes, submodule upstream is updated
# and you now want to update

# Change to the submodule directory
cd submodule_dir

# Checkout desired branch
git checkout master

# Update
git pull

# Get back to your project root
cd ..

# Now the submodules are in the state you want, so
git commit -am "Pulled down update to submodule_dir"

或者,如果你很忙:

git submodule foreach git pull origin master

418
git submodule foreach git pull - Mathias Bynens
105
在这种情况下,使用git submodule foreach git pull origin master命令。 - Mathias Bynens
63
现在,经过这些更正和修正,我需要有人撰写一篇解释性的博客文章并将我指向那里。拜托了。 - Suz
43
对“foreach”方法进行微小改进-如果您的子模块中还有子模块,您可能需要在其中添加“--recursive”。因此,命令为: git submodule foreach --recursive git pull origin master - orion elenzil
8
如果每个 Git 子模块都有不同的默认分支,会怎样? - Fernando Montoya
显示剩余11条评论

661

Git 1.8.2推出了一个新选项,--remote,可以启用这种行为。运行

git submodule update --remote --merge

该命令将从每个子模块中获取最新的更改,合并它们并检出子模块的最新修订版本。正如文档所述:

--remote

此选项仅对更新命令有效。不使用超级项目记录的SHA-1来更新子模块,而使用子模块远程跟踪分支的状态。

这等同于在每个子模块中运行git pull <remote> <default_branch>(通常是git pull origin mastergit pull origin main),这通常正是您想要的。


6
这句话的意思是“相当于在每个子模块中运行 git pull”。需要澄清的是,从用户的角度来看,您给出的答案和 git submodule foreach git pull 没有区别。 - Dennis
9
我希望我能以一万倍的赞同来支持这个。为什么 Git 的文档中没有展示这个?这是一个巨大的疏忽。 - serraosays
5
对我来说,它们实际上有很大的区别;foreach git pull只是检查了它们,但没有更新主存储库的指针指向子模块的新提交。只有使用--remote选项才会使其指向最新的提交。 - Ela782
8
为什么需要使用“--merge”选项?它有什么不同之处? - Michel Feinstein
13
现在,由于使用了mastermain分支的混合存储库,git submodule foreach git pull origin master将会失败。因此,git submodule update --remote是更好的解决方案。 - wlbr
显示剩余15条评论

167
在您的项目父目录中运行以下命令:
git submodule update --init

如果您有递归子模块,请运行以下命令:

git submodule update --init --recursive

有时这仍然不起作用,因为在更新子模块时,本地子模块目录中可能会有本地更改。
大多数情况下,本地更改可能不是您想要提交的更改。这可能是由于在您的子模块中删除了文件等原因。如果是这样,请在您的本地子模块目录中进行重置,并在您的项目父目录中再次运行:
git submodule update --init --recursive

7
这是正确的答案。我能以某种方式将其推送到我的远程存储库吗? - Furkan Gözükara
1
这适用于新的子模块!我可以更新所有其他的,但是新子模块的文件夹在运行此命令之前将保持为空。 - Alexis Wilke
3
它不会拉取现有子模块的更改。 - SerjG
3
这将克隆子模块,但仅到主存储库中指定的提交。您需要cd进入子模块文件夹并运行git pull origin <branch_name>以获取最新的提交,在运行git submodule update --init之后。 - jarrad_obrien

88
你的主项目指向子模块应该在的特定提交。git submodule update 尝试在已初始化的每个子模块中检出该提交。子模块实际上是一个独立的存储库 - 只在子模块中创建新提交并推送它是不够的。你还需要显式地将子模块的新版本添加到主项目中。
所以,在你的情况下,你应该在子模块中找到正确的提交 - 假设这是 master 的最新代码。
cd mod
git checkout master
git pull origin master

现在回到主项目,对子模块进行暂存并提交更改:

cd ..
git add mod
git commit -m "Updating the submodule 'mod' to the latest version"

现在推送您的主要项目的新版本:

git push origin master

从此时开始,如果其他人更新了他们的主项目,那么对于他们来说,git submodule update 命令将会更新该子模块,前提是它已经被初始化。


30

在这个讨论中似乎混淆了两种不同的情况:

情况1

使用我的父级仓库指向子模块的指针,我想要检出父级仓库指向的每个子模块的提交记录,在此之前可能需要先迭代所有子模块并从远程更新/拉取。

如之前所指出的那样,这可以通过进行以下操作来完成:

git submodule foreach git pull origin BRANCH
git submodule update

场景2,我认为这是OP的目标

一个或多个子模块中出现了新内容,我想要 1) 拉取这些更改并 2) 更新父仓库以指向这些子模块的最新提交(HEAD)。

操作步骤如下:

git submodule foreach git pull origin BRANCH
git add module_1_name
git add module_2_name
......
git add module_n_name
git push origin BRANCH

这并不是很实用,因为您需要在例如脚本中硬编码n路径到所有n个子模块,以更新父存储库的提交指针。

有一个自动迭代每个子模块,将父存储库指针(使用git add)指向子模块头部会很酷。

为此,我制作了这个小型Bash脚本:

git-update-submodules.sh

#!/bin/bash

APP_PATH=$1
shift

if [ -z $APP_PATH ]; then
  echo "Missing 1st argument: should be path to folder of a git repo";
  exit 1;
fi

BRANCH=$1
shift

if [ -z $BRANCH ]; then
  echo "Missing 2nd argument (branch name)";
  exit 1;
fi

echo "Working in: $APP_PATH"
cd $APP_PATH

git checkout $BRANCH && git pull --ff origin $BRANCH

git submodule sync
git submodule init
git submodule update
git submodule foreach "(git checkout $BRANCH && git pull --ff origin $BRANCH && git push origin $BRANCH) || true"

for i in $(git submodule foreach --quiet 'echo $path')
do
  echo "Adding $i to root repo"
  git add "$i"
done

git commit -m "Updated $BRANCH branch of deployment repo to point to latest head of submodules"
git push origin $BRANCH

要运行它,请执行

git-update-submodules.sh /path/to/base/repo BRANCH_NAME

详细说明

首先,我假设所有存储库中都存在名称为$BRANCH(第二个参数)的分支。请随意使其更加复杂。

前几个部分是一些检查参数是否存在的内容。然后我拉取父存储库的最新内容(如果我只是执行拉取操作,我更喜欢使用--ff(快进)。顺便提一下,我已经关闭了变基)。

git checkout $BRANCH && git pull --ff origin $BRANCH

如果添加了新的子模块或者尚未初始化,可能需要一些子模块初始化:

git submodule sync
git submodule init
git submodule update

然后我更新/拉取所有子模块:

git submodule foreach "(git checkout $BRANCH && git pull --ff origin $BRANCH && git push origin $BRANCH) || true"

请注意几件事情:首先,我使用 && 来连接一些 Git 命令 - 这意味着前一个命令必须无错误执行。

在可能成功拉取(如果远程有新内容)之后,我进行推送以确保客户端上不会留下可能的合并提交。同样地,它只会发生在如果拉取实际带来了新内容。

最后,最终的 || true 确保脚本在错误时继续运行。为使其工作,迭代中的所有内容都必须用双引号括起来,并且 Git 命令被括在括号内(操作符优先级)。

我最喜欢的部分:

for i in $(git submodule foreach --quiet 'echo $path')
do
  echo "Adding $i to root repo"
  git add "$i"
done

迭代所有子模块 - 使用--quiet参数,可以移除 'Entering MODULE_PATH' 的输出。使用'echo $path'(必须用单引号)将子模块的路径写入输出。

相对子模块路径列表被捕获到一个数组中 ($(...)) - 最后迭代这个数组并执行git add $i以更新父存储库。

最后,提交一条消息来解释已经更新了父存储库。如果没有进行任何操作,则默认情况下将忽略此次提交。将其推送到origin,然后完成。

我有一个在Jenkins任务中运行这个脚本,在自动部署之后进行链式调用,非常好用。

希望这对别人有所帮助。


2
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Ger Hobbelt
2
这很好,你说得对,有些答案对问题的理解甚至存在误解,但正如David Z的出色回答所指出的那样,自从2013年中期Git添加了“--remote”选项以来,你的脚本就不再必要了。git submodule update --remote的行为与你的脚本大致相同。 - Mark Amery
@GerHobbelt 谢谢。你说得对,我们只有一级子模块,所以我从来没有考虑过要使它递归。在我验证脚本是否按预期工作之前,我不会更新脚本,但是我的脚本肯定会忽略子子模块。至于文件夹中的空格,这绝对听起来像是要避免的事情! :S - Frederik Struck-Schøning
1
@MarkAmery 感谢您的反馈。然而,我看到了一个问题:不能通过参数指定子模块的分支。从git手册中可以看到:“使用的远程分支默认为master,但是可以通过在.gitmodules或.git/config(以.git/config优先)中设置submodule.<name>.branch选项来覆盖分支名称。” 我不想每次想要将其应用于除master之外的另一个分支时都编辑.gitmodules或.git/config。但也许我错过了什么?此外,该方法似乎强制进行递归合并(因此错过了快进的可能性)。 - Frederik Struck-Schøning
最后一件事:我尝试了@DavidZ的方法,但它似乎并没有完成我想要做的确切的事情(也是op所问的):将子模块的HEAD提交添加到父级(即“更新指针”)。然而,它似乎非常出色地完成了唯一的工作(并且更快),即获取和合并所有子模块中的最新更改。不幸的是,默认情况下仅从主分支获取(除非您编辑.gitmodules文件(请参见上文))。 - Frederik Struck-Schøning
@GerHobbelt git submodule foreach --quiet "printf '%q\n' \"\$(pwd)\"" 可以帮助处理包含空格和其他特殊字符的路径;手动 %q 选项 man --pager='less -p " %q "' printf 表示 "ARGUMENT is printed in a format that can be reused as shell input, escaping non-printable characters with the proposed POSIX $'' syntax"... 例如,printf '%q\n' "/foo bar/path" 的输出结果为 /foo\ bar/path - S0AndS0

29
请注意,尽管现代化的子模块提交更新形式如下:
git submodule update --recursive --remote --force

请参阅Gabriel Staples的答案,以获得另一种方法,不使用--merge --force--force选项允许即使包含存储库中的索引中指定的提交已与检出在子模块中的提交匹配,也可以进行检出。
在这种情况下,--merge选项似乎是不必要的:“记录在超级项目中的提交将合并到子模块中的当前分支中。”

旧的形式是:

git submodule foreach --quiet git pull --quiet origin

除此之外……这个第二种形式并不是真正的“安静”。

请参见commit a282f5a(2019年4月12日)由Nguyễn Thái Ngọc Duy (pclouds)提交。
(由Junio C Hamano -- gitster --commit f1c9f6c合并,2019年4月25日)

submodule foreach:修复了未能遵守“<command> --quiet”的问题

Robin reported that

git submodule foreach --quiet git pull --quiet origin

is not really quiet anymore.
It should be quiet before fc1b924 (submodule: port submodule subcommand 'foreach' from shell to C, 2018-05-10, Git v2.19.0-rc0) because parseopt can't accidentally eat options then.

"git pull" behaves as if --quiet is not given.

This happens because parseopt in submodule--helper will try to parse both --quiet options as if they are foreach's options, not git-pull's.
The parsed options are removed from the command line. So when we do pull later, we execute just this

git pull origin

When calling submodule helper, adding "--" in front of "git pull" will stop parseopt for parsing options that do not really belong to submodule--helper foreach.

PARSE_OPT_KEEP_UNKNOWN is removed as a safety measure. parseopt should never see unknown options or something has gone wrong. There are also a couple usage string update while I'm looking at them.

While at it, I also add "--" to other subcommands that pass "$@" to submodule--helper. "$@" in these cases are paths and less likely to be --something-like-this.
But the point still stands, git-submodule has parsed and classified what are options, what are paths.
submodule--helper should never consider paths passed by git-submodule to be options even if they look like one.


Git 2.23 (2019年第三季度)修复了另一个问题:当使用“--recursive”选项时,“git submodule foreach”未正确保护传递给每个子模块运行的命令的命令行选项。请参见commit 30db18b(2019年6月24日),作者为Morian Sonnet(momoson)(由Junio C Hamano -- gitster --commit 968eecb合并,2019年7月9日)

submodule foreach: fix recursion of options

Calling:

git submodule foreach --recursive <subcommand> --<option>

leads to an error stating that the option --<option> is unknown to submodule--helper.
That is of course only, when <option> is not a valid option for git submodule foreach.

The reason for this is, that above call is internally translated into a call to submodule--helper:

git submodule--helper foreach --recursive \
   -- <subcommand> --<option>

This call starts by executing the subcommand with its option inside the first level submodule and continues by calling the next iteration of the submodule foreach call

git --super-prefix <submodulepath> submodule--helper \
  foreach --recursive <subcommand> --<option>

inside the first level submodule. Note that the double dash in front of the subcommand is missing.

This problem starts to arise only recently, as the PARSE_OPT_KEEP_UNKNOWN flag for the argument parsing of git submodule foreach was removed in commit a282f5a.
Hence, the unknown option is complained about now, as the argument parsing is not properly ended by the double dash.

This commit fixes the problem by adding the double dash in front of the subcommand during the recursion.


请注意,在Git 2.29之前(2020年第四季度), "git submodule update --quiet" (man)未压制底层的"rebase"和"pull"命令。
请参见commit 3ad0401(2020年9月30日)由Theodore Dubois (tbodt)提交。
(由Junio C Hamano -- gitster --commit 300cd14中合并,2020年10月5日)

submodule update: 使用"--quiet"选项,使底层合并/变基静默无声

签名作者:Theodore Dubois

Commands such as

$ git pull --rebase --recurse-submodules --quiet  

produce non-quiet output from the merge or rebase.
Pass the --quiet option down when invoking "rebase" and "merge".

Also fix the parsing of git submodule update(man) -v.

When e84c3cf3 ("git-submodule.sh: accept verbose flag in cmd_update to be non-quiet", 2018-08-14, Git v2.19.0-rc0 -- merge) taught "git submodule update"(man) to take "--quiet", it apparently did not know how ${GIT_QUIET:+--quiet} works, and reviewers seem to have missed that setting the variable to "0", rather than unsetting it, still results in "--quiet" being passed to underlying commands.


在 Git 2.38 (2022年第三季度) 中,git-submodule.sh 准备成为内置命令,这意味着上述存在问题的 submodule--helper 将被淡化。

请查看由Ævar Arnfjörð Bjarmason (avar)于2022年6月28日提交的commit 5b893f7, commit 2eec463, commit 8f12108, commit 36d4516, commit 6e556c4, commit 0d68ee7, commit d9c7f69, commit da3aae9, commit 757d092, commit 960fad9, commit 8577525
请查看由Glen Choo (chooglen)于2022年6月28日提交的commit b788fc6
(在commit 361cbe6中由Junio C Hamano -- gitster --合并,日期为2022年7月14日)

git-submodule.sh: 使用 "$quiet",而不是 "$GIT_QUIET"

签名作者:Ævar Arnfjörð Bjarmason

自从b3c5f5c(“submodule: move core cmd_update() logic to C”,2022年3月15日,Git v2.36.0-rc0 -- merge),我们已经不再使用"$GIT_QUIET"变量,而是使用我们自己的"$quiet"。在git-sh-setup.sh中,只有使用"GIT_QUIET"才会受到影响的是say函数。

然而,我们仍然希望支持--quiet,但让我们为此使用我们自己的变量。
现在很明显,我们只关心将"--quiet"传递给git submodule--helper,而不是更改任何"say"调用的输出。


在cmd中,git submodule update --recursive --remote --merge --force命令中,--force似乎很危险,因为它会丢弃子模块中的本地更改,而--merge则让我感到困惑。我无法弄清楚它在做什么,除了在运行该命令后,当我进入子模块查看是否有更改时,它可以保持我的子模块实际上没有更改。请问您能否验证我的对--force的理解,并帮助我理解--merge的细微差别? - Gabriel Staples
通常情况下,这是一个复杂的话题。以下是我迄今为止的总结和结论。 - Gabriel Staples
@GabrielStaples 很好的建议。我已经删除了--merge选项(保留了--force选项,并提供了文档),并引用了您自己的答案(已赞)。 - VonC

23

简单明了,获取子模块的方法:

git submodule update --init --recursive

现在继续将它们更新到最新的主分支(例如):

git submodule foreach git pull origin master

10
git pull --recurse-submodules

这将会拉取所有最新的提交。


10
如何更新存储库中的所有git子模块(两种方法,实现两个非常不同的功能!)
快速摘要
# Option 1: as a **user** of the outer repo, pull the latest changes of the
# sub-repos as previously specified (pointed to as commit hashes) by developers
# of this outer repo.
# - This recursively updates all git submodules to their commit hash pointers as
#   currently committed in the outer repo.
git submodule update --init --recursive

# Option 2. As a **developer** of the outer repo, update all subrepos to force
# them each to pull the latest changes from their respective upstreams (ex: via
# `git pull origin main` or `git pull origin master`, or similar, for each
# sub-repo). 
git submodule update --init --recursive --remote
#
# For just Option 2 above: now add and commit these subrepo changes 
# you just pulled
git add -A
git commit -m "Update all subrepos to their latest upstream changes"

详情

选项1:作为外部仓库的用户,尝试将所有子模块恢复到开发者预期的状态:
git submodule update --init --recursive

选项2:作为外部仓库的开发者,尝试将所有子模块更新到每个远程仓库默认分支推送的最新提交(即将所有子仓库更新到每个子仓库开发者预期的最新状态):
git submodule update --init --recursive --remote
...代替使用git submodule foreach --recursive git pull origin mastergit submodule foreach --recursive git pull origin main

对我来说,以上两个选项的最佳答案似乎是不要使用我在其他答案中看到的--merge--force选项。

上述选项的说明:

  • --init部分用于初始化子模块,以防您刚克隆了仓库并且尚未执行此操作
  • --recursive用于子模块内部的子模块,递归下去直到无穷
  • --remote表示将子模块更新为默认远程存储库上默认分支的最新提交。在大多数情况下,它类似于执行git pull origin mastergit pull origin main,例如对于每个子模块。如果要更新到最外层仓库(超级仓库)指定的提交,请省略--remote

git submodule foreach --recursive git pull(不要使用此选项--它常常失败)与git submodule update --recursive --remote(使用此选项--它总是有效)

我在此答案下方留下了以下评论。我认为它们很重要,所以我也将它们放在我的答案中。

基本上,对于某些情况,git submodule foreach --recursive git pull 可能有效。对于其他情况,git submodule foreach --recursive git pull origin master 可能更适合你。对于其他情况,git submodule foreach --recursive git pull origin main 可能更适合你。而对于其他情况,甚至可能没有任何一个命令可以更新你的外部 repo,因为每个子模块可能需要不同的命令来从其默认远程和默认分支更新自己。然而,在我找到的所有情况中,这个方法有效,包括你可能使用上述几个 git submodule foreach 命令的所有情况。所以,请使用这个方法:

git submodule update --recursive --remote

无论如何,这是我对此答案下方的几点评论:

(1/4)@DavidZ,很多人认为git submodule foreach git pullgit submodule update --remote是相同的命令,只是后者是新的命令。然而,它们并不相同。git submodule foreach git pull在多种情况下会失败,而git submodule update --remote却可以正常工作!如果你的子模块指向一个没有分支指向它的提交哈希值,这在实际开发中经常出现,因为你想要一个特定版本的子模块用于外部仓库,那么这个子模块...
(2/4)就处于分离 HEAD 状态。在这种情况下,git submodule foreach git pull无法在该子模块上运行git pull,因为分离 HEAD 不能有上游分支。然而,git submodule update --remote却可以正常工作!它似乎会在该子模块上调用git pull origin main,如果origin是默认远程,并且main是该默认远程上的默认分支,或者例如git pull origin master,如果origin是默认远程,但master是默认分支。
(3/4)此外,git submodule foreach git pull origin master在许多情况下甚至会失败,而git submodule update --remote却可以正常工作,因为许多子模块使用master作为默认分支,而许多其他子模块使用main作为默认分支,因为GitHub最近从与美国奴隶制有关的术语(“master”和“slave”)更改为main
(4/4)因此,我添加了显式的远程和分支,以使它更清晰,这些通常是必需的,并提醒人们git pull通常是不够的,git pull origin master可能无法工作,git pull origin main可能在前者不起作用时起作用,但也可能不起作用,而且没有一个单独的命令与git submodule update --remote相同,因为后者的命令足够聪明,可以为每个子模块执行git pull <default_remote> <default_branch>,并根据需要调整远程和分支。

相关研究和其他内容

我的一般性的git submodule笔记:https://github.com/ElectricRCAircraftGuy/eRCaGuy_dotfiles#git-submodules-and-git-lfs-how-to-clone-this-repo-and-all-git-submodules-and-git-lfs-files 如何找到一个repo的主分支:https://dev59.com/al4b5IYBdhLWcg3wzUfa#49384283 如何通过git submodule foreach <cmd>在每个子repo中运行自定义命令来更新每个子repo:https://dev59.com/RW025IYBdhLWcg3wvIko#45744725 man git submodule - 然后搜索foreach--remote等。
我在How to resolve conflicts with git submodules, in your outer repo containing them上的回答。

1
非常感谢您提供的所有额外细节! - robrecord

9
这对我来说是更新到最新提交的有效方法:
git 子模块更新 --递归--远程--初始化

4
此问题已经有很多类似但不完全相同的答案。如果您能解释一下你的回答如何改进当前已有的答案,那会很有帮助。 - joanis

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