T

Besides obvious functionality (like adding a submodule, cloning a repo together with its submodules, etc.) and a few edge cases, Pro Git book explains how to control submodules while running commands from superproject directory.

The book does a good job covering “what” and “how”, but I feel like it doesn’t give enough attention to when, when not to and why. In this article I am trying to bridge that gap in regard to submodules.

As the book author points out – if you’re using submodules, you’re probably doing so, because you’re working on both superproject and submodule at the same time, otherwise you’d probably use a simpler library management system.

But actually, it is still not good enough reason to use submodules.

For example, I am working on this Hugo site as well as its Hugo theme as a submodule. But while writing this article I realized that I don’t need to use submodules, because the key feature and the actual reason why you would want to use submodules is to be able to track the exact version of submodules in your superproject. This is what submodules are actually for. If the state of superproject is effectively independent from submodule’s state, using simple clone inside superproject and adding the repo to .gitignore is enough.

On the other hand if it’s not a single-dev project, submodules are the next thing to consider besides Git Subtree.

If the amount of nested repos is large, there are other option as well, which have their own caveats, such as Monorepo 1, Repo tool (form Google) and a few others. These methods are outside the scope of this article.

Updating submodules

git submodule update is the command that updates the submodule from superproject directory.

It is very important to understand what we mean by updating a submodule; in other words, what git submodule update actually does – which is one or two things depending on if the first thing is already done (e.g., by git fetch or git pull):

  1. It fetches commits of the default (the one HEAD point to on remote or the one specified in submodule config file) remote branch, but only up to the commit registered in superproject; adding --remote flag fetches all new commits of that branch.
  2. It switches HEAD pointer to the last fetched commit no matter where it was before (even if it was detached – not safe for your detached-HEAD experiments – more on this below).

git submodule update command, as is, make sense only if your submodule is pull-only; meaning you never touch it and just want to update it to the latest version “compatible” with its superproject.

Features you don't need

If you actually work on the submodule and someone pushes new changes to the submodule remote, you can merge the new commits to your local changes by git submodule update --remote --merge or use git submodule update --remote --rebase to rebase your local work on top of the fetched changes. For this to work you should be checked at a working branch in the submodule, so that --merge or --rebase have something to work with.

I don’t find it to be a good habit merging/rebasing submodule changes this way though. Unless you are super confident what HEAD in the submodule points to, it is much better to go there (or just switch the terminal instance, since you probably already have it opened) and actually look. And since you’re already there, you might just as well perform the merge the way you always did.

Let’s say you’re checked out at a feature branch in a submodule and run git submodule update --remote --merge, in which case you might end up merging remote’s default (or configured) branch into that feature branch. Which is probably not what you want. You may also lose your local changes if you were detached at commit without a branch; sure, you can recover those “lost” commits, though you wont be notified that they are gone.

Pulling with superproject

If you’re like me, who likes to experiment without a branch (or even if you don’t), you’d probably want to keep the default pull behavior, otherwise you might lose your work, just like you would moving HEAD away from commit without a branch; the problem is, again, that you don’t exactly see that you’re checked out at commit, not branch, when running pull from superproject directory.

It makes sense to use pull --recurse-submodules if your submodules are pull-only; meaning you never touch them. In this case you could even set submodule.recurse option in your superproject config to true: git config submodule.recurse true; but don’t set it to true in your global config.

Switching branches

It is important to remember that switching branches that point at different submodule states does not align the actual state with recorded state in the branch. That means that after switching branches the commit pointer (160000 mode) in superproject will appear as modified, pointing at the commit, submodule’s HEAD is pointing at.

Unless state of the branch you’re switching to is functionally dependent on submodule’s state the branch (last commit at the branch that records submodule state) has a record of, that is probably what you want 2, otherwise you can force Git (>= 2.13) to align submodules state to recorded version in the branch by running git checkout with --recurse-submodules flag. Since 2.14, Git allows you to set the config option submodule.recurse to true, which automates this.

Turning submodule.recurse “on” makes submodule updates (git submodule update) automatic (since Git 2.15) for git pull as well, which as discussed above, might not be optimal if you actively work on submodules and don’t want pull to auto-update them. In this case the solution would be an alias that only affects checkout: git config --global alias.superswitch 'checkout --recurse-submodules'.


  1. Monorepo is just a concept of one repository containing multiple projects/packages that could otherwise be separate repos; git subtree add is one way to populate a monorepo; subtree split is used to publish parts of monorepo if you need to. ↩︎

  2. If superproject state upon switching branches does not have a problem with unchanged state of its submodule, you get to keep the latest version of the submodule. ↩︎