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 optionalrm -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
| Stage | Command | What it does |
|---|---|---|
| Init main repo | git init -b main | Creates .git/, sets default branch to main |
| Create bare repos | git init -b main --bare .git/local-submodules/<n>.git | Repo with no working tree — push/clone only |
| Seed bare repo | clone → empty commit → push → rm temp | Gives bare repo a valid HEAD so it can be used as a source |
| Add submodule | git submodule add ./<url> <folder> | Clones + registers path and url in .gitmodules |
| Update to real URL | git submodule set-url <n> <url> | Edits .gitmodules with the new remote URL |
| Sync URLs | git submodule sync | Copies URLs from .gitmodules into .git/config (makes them active) |
| Pull from remotes | git submodule update --init --recursive | Fetches and checks out correct commit from remote |
| Cleanup temp repos | rm -rf .git/local-submodules | Deletes local bare repos, no longer needed |