在 git 有和没有子模块的分支之间切换的可靠命令集

Reliable set of command to switch between git branches with and without submodules

我正在尝试创建一个 node/go 库 (https://github.com/simpleviewinc/git-tools) 以帮助使用 git 将回购检出到特定 remote/branch 以处理生产检出以及开发人员检查以供同行评审。在这两种情况下,当开发人员执行结帐命令时,我希望它执行并将工作副本设置为远程存储库中声明的确切状态。所以这意味着它将抛出任何未跟踪的更改、未推送的提交,所有这些。这是为了使他们的本地工作副本或服务器工作副本与远程完全匹配。在生产中,这显然是为了让实时服务器匹配准确的状态。在本地,它确保开发人员拥有他们正在审查的分叉的确切状态(本地盒子上的更改不会干扰审查)。在本地,它会在执行破坏性操作之前提示用户(如 git 重置,git 清理)。

我面临的问题是,如果 b运行 ch 有子模块。在我的测试中,我有一个包含 3 个 b运行ches 的 repo,一个 b运行ch 没有子模块(master),另一个模块有 1 个子模块(submodule-test),另一个 b运行ch 有另一个子模块(submodule-test2)指向不同的存储库(在同一路径)。我希望我的库能够将工作副本从任何 b运行ch 切换到任何其他 b运行ch,执行完全相同的一组命令,而不需要开发人员执行需要知道具体设置与目的地 b运行ch 有关。基本上,它应该是“无论它在遥控器中声明什么,都给我这个代码”。例如,如果开发人员正在进行同行评审,并且主仓库和子模块都有 forks/branches。我希望开发人员能够只使用 gun git-tools checkout proj1 --remote=dev1 --branch=pr-150 并且它将检查 dev1 的 proj1 和 b运行ch pr-150 的分支。然后,如果他们 运行 git-tools checkout proj1 它将切换回 proj1 的主人。

现在,我几乎可以开始工作的最接近的切换 b运行ches 的命令集是:

git submodule deinit --all
git checkout branch
git submodule sync
git submodule update

这几乎可以工作,除了第一次从一个带有子模块的 b运行ch 切换到另一个带有不同子模块的 b运行ch 时失败(或同一子模块的不同遥控器) -模块,例如开发者分支)。

这里的示例是一组命令及其在 git 2.20.1

中的失败
cd /tmp
git clone git@github.com:simpleviewinc/git-tools-test.git ./checkout --recurse-submodules
cd checkout
git checkout submodule-test
git submodule sync
git submodule update
# branch submodule-test fully checked out, all submodules downloaded, looking good!
git submodule deinit --all
git checkout submodule-test2
git submodule sync
git submodule update
fatal: remote error: upload-pack: not our ref c1bba6e3969937125248ee46e308a8efec8ac654
Fetched in submodule path 'submodule', but it did not contain c1bba6e3969937125248ee46e308a8efec8ac654. Direct fetching of that commit failed.

失败是因为它使用了错误的远程子模块,尽管我认为这是子模块同步的明确目的。如果我从 submodule-test 转到 master,它会成功,但如果 master 有一个子模块,它就会失败,所以这没有帮助。

我试过 --recurse-submodules 但也失败了,但是这次是从没有子模块的 b运行ch 检查到有子模块的 b运行ch 时。

cd /tmp
git clone git@github.com:simpleviewinc/git-tools-test.git ./checkout --recurse-submodules
cd checkout
git checkout submodule-test --recurse-submodules
fatal: not a git repository: ../.git/modules/submodule
fatal: could not reset submodule index

Master 没有子模块,所以当我切换到另一个 b运行ch 时,出现了问题。

必须有一些 git 咒语可以可靠地让你从 b运行ch A 切换到 b运行ch B 可以是 运行任何 b运行ch A 和任何 b运行ch B 而不管正在播放的子模块。如果您检查我正在测试的存储库,它基本上是空的,所以您执行完全相同的命令并看到我遇到的完全相同的错误是完全安全的。最终似乎需要在 .git/config、.git/modules/module-name/config 和 .git 模块之间进行同步,但我无法找出满足目标的依赖序列集。任何帮助都将是不可思议的,因为我已经花了很长时间试图对付这些愚蠢的子模块。

我想通了。这不是微不足道的,但到目前为止它正在工作。关键要素是,在从一个分支切换到另一个分支之前,我根据当前分支的名称将 .git/modules 文件夹存储在某处。这样,当我切换回该分支时,我可以恢复隐藏的模块,因为它存储了该分支上所有活动子模块的 git 存储库信息。

从任意一个分支到任意一个分支的大致流程如下:

export TARGET_BRANCH="my-branch-name"
export CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ -f ".gitmodules" ]; then
  git submodules deinit --all
  mkdir -p .git/git-tools/modules
  mv .git/modules .git/git-tools/modules/$CURRENT_BRANCH
fi

git checkout $TARGET_BRANCH

if [ -f ".gitmodules" ]; then
  if [ -f ".git/git-tools/modules/$TARGET_BRANCH" ]; then
    git mv .git/git-tools/modules/$TARGET_BRANCH .git/modules
  fi

  git submodule sync && git submodule update --init
fi

这个问题是asked on the Git mailing list,我在那里回答了。在这里复制我的答案,稍作编辑:

Currently, the only method I've seen that you can reliably use to switch between different branches when they don't all have the same contained submodules comes from the Stack Overflow answer at . I'll reproduce the Bash snippet it presents as a solution here for completeness's sake:

export TARGET_BRANCH="my-branch-name"
export CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ -f ".gitmodules" ]; then
   git submodules deinit --all
   mkdir -p .git/git-tools/modules
   mv .git/modules .git/git-tools/modules/$CURRENT_BRANCH
fi

git checkout $TARGET_BRANCH

if [ -f ".gitmodules" ]; then
   if [ -f ".git/git-tools/modules/$TARGET_BRANCH" ]; then
     git mv .git/git-tools/modules/$TARGET_BRANCH .git/modules
   fi

   git submodule sync && git submodule update --init
fi

在我看来是复杂且不必要的。底线是:

  1. git checkout --recurse-submodules $ref应该总是工作在理想状态 世界,但截至今天(Git 2.33.0,2021 年 9 月)仍有一些缺失。
  2. git checkout $ref && git submodule sync --recursive && git submodule update --recursive 应该总是有效。

这里是:

The problem I am facing is that I cannot figure out a set of git commands that will consistently work in all cases when switching between branches if the branch has submodules. In my test, I have one repo with 3 branches, one branch has no submodules (master), another module [sic, should be "branch"] has 1 submodule (submodule-test), and another branch has another submodule (submodule-test2) that points to a different repository (at the same path).

[...]

In example here is one set of commands and their failure in git 2.20.1

cd /tmp
git clone git@github.com:simpleviewinc/git-tools-test.git ./checkout --recurse-submodules
cd checkout
git checkout submodule-test
git submodule sync
git submodule update
# branch submodule-test fully checked out, all submodules downloaded, looking good!
git submodule deinit --all
git checkout submodule-test2
git submodule sync
git submodule update
fatal: remote error: upload-pack: not our ref c1bba6e3969937125248ee46e308a8efec8ac654
Fetched in submodule path 'submodule', but it did not contain c1bba6e3969937125248ee46e308a8efec8ac654. Direct fetching of that commit failed.

It fails because it uses the wrong submodule remote, even though I thought that was the explicit purpose of submodule sync.

失败的原因是 git submodule sync 没有 更新 remote.origin.url 配置在子模块的 Git 配置文件中,即 /tmp/checkout/.git/modules/submodule/config,尽管(令人困惑地)输出“为 'submodule' 同步子模块 url”。它确实改变了 submodule.$name.url 配置文件 的超级项目 中的值,虽然 (/tmp/checkout/.git/config) 但 git submodule update 仅使用 remote.origin.url 在子模块的配置文件中(如果存在)。

第二个git submodule sync没有变化的原因remote.origin.url是因为子模块在 命令是运行,因为前面的命令是git submodule deinit --all,它取消了所有子模块的初始化。所以这第二步改变 remote.origin.url 被取消初始化的子模块跳过(这个事实是 文档中缺少)。

以下顺序是正确的:

git clone git@github.com:simpleviewinc/git-tools-test.git ./checkout [--recurse-submodules] # --recurse-submodules is optional
cd checkout
git checkout submodule-test
git submodule update --init
git checkout submodule-test2
git submodule sync
git submodule update
# and we can switch back
git checkout submodule-test
git submodule sync
git submodule update

请注意,初始化所有子模块的正确命令是 git submodule init,或者,要初始化、克隆并一步检查它们,git submodule update --init。第一个 git submodule sync 用于初始化子模块的事实实际上是由于对 git clone 使用 --recurse-submodules,它将 submodule.active 设置为匹配所有路径规范 . 在超级项目的配置中,git submodule sync 递归到 active 子模块(文档中也没有)。

所以对于 git checkout,你想要实现的可以用一个 post-结帐挂钩:

#!/bin/sh

# If the checkout was a branch checkout [1], update the submodules
# to the commits recorded in the superproject
# [1] https://git-scm.com/docs/githooks#_post_checkout

previous_head=
new_head=
checkout_type_flag=

if [ "$checkout_type_flag" -eq 1 ] ; then
   git submodule sync --recursive
   git submodule update --init --recursive
fi

在这里,我将 --recursive 添加到两个命令中,以防万一您的任何子模块本身包含子模块。


现在,大约 git checkout --recurse-submodules:

I tried --recurse-submodules but that fails too, but this time when checking from a branch without submodules to a branch with submodules.

cd /tmp
git clone git@github.com:simpleviewinc/git-tools-test.git ./checkout --recurse-submodules
cd checkout
git checkout submodule-test --recurse-submodules
fatal: not a git repository: ../.git/modules/submodule
fatal: could not reset submodule index

是的,那个很糟糕。失败的原因是:

  1. git clone --recurse-submodules 实际上几乎 运行 是一个简单的 git submodule update --init --recursive 在过程结束时,在检查工作树的步骤。这意味着只有 在被签出的分支 中出现的子模块才会被初始化和克隆。

  2. git clone --recurse-submodules 总是submodule.active=. 写入超级项目的配置。

  3. git checkout --recurse-submodules 递归到活动子模块中,为此它需要访问子模块的 Git 存储库,该存储库尚不存在,因为它未被克隆,并且所以它出错了。

这与您尝试递归检出旧版本时遇到的错误相同 包含自删除后的子模块的修订 [1], [2].

我在这些帖子中提出了一些可以改进所有这些的方法;这是我对这个主题的最新看法:

  1. git clone --recurse-submodules 应该至少可以克隆 all 所有分支的子模块被克隆,并将它们的Git目录放在.git/modules/中。这将允许您的用例与 git checkout --recurse-submodules.

    一起“正常工作”
  2. git clone --recurse-submodules 也可以被教导为 all 版本的 all 分支克隆所有子模块被克隆。这个会 允许我提到的“删除的子模块”案例起作用,但不会 在所有情况下都需要,因此它可以作为 git clone.

    的补充标志
  3. git fetch 应该学会克隆新的子模块。

  4. git checkout --recurse-submodules 可以教克隆丢失 子模块并获取丢失的子模块提交。这将涵盖您的 用例以及“删除的子模块”用例。

  5. 无论如何,git checkout --recurse-submodules如果找不到子模块的Git仓库并离开.gitmodules,不应该中途退出 在工作树中未跟踪,.git/modules/$name/ 中的 config 文件仅设置了 core.worktree

git checkout --recurse-submodules $commit 在子模块 url/path 在当前状态和 $commit 之间变化的情况下对我不起作用,我尝试了以下运行良好的命令(也被引用 here):

  git checkout $commit
  git submodule sync --recursive
  git submodule update --init --recursive
  git clean -ffdx

这里关键是submodule sync刷新子模块path/urls