Git Submodules Setup Guide

Git Submodules Setup Guide

Setting up a clean main repo with submodules using temporary bare repositories stored inside .git/ — no sibling folder pollution, zero cleanup drama when real remote URLs arrive.

Target Structure

.
├── .git/
│   ├── local-submodules/         ← temporary bare repos (hidden, never tracked)
│   │   ├── Middlewares.git
│   │   ├── PowerAut0mater.git
│   │   └── Shared.git
├── .gitmodules
├── Middlewares/                  ← submodule
├── PowerAut0mater/               ← submodule
└── Shared/                       ← submodule

Why .git/local-submodules/? The .git/ folder is never tracked by Git. This means the bare repos live completely hidden from your working tree, no sibling folders are created, and when you switch to real remote URLs later there is nothing to clean up except one optional rm -rf.

Step 1 — Initialize the Main Repository

# Navigate into your project root
cd /projects

# Initialize a new Git repository and name the default branch "main"
# -b main → sets the initial branch name (instead of the old default "master")
git init -b main

Step 2 — Create Bare Repos Inside .git/

# Create the hidden folder that will hold all temporary bare repos
# -p → creates parent directories as needed; no error if folder already exists
mkdir -p .git/local-submodules

# Initialize a bare Git repository for each submodule
# --bare → creates a repo with NO working tree (just raw git data)
#          correct format for a repo that will only be pushed to / cloned from
# -b main → sets the default branch name to "main"
git init -b main --bare .git/local-submodules/Middlewares.git
git init -b main --bare .git/local-submodules/PowerAut0mater.git
git init -b main --bare .git/local-submodules/Shared.git

Bare repos need at least one commit before they can be used as submodule sources. Clone each into /tmp, make an empty commit, push back, then clean up:

# Clone the bare repo into /tmp so we have a working tree to commit from
# This creates a temporary normal (non-bare) clone at /tmp/Middlewares
git clone .git/local-submodules/Middlewares.git /tmp/Middlewares

cd /tmp/Middlewares

# Create an empty commit (no files needed) to give the bare repo a valid HEAD
# --allow-empty → normally Git refuses commits with no changes; this flag bypasses that
git commit --allow-empty -m "init"

# Push the empty commit back into the bare repo
# This is what makes the bare repo usable as a submodule source
git push

cd /projects

# Repeat the same process for PowerAut0mater
git clone .git/local-submodules/PowerAut0mater.git /tmp/PowerAut0mater
cd /tmp/PowerAut0mater && git commit --allow-empty -m "init" && git push && cd /projects

# Repeat the same process for Shared
git clone .git/local-submodules/Shared.git /tmp/Shared
cd /tmp/Shared && git commit --allow-empty -m "init" && git push && cd /projects

# Delete all three temporary clones — only needed them to seed the bare repos
# -r → recursive (required for directories)
# -f → force (skip confirmation prompts)
rm -rf /tmp/Middlewares /tmp/PowerAut0mater /tmp/Shared

Step 3 — Add Submodules to the Main Repo

# Register each bare repo as a submodule of the main repo
# git submodule add <url> <folder>
#   <url>    → path to the submodule's git repo (local path or remote URL)
#   <folder> → where it will appear in the working tree
#
# This command does three things automatically:
#   1. Clones the bare repo into the specified folder
#   2. Creates / updates the .gitmodules file with the path <-> url mapping
#   3. Stages both the .gitmodules change and the new submodule folder pointer
#
# ⚠️  IMPORTANT: the URL must explicitly start with ./
#     Git requires local paths to begin with ./ or ../
#     to distinguish them from remote hostnames
#
#     ❌ git submodule add .git/local-submodules/Middlewares.git Middlewares
#        fatal: repo URL: must be absolute or begin with ./|../
#
#     ✅ git submodule add ./.git/local-submodules/Middlewares.git Middlewares
#        Cloning into '...' done.
git submodule add ./.git/local-submodules/Middlewares.git Middlewares
git submodule add ./.git/local-submodules/PowerAut0mater.git PowerAut0mater
git submodule add ./.git/local-submodules/Shared.git Shared

Step 4 — Initial Commit

# Stage everything: .gitmodules + all three submodule folder pointers
git add .

# Commit — this snapshot records which exact commit each submodule is pinned to
git commit -m "Initial commit with submodules"

Step 5 — Verify

# Print the contents of .gitmodules to confirm all three submodules are registered
# .gitmodules is the config file Git uses to track submodule paths and their source URLs
cat .gitmodules

Expected output:

[submodule "Middlewares"]
    path = Middlewares
    url = ./.git/local-submodules/Middlewares.git

[submodule "PowerAut0mater"]
    path = PowerAut0mater
    url = ./.git/local-submodules/PowerAut0mater.git

[submodule "Shared"]
    path = Shared
    url = ./.git/local-submodules/Shared.git

Later — Switching to Real Remote URLs

When you have real remote URLs, run the following from inside the main repo:

Step A — Update the URLs

# Update the URL for each submodule inside .gitmodules
# This only edits the config file — it does NOT yet affect the live local clone
git submodule set-url Middlewares https://github.com/you/Middlewares.git
git submodule set-url PowerAut0mater https://github.com/you/PowerAut0mater.git
git submodule set-url Shared https://github.com/you/Shared.git

Step B — Sync and Update

# Propagate the URL changes from .gitmodules into .git/config
# Without this, Git still uses the OLD URLs internally even though .gitmodules was updated
#
# .gitmodules = "source of truth" (tracked file, shared with the whole team via commits)
# .git/config  = "active runtime config" (local only, NOT tracked by Git)
#
# sync reads .gitmodules and writes the new URLs into .git/config to make them active
git submodule sync

# Stage the updated .gitmodules so the URL change is recorded in history
git add .gitmodules

# Commit the URL change so teammates get the correct URLs when they pull
git commit -m "Update submodule remote URLs"

# Fetch and checkout the correct commit for each submodule from the new remote URLs
# --init      → initializes any submodule not yet set up locally (safe to re-run anytime)
# --recursive → also handles nested submodules (submodules inside submodules)
git submodule update --init --recursive

Step C — Optional Cleanup

# Remove the temporary bare repos — no longer referenced by anything
# All submodules now point to real remote URLs so this folder is dead weight
rm -rf .git/local-submodules

Your final structure is exactly:

.
├── .git
├── .gitmodules
├── Middlewares      (submodule → real remote)
├── PowerAut0mater   (submodule → real remote)
└── Shared           (submodule → real remote)

Quick Reference

StageCommandWhat it does
Init main repogit init -b mainCreates .git/, sets default branch to main
Create bare reposgit init -b main --bare .git/local-submodules/<n>.gitRepo with no working tree — push/clone only
Seed bare repoclone → empty commit → push → rm tempGives bare repo a valid HEAD so it can be used as a source
Add submodulegit submodule add ./<url> <folder>Clones + registers path and url in .gitmodules
Update to real URLgit submodule set-url <n> <url>Edits .gitmodules with the new remote URL
Sync URLsgit submodule syncCopies URLs from .gitmodules into .git/config (makes them active)
Pull from remotesgit submodule update --init --recursiveFetches and checks out correct commit from remote
Cleanup temp reposrm -rf .git/local-submodulesDeletes local bare repos, no longer needed