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

I'm attempting to create a node/go library (https://github.com/simpleviewinc/git-tools) to help using git to checkout a repo to a specific remote/branch for handling production checkouts as well as developer checkouts for peer review. In both cases when a developer executes the checkout command I want it to execute and setup the working copy to the exact state declared in the remote repositories. So this means it will toss any untracked changes, unpushed commits, all of that. It is to make their local working copy or the server working copy exactly match the remotes. In production this is obviously so that the live server matches the exact state. On local, it ensures that the developer has the exact state of the fork that they are reviewing (with no changes on their local box interfering with the review). On local, it will prompt the user before doing destructive actions (like git reset, git clean).

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 has 1 submodule (submodule-test), and another branch has another submodule (submodule-test2) that points to a different repository (at the same path). I want my library to be able to switch the working copy from any branch to any other branch, executing the exact same set of commands, without the developer executing needing to know the specific setup related to the destination branch. Basically, it should be "give me this code whatever it is declared in the remote". In example if a dev is peer reviewing and both the main repo and the submodule have forks/branches. I want the dev to be able to just gun git-tools checkout proj1 --remote=dev1 --branch=pr-150 and it will checkout dev1's fork of proj1 and branch pr-150. Then if they ran git-tools checkout proj1 it would switch it back to master of proj1.

Right now the closest set of commands for switching branches I've been able to almost get working is:

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

That almost works, except for it fails when switching from one branch with submodules to another branch with different submodules the first time (or a different remote for the same sub-module, such as a developer fork).

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

cd /tmp
git clone [email protected]: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. If I go from submodule-test to master, it will succeed, but if master has a submodule it fails, so that doesn't help.

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 [email protected]: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 doesn't have submodules so when I switch to another branch, something goes awry.

There has to be some git incantation that will reliably allow you to switch from branch A to branch B that can be run for any branch A and any branch B regardless of the submodules in play. If you examine the repo I'm testing with, it's basically empty, so it's perfectly safe for you to execute the exact same commands and see the exact same errors I'm hitting. Ultimately it seems like something needs to be synchronized between .git/config, .git/modules/module-name/config and the .gitmodules, but I cannot figure out a rely set of sequences that meets the goal. Any assistance would be incredible as I've already spent way to long trying to beat my head against these stupid submodules.


Solution 1:

I was able to figure this out. It's not trivial but so far it's working. The key element is that prior to switching from one branch to another I stash the .git/modules folder somewhere based on the name of the current branch. This way when I switch back to that branch, I can restore the stashed modules since that stores the git repository information for all of the active submodules on that branch.

Roughly the process of going from any branch to any other branch is as follows:

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