submodules

Git submodules provide a way to consider any repository as a versioned "package" that can be included in any other git repo.

clone a repo that has submodules

    
        git clone --recurse-submodules -j8 git://github.com/foo/bar.git
    

Note: -j8 is an optional performance optimization that became available in version 2.8, and fetches up to 8 submodules at a time in parallel — see man git-clone

clone in repositories after cloning a repo

When you clone a repository git will not automatically clone in the contents of any submodules, but it will clone an empty directory with the name of that repository, in order to actually get the contents of those submodules you have to do the following

    
        git submodule init
        git submodule update
    
alternatively if you know that it recursively contains more submodules
    
        git submodule update --init --recursive
    

git pulling in submodules

Suppose you're in a submodule that contains more submodules, currently git status tells you that there are no changes at all, now you run git pull in that submodule and there are updates, namely the updates update the references to a few of the submodules, this can occur when someone has updated the versions of the submodules elsewhere and you are just getting those changes.

What usually appears is something like this:

    
$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
        modified:   build_notifier (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

    

This may seem confusing, but just know that even though it says that there are changes, the changes are not from you editing files, its just that when you git pulled it changed the reference to the git submodule to the new version, and since your local version still contains the old copy, there is a diff being generated. To fix this run:

    
git submodule update
    

Don't read these command as magic incantations to solve all your problems, for further understanding run man git submodule

going deep

By default most of the git submodule commands only operate one layer deep, so for example if you're in a git directory which has submodules which themselves contain submodules, then running git submodule update won't updated the nested submodules, so in that case run this:

    
git submodule update --init --recursive
    
To do this on each submodule in your project run
    
git submodule foreach --recursive 'git submodule update --init --recursive'
    

mistakes

ssh submodules, github organizations and collaborators

When adding submodules from an organization using ssh links, then there comes a problem that people who are not part of the organization will not be able to clone in submodules as they do not have access for ssh as only collaborators can do this (even with public repositories). One fix to this is to add people as collaborators to the organization, but eventually adding everyone to an organization just so they can clone in the submodules becomes a little unwieldy. Here's a way we can fix this:

HEAD detached from 54b9bf8

If you see that you are on a detached head, this means that you cannot commit any changes here, if you have uncommitted changes you can switch to main with git checkout main, if you do have committed changes then you have to do this:

    
git switch -c temp-work
git switch main
git merge temp-work
git branch -d temp-work
    

To avoid this problem in the future, we have to realize why this usually occurs, it happens when you clone a repository and initialize its submodules such as by doing git clone --recursive URL, it makes sense that git puts every submodule at its respective commit, this is so that you can have reproducible behavior when you clone in submodules, but sometimes you know what you want the most up-to-date version of a submodule, in that case run this after the fact:

    
        git submodule foreach --recursive git checkout main
    

copying a directory with submodules

In a Git repository, I have a subdirectory (e.g., client/) that contains a mix of regular files and nested submodules. I want to duplicate this entire directory to a new location within the same repository (e.g., single_player/). However, simply copying the directory with cp doesn't properly register the submodules in the new location — Git doesn't update .gitmodules or .git/modules, and the new submodule paths aren't tracked. How can I correctly duplicate the directory and ensure that all nested submodules are properly re-added and recognized by Git in their new location?

    
import os
import shutil
import subprocess
from pathlib import Path
import configparser

def get_repo_root():
    result = subprocess.run(['git', 'rev-parse', '--show-toplevel'],
                            stdout=subprocess.PIPE, text=True, check=True)
    return Path(result.stdout.strip())

def parse_gitmodules(repo_root):
    config = configparser.ConfigParser()
    gitmodules_path = repo_root / '.gitmodules'
    if not gitmodules_path.exists():
        return {}

    config.read(gitmodules_path)
    submodules = {}

    for section in config.sections():
        if not section.startswith("submodule"):
            continue
        path = config[section].get("path")
        url = config[section].get("url")
        if path and url:
            submodules[path] = url
    return submodules

def copy_directory_with_submodules(src, dst):
    repo_root = get_repo_root()
    submodules = parse_gitmodules(repo_root)

    src = Path(src).resolve()
    dst = Path(dst).resolve()

    submodules_to_add = []

    for root, dirs, files in os.walk(src):
        rel_root = Path(root).relative_to(src)
        dst_root = dst / rel_root

        # Check if any submodules exist at this level
        to_remove = []
        for d in dirs:
            full_path = (Path(root) / d).resolve()
            rel_path = full_path.relative_to(repo_root).as_posix()
            if rel_path in submodules:
                dst_submodule_path = (dst / rel_root / d).relative_to(repo_root)
                submodules_to_add.append((dst_submodule_path.as_posix(), submodules[rel_path]))
                to_remove.append(d)

        # Prevent os.walk from descending into submodules
        for d in to_remove:
            dirs.remove(d)

        # Copy files and dirs
        os.makedirs(dst_root, exist_ok=True)
        for f in files:
            src_file = Path(root) / f
            dst_file = dst_root / f
            shutil.copy2(src_file, dst_file)

    # Re-add submodules
    for rel_path, url in submodules_to_add:
        print(f"Adding submodule: {url} -> {rel_path}")
        subprocess.run(['git', 'submodule', 'add', url, rel_path], check=True)

    print("✅ Done.")

if __name__ == "__main__":
    import sys
    if len(sys.argv) != 3:
        print("Usage: python copy_directory_with_submodules.py  ")
        sys.exit(1)

    copy_directory_with_submodules(sys.argv[1], sys.argv[2])

    

edit this page