Highly coupled git submodules

A few notes for anyone else happening across this!

The biggest mistake rookies are going to make is committing with detached HEAD in the submodule, after having done a submodule update. I’m going to try to counter this with strong warnings from hooks.

The next biggest will probably be failing to do a submodule update after doing a checkout which requires one. Again, hooks can check for this and warn.

As for development process, this setup makes it much more important to have a good test infrastructure in the submodule, so that if possible you can work in it without having to do work in the parent, and avoid the issue entirely.

I’ll try and post sample code from the hooks I end up using, and follow up after a month with (hopefully not too many) true horror stories.


Here are the first drafts of the hooks. Keep in mind this is a rush job and go easy on me!

In the parent repo:

For post-merge and post-checkout, we warn the user if the submodule’s out of sync. (post-merge is included in particular for fast-forward merges, pulling from origin) Also note that they’ll want to check out a branch, though the submodule’s post-checkout hook will also do that when they run submodule update. The more reminders the merrier.

if git submodule status | grep '^+' > /dev/null; then
    echo "WARNING: common model submodule now out of sync. You probably want to run" 1>&2
    echo "         git submodule update, then make sure to check out an appropriate branch" 1>&2
    echo "         in the submodule." 1>&2

For post-commit, if there are submodule changes, we warn the user that they may have forgotten to include them in their commit. In this highly coupled case, this is a very good guess. It’s unlikely the user will have modified the simulation and common models separately.

if git submodule status | grep '^+' > /dev/null; then
    echo "WARNING: common model submodule has changes. If the commit you just made depends" 1>&2
    echo "         on those changes, you must run git add on the submodule, and then run" 1>&2
    echo "         git commit --amend to fix your commit." 1>&2

And in the submodule, a post-checkout hook to strongly warn about detached HEAD:


get_ppid() {
    ps --no-headers -o ppid $1

# Check to see if this checkout is part of a submodule update
# git submodule calls git checkout, which calls this script, so we need to
# check the grandparent process.
if ps --no-headers -o command $(get_ppid $(get_ppid $$)) | grep 'submodule update' &> /dev/null; then
    if ! git symbolic-ref HEAD &> /dev/null; then
        echo "WARNING: common model submodule entering detached HEAD state. If you don't know" 1>&2
        echo "         what this means, and you just ran 'git submodule update', you probably" 1>&2
        echo "         want to check out an appropriate branch in the submodule repository." 1>&2
        # escape the asterisk from SO's syntax highlighting (it sees C comments)
        branches=($(git for-each-ref --format="%(objectname) %(refname:short)" refs/heads/\* | grep ^$(git rev-parse HEAD) | cut -d\  -f2))
        case ${#branches} in
            0 )
            1 ) 
                echo "Branch '${branches[0]}' is at HEAD"
            * )
                echo "The following branches are at HEAD: ${branches[@]}"

I’m also adding a pre-commit hook to simply abort commits made with detached HEAD (unless it’s a rebase). I’m pretty terrified of getting the classic “all of my commits disappeared” panicked complaint. You can always bypass it with --no-verify if you know what you’re doing.

Leave a Comment