Home / Alt manpages / git-submodule(1)

  • git-submodule(1)
  • User command
  • linux

Clone, Update and Safely Inspect Git Submodules

You will finish with a repeatable workflow for adding, checking out, updating and inspecting Git submodules without confusing the superproject's recorded commit with the submodule's current branch. The examples match Git 2.43.0 from the installed git and git-man packages.

Allow about fifteen minutes. You need Git, a repository you can modify, and a submodule URL that you are authorised to read. These commands normally run as your ordinary user. Do not use sudo: submodule state belongs to the repository owner, not to root.

1. Check the installed command

Start in the top-level directory of the superproject, the repository that records the submodule commit:

$ git --version
git version 2.43.0
$ git submodule -h
usage: git submodule [--quiet] [--cached]
...

The help output is deliberately abbreviated here. It confirms the command family and shows the available operations, including add, status, init, deinit, update, set-url, foreach and sync.

Checkpoint: confirm that you are in the intended superproject before changing anything:

$ git rev-parse --show-toplevel
/path/to/superproject
$ git status --short

2. Inspect existing submodules

With no subcommand, Git shows the status of existing submodules. The explicit form is easier to remember in scripts:

$ git submodule status
 7c1e2ab3f4... libs/example (v1.4.0)

Your object name and description will differ. A leading - means the submodule is not initialised. A leading + means its checked-out commit differs from the commit recorded in the superproject's index. A leading U indicates a merge conflict.

Use the index as the comparison point when you need to see exactly what the next superproject commit records:

$ git submodule status --cached
$ git submodule status --recursive

--recursive includes nested submodules. This is a read-only check. If the result contains +, do not immediately run update: first decide whether the local submodule work is valuable and whether the superproject should record it.

3. Add a submodule deliberately

Adding a submodule changes the worktree and stages a gitlink plus .gitmodules. Review the target path and URL before running this ordinary, non-privileged command:

$ git submodule add https://example.org/team/library.git vendor/library
Cloning into '/path/to/superproject/vendor/library'...

The URL is written to the top-level .gitmodules file, while local clone details are kept in .git/config. The path becomes the logical submodule name unless you provide --name. If the repositories are kept together, a relative URL can be useful, but remember that a sibling repository is written as ../library.git, not ./library.git.

Inspect what will be committed:

$ git diff -- .gitmodules
$ git status --short
A  .gitmodules
A  vendor/library

Do not commit until the URL, path and checked-out commit are correct. Undo an accidental add before committing with git submodule deinit -f -- vendor/library followed by git rm --cached vendor/library, then remove the unwanted entry from .gitmodules and review the diff. The -f can remove the submodule worktree, so preserve any local work first.

4. Initialise and update a clone

After cloning a superproject, its .gitmodules file describes submodule paths and URLs, but the local repository may not yet have clone URLs in .git/config. The shortest normal setup is:

$ git submodule update --init --recursive
Submodule 'vendor/library' (https://example.org/team/library.git) registered for path 'vendor/library'
Submodule path 'vendor/library': checked out '7c1e2ab3f4...'

--init copies the usable URL and supported update settings from .gitmodules into local configuration, then update clones missing submodules, fetches missing commits and checks out the commit recorded by the superproject. --recursive applies the same process to nested submodules.

If you need to customise a local URL, split the operation:

$ git submodule init vendor/library
$ git config submodule.vendor/library.url ssh://[email protected]/team/library.git
$ git submodule update vendor/library

A custom command in submodule.<name>.update is not copied from .gitmodules during initialisation. That is a security boundary: do not treat a repository's configuration as permission to execute arbitrary local commands.

Checkpoint: verify the checkout against the superproject:

$ git submodule status --recursive
 7c1e2ab3f4... vendor/library (v1.4.0)

5. Understand the detached HEAD

Normal git submodule update uses checkout mode and places the submodule on the recorded commit, usually with a detached HEAD. That is expected: the superproject records a commit, not a moving branch name.

$ git -C vendor/library status --short --branch
## HEAD (no branch)
$ git -C vendor/library rev-parse HEAD
7c1e2ab3f4...

To develop inside the submodule, create or switch to a local branch before editing:

$ git -C vendor/library switch --create feature/library-change
$ git -C vendor/library status --short --branch
## feature/library-change

After committing in the submodule, return to the superproject and review the changed gitlink. The superproject does not contain the submodule's files as ordinary tracked files:

$ git status --short
 M vendor/library
$ git diff --submodule=log -- vendor/library

Commit the submodule repository first, push it somewhere the other users can fetch, then commit the new gitlink in the superproject. Otherwise another clone may be unable to obtain the recorded object.

6. Choose between recorded and remote updates

Use the default update when you want the submodule to match the superproject's recorded commit:

$ git submodule update --recursive

This may discard local changes only when you explicitly add --force, but checkout can still fail if local work would be overwritten. Stop and inspect the submodule rather than forcing through the error.

Use --remote when the purpose is to follow the submodule's configured remote-tracking branch instead:

$ git submodule update --remote --merge vendor/library
$ git status --short
 M vendor/library

This fetches the submodule remote before selecting its target. The branch comes from submodule.<name>.branch in .gitmodules or .git/config, with local configuration taking precedence; without it, Git uses the remote HEAD. Add --no-fetch only when you knowingly accept possibly stale remote-tracking data.

--merge and --rebase keep the submodule on a branch, but can create conflicts inside that repository. Resolve or abort the merge or rebase there. Do not use --force as a shortcut for conflict recovery.

7. Synchronise a changed URL

When an upstream maintainer changes the URL in .gitmodules, update already-initialised local submodules with:

$ git submodule sync --recursive
Synchronizing submodule url for 'vendor/library'
$ git config --get submodule.vendor/library.url
ssh://[email protected]/team/library.git

sync only affects submodules that already have a URL entry in local configuration. It does not initialise a missing checkout and it does not change the URL in .gitmodules. Review that file separately before committing a URL change.

8. Remove only a local checkout

Warning

Deinitialisation removes the submodule's working tree. It unregisters the local configuration but does not remove the submodule from the superproject's history:

$ git submodule deinit -- vendor/library
Cleared directory 'vendor/library'

Use an explicit path. Without a path, the command refuses to deinitialise everything; --all opts into that broader action. If local modifications exist, stop and copy or commit them. deinit --force removes that worktree despite local changes and is destructive.

To restore a deliberately deinitialised checkout, run:

$ git submodule update --init -- vendor/library

To remove a submodule from the project itself, use the repository's normal git rm review and commit workflow, not just deinit. Check the resulting .gitmodules diff and any nested submodule references before committing.

Done means

  • You confirmed the installed Git version and the superproject root.
  • git submodule status --recursive shows the intended commits without unexplained -, + or U prefixes.
  • New submodules have reviewed paths, URLs and .gitmodules changes.
  • You know that ordinary update checkout normally leaves a detached HEAD.
  • You used --remote only when following a configured upstream branch was intended.
  • You treated deinit --force as destructive and kept a recovery path.