Submodule Won't Update to the Latest Commit

git submodule update keeps checking out an old commit even after you changed .gitmodules. Fix the pointer and sync in three steps.

You edited .gitmodules to point vendor/ui-kit at a new URL, ran git submodule update --init --recursive, and the submodule directory still contains the old commit — or worse, it is empty. Or a teammate committed a submodule bump on their machine but when you pull and run git submodule update, you get a SHA that does not exist in the submodule’s history. Submodules are famously confusing because three separate things must stay in sync: the .gitmodules file, the .git/config section, and the actual SHA pointer stored as a “gitlink” tree entry in the parent repo. A mismatch between any two of these produces an update that silently does nothing or errors out.

Common causes

Ordered by hit rate, highest first.

1. .gitmodules was changed but .git/config was not synced

Editing .gitmodules directly (instead of using git submodule set-url) updates the text file but leaves .git/config’s [submodule] section pointing at the old URL.

How to spot it: Compare git config --file .gitmodules submodule.vendor/ui-kit.url with git config submodule.vendor/ui-kit.url. If they differ, a sync is needed.

A teammate pushed a submodule bump commit before pushing the corresponding commits to the submodule’s remote. The parent repo now references a SHA that does not exist on the public submodule remote.

How to spot it: cd vendor/ui-kit && git fetch && git cat-file -t <SHA> — if it returns fatal: Not a valid object name, the SHA is not available on the remote yet.

3. Submodule was initialized from the wrong URL

The team changed hosting from GitHub to GitLab, updated .gitmodules, but developers who cloned before the change never ran git submodule sync. Their .git/config still has the old GitHub URL.

How to spot it: git remote get-url origin inside the submodule directory returns the old URL.

4. Submodule directory exists as a regular directory (not a git repo)

The submodule was accidentally deleted and re-created as a plain directory, or the .git directory inside the submodule was removed. Git cannot update a plain directory as a submodule.

How to spot it: ls .git/modules/vendor/ui-kit — if this path does not exist, the submodule was never initialized as a nested git repo.

5. --recursive flag omitted for nested submodules

Submodule A contains submodule B. Running git submodule update --init without --recursive updates A but leaves B at the default empty state.

How to spot it: git submodule status --recursive shows entries starting with - (not initialized) or + (wrong SHA) in nested paths.

6. Shallow clone cut off the submodule SHA

The CI pipeline cloned the parent repo with --depth 1 and the submodule with --depth 1, but the submodule’s pinned SHA is older than the shallow horizon.

How to spot it: git -C vendor/ui-kit log --oneline | wc -l returns 1 (shallow boundary), and the required SHA is not in that single commit.

Shortest path to fix

Step 1: Sync .gitmodules into .git/config

# Run from the root of the parent repo
git submodule sync --recursive

This copies the current URLs from .gitmodules into .git/config for all submodules.

Step 2: Initialize and update all submodules

git submodule update --init --recursive

If a submodule SHA is missing from the remote, you will see fatal: reference is not a tree. In that case, ask the teammate who bumped it to push the submodule commits first, then retry.

Step 3: Check out the latest commit on a tracking branch (optional)

If you want the submodule to track a branch instead of a pinned SHA:

git submodule update --remote --merge vendor/ui-kit

This fetches the latest commit from the submodule’s default branch and merges it. Commit the new gitlink pointer in the parent:

git add vendor/ui-kit
git commit -m "chore: bump ui-kit submodule to latest main"

Step 4: Recover a missing submodule directory

# Remove the broken directory and re-initialize
git rm --cached vendor/ui-kit
rm -rf vendor/ui-kit .git/modules/vendor/ui-kit
git submodule add <url> vendor/ui-kit
git submodule update --init vendor/ui-kit

Step 5: Fix shallow clone issues in CI

# Unshallow the submodule to access all history
git -C vendor/ui-kit fetch --unshallow
git submodule update --init --recursive

Or change the CI pipeline’s clone step to git clone --recurse-submodules without --depth.

Prevention

  • Use git clone --recurse-submodules <url> by default so new clones initialize submodules automatically.
  • Add git submodule sync --recursive && git submodule update --init --recursive to your project’s bootstrap script or Makefile target.
  • When bumping a submodule, always push to the submodule remote first, then commit the pointer change in the parent repo.
  • Consider using a package manager (npm, pip, Cargo) instead of submodules for third-party dependencies — submodules are best suited for first-party code you also develop.
  • Set submodule.recurse = true in .gitconfig so git pull automatically updates submodule pointers: git config --global submodule.recurse true.
  • Document the submodule workflow in CONTRIBUTING.md — many submodule bugs come from developers running git pull without knowing they also need git submodule update.
  • In CI, use git submodule update --init --recursive --jobs 4 to parallelize multi-submodule updates.

FAQ

Q: After git submodule update, the submodule is in detached HEAD. Is that normal? A: Yes. Submodule updates check out a specific SHA, not a branch, so detached HEAD is the expected state. If you want to make commits inside the submodule, run git switch -c my-branch inside the submodule directory.

Q: How do I remove a submodule entirely from the repo? A: Run git submodule deinit -f vendor/ui-kit, then git rm -f vendor/ui-kit, then rm -rf .git/modules/vendor/ui-kit. Finally, remove the [submodule "vendor/ui-kit"] section from .gitmodules and commit.

Q: Can I use --depth with submodule update to speed up CI? A: Yes, with git submodule update --init --recursive --depth 1. But if the pinned SHA is older than the depth cutoff, the fetch will fail. Prefer --depth 10 as a safer minimum.

Q: We changed the submodule URL across the whole team. What is the fastest migration path? A: One person updates .gitmodules, commits, and pushes. Everyone else runs git pull && git submodule sync --recursive && git submodule update --init --recursive in that order.

Tags: #git #version-control #Troubleshooting