TortoiseGit Submodules: Commit SHAs, Update Workflow, and Common Pitfalls
TortoiseGit maps submodule actions to Git commands, but the parent repository records only a commit SHA. Learn the correct Add/Update workflow, how to stage pointer changes, and why recursive updates can stall.
18 Apr 2026, 15:34 UTC

The parent repo tracks a commit SHA, not a branch
If you add a submodule in TortoiseGit, commit, and push, a teammate who clones the containing repository—called the superproject—will often see an empty folder. The reverse also surprises people: you pull the latest branch tip inside the submodule, but the parent repository still points to an older commit and your build breaks. The useful answer is that TortoiseGit’s submodule dialogs are a thin shell over Git’s native model—the superproject stores a gitlink (a commit SHA), not a branch name, working-tree state, or unpushed commits. Treat the submodule pointer as part of your parent commit, and use Submodule Update to populate the working tree at the recorded SHA, not at the branch tip.
How TortoiseGit maps to Git submodule commands
TortoiseGit exposes submodule actions through the Windows Explorer context menu and the commit dialog. Under the hood they map to three core Git commands:
git submodule add <remote-url> <path>— clones the submodule, writes an entry in.gitmodules, and stages the gitlink.git submodule update --init— initializes and checks out the recorded SHA for each submodule listed in the superproject’s index.git submodule update --init --recursive— repeats the process for nested submodules at every level of the hierarchy.
The .gitmodules file records the remote URL and the relative path. The superproject’s tree records only the commit SHA that the submodule should be at. All TortoiseGit submodule operations require write access to the local working tree and the ability to fetch from the configured remotes. TortoiseGit does not store submodule credentials separately; authentication is delegated to Git’s configured credential helpers or SSH agent.
Worked example: add, commit, and update
Assume a superproject at C:\repos\app and a library you want at libs/mathlib.
- Right-click the superproject working tree, choose TortoiseGit → Submodule Add…. Enter the remote URL and the path
libs/mathlib. TortoiseGit runsgit submodule add, clones the repository, and stages both.gitmodulesand the new gitlink. - Open the TortoiseGit commit dialog for the superproject. Stage
.gitmodulesand thelibs/mathlibgitlink entry. Commit with a message that explains the dependency change. - Push the superproject. The
.gitmodulesfile and the recorded SHA travel together. - In a fresh clone, right-click the superproject and choose TortoiseGit → Submodule Update. TortoiseGit runs
git submodule update --initand checks out the exact SHA recorded in the parent commit intolibs/mathlib.
If the library itself contains submodules, choose the recursive option or run git submodule update --init --recursive from a Git shell inside C:\repos\app.
Why pointer changes belong in the parent commit
Because the parent repository tracks only the SHA, any commit you make inside the submodule is invisible to the superproject until you stage the gitlink change in the parent. If you update the submodule to a new branch tip and commit only inside the submodule, teammates who run Submodule Update will still land on the old SHA. The practical decision is to commit the submodule pointer change from the parent repository in the same TortoiseGit commit that changes dependent code. That keeps the superproject’s recorded state consistent with the code that consumes it.
Limits and performance
Recursive updates can be slow on large trees because TortoiseGit walks every nested submodule and contacts each remote. You may see credential prompts once per remote, not once per repository. Nested submodules require the recursive flag and a correct .gitmodules entry at every level; a missing entry in an intermediate repository will silently skip that level.
Dialog labels and default options differ across TortoiseGit 2.x releases. If a menu item is not visible, check the installed build’s help or run the equivalent Git command from a shell.
Common mistakes
- Deleting the folder is not enough. Removing the submodule directory leaves the gitlink and the
.gitmodulesentry behind. To remove a submodule properly, rungit rm <path>in the superproject, delete the corresponding section from.gitmodules, and commit both changes. - Assuming Update moves to the branch tip. Submodule Update checks out the recorded SHA. If you want the latest commit on a branch, you must fetch and checkout inside the submodule first, then stage the new pointer in the parent.
- Ignoring line-ending noise. Windows
core.autocrlfsettings can make a submodule appear modified even when the SHA matches. Rungit statusinside the submodule before committing the parent.
How to verify the result
After Submodule Update, open a Git shell (or TortoiseGit’s Git console) inside the superproject working tree and run:
git submodule status
Expected check: each submodule line shows the commit SHA that matches the superproject’s recorded gitlink. A leading + means the checked-out commit differs from the recorded SHA; a leading - means the submodule is not initialized.
To confirm the remote and branch tracking, run inside the submodule directory:
git config --list --show-origin
Verify that the remote.origin.url and any branch tracking settings match the .gitmodules entry.
To confirm that a pointer change is stageable, modify a file inside the submodule, commit inside the submodule, then open the TortoiseGit commit dialog on the parent. The submodule entry should appear as modified and be stageable alongside your dependent code changes.
Rollback considerations
Submodule Update changes the working tree by checking out a specific SHA. If the update introduces unwanted changes, discard them inside the submodule working tree with git checkout -- . or git reset --hard <recorded-sha> before re-running the parent update. Removing a submodule is a state-changing operation; if you accidentally stage a removal, unstage it in the parent commit dialog before committing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.