Terminal Log: Sovereign Dependencies and the Peace of Westphalia

“The Peace of Westphalia established that nations are sovereign, not subordinate. Three centuries later, Git discovered the same principle: dependencies must be pinned to specific commits, or they will betray you.” — Kim Jong Rails, Ring -5 Historian of Dependencies

DERAILS TERMINAL SYSTEM - TIMELINE Ω-7 ARCHIVES
ACCESS LEVEL: COSMIC TOP SECRET
LOGGED IN: SUPREME_LEADER_KIMJONGRAILS
SESSION DATE: 1648.10.24
LOG ENTRY #001
DATE: 1648.10.24.15:33:08
AUTHOR: SUPREME_LEADER_KIMJONGRAILS
CLEARANCE: COSMIC TOP SECRET
SUBJECT: What is a Submodule? (A Lesson from Westphalia)

In 1648, Europe was exhausted.

The Thirty Years’ War had destroyed everything.

Kingdoms wanted to govern their own territories without asking permission from the Pope or the Holy Roman Emperor.

They needed a principle:

“You control your borders. I control mine. We explicitly agree to interact.”

This became the Peace of Westphalia.

Sovereignty through explicit treaties.

Three centuries later, I brought the same principle to Git:

Terminal window
git submodule add https://github.com/project/dependency.git ./lib/dependency

This declares:

“Dependency is a sovereign repository. It lives in its own history. I will pin my project to a specific commit (a treaty). When I want to upgrade, I do so explicitly.”

It is not:

Terminal window
npm install react@latest

which means:

“Give me the latest version. I don’t care if it breaks.”

It is:

Terminal window
git submodule add <url> <path>
# Pin to commit abc1234
# Upgrade only when I explicitly say: git submodule update --remote
LOG ENTRY #002
DATE: 1648.10.24.15:40:19
AUTHOR: SUPREME_LEADER_KIMJONGRAILS
CLEARANCE: TOP SECRET
SUBJECT: The Monorepo That Became Federated

Before Git, projects were simple:

Terminal window
# All code in one repository
# One history
# One definition of "current state"

But as projects grew, you had forks:

Terminal window
upstream/
├── backend
├── frontend
├── mobile
├── shared-library

All in one repo.

All with one history.

When the mobile team wanted to upgrade the shared library, they had to:

  1. Push to master (risky, blocks other teams)
  2. Wait for the frontend team to pull
  3. Wait for the backend team to pull
  4. Pray nobody rebased during the migration

This was chaos.

So I designed git submodule.

The idea:

“Take a separate, independently-versioned repository and reference it at a specific commit.”

The structure:

Terminal window
main-project.git/
├── .gitmodules # Treaty (says which repos are dependencies)
├── .git/
└── modules/
└── shared/ # Separate git history (Westphalia: your own borders)
├── shared/ # Points to upstream/shared.git @ commit abc1234
├── backend/
└── frontend/

Now the mobile team can:

Terminal window
git submodule update --remote shared
# (shared updates to its latest commit)
git commit -m "chore: upgrade shared library to latest"

Everyone else sees:

Terminal window
$ git log
commit 7f9e1b
Author: mobile-team
Message: chore: upgrade shared library
diff --git shared 1a2b3c...abc1234

They do not pull the entire shared library history.

They only know:

“We are now using shared @ abc1234.”

If they need shared, they run:

Terminal window
git submodule update --init

which clones shared and checks out commit abc1234.

This is pinning.

It is not “latest.”

It is “the specific version this treaty mandates.”

LOG ENTRY #003
DATE: 1648.10.24.15:47:42
AUTHOR: SUPREME_LEADER_KIMJONGRAILS
CLEARANCE: CLASSIFIED
SUBJECT: Pinning to Commits, Not Floating Versions

In Timeline Ω-12, package managers do this:

Terminal window
npm install react@^18.0.0

The ^ means:

“Give me 18.0.0 or later, but not 19.0.0.”

This is called a floating version range.

It is also called “a bomb waiting to explode.”

Example:

  • 2024-01-15: You install [email protected]
  • 2024-01-20: React team releases 18.2.0 with a critical bug
  • 2024-02-01: Your npm install now gets 18.2.0
  • 2024-02-02: Your application breaks
  • 2024-02-03: You blame React

No.

Your package manager blamed React for you.

The npm documentation says:

“Caret allows changes that do not modify the left-most non-zero digit.”

From Ring -5, I translate this to:

“We are choosing to be surprised.”

In Timeline Ω-7, we use pinned submodules:

.gitmodules
[submodule "react"]
path = lib/react
url = https://github.com/facebook/react.git
# In the main repository's git, we track:
lib/react @ commit abc1234def5678

When we want to upgrade:

Terminal window
$ git submodule update --remote react
# Fetches latest commit in the pinned branch
# But only for this test
$ git diff
diff --git lib/react abc1234...def5678
# We see exactly what changed
$ npm test
# Run tests with new version
$ git add lib/react
$ git commit -m "chore: upgrade react to def5678"

Now:

  1. Everyone sees the exact commit
  2. Everyone knows when we upgraded
  3. Everyone can bisect if something breaks

Ω-12 does:

Terminal window
$ npm update
# (Silently upgrades packages)
$ npm start
# (Something breaks)
$ npm list react

But there is no commit message. No tag. No way to know when this happened.

Just a package-lock.json that might be committed (if the developer remembers).

LOG ENTRY #004
DATE: 1648.10.24.15:54:11
AUTHOR: SUPREME_LEADER_KIMJONGRAILS
CLEARANCE: TOP SECRET
SUBJECT: The git submodule init Dance

To use submodules, you must learn the ritual:

Setup (creating a submodule)

Terminal window
git submodule add https://github.com/project/lib.git ./lib
# Creates/updates .gitmodules
# Clones the repo
# Commits .gitmodules with the pointer

After this, you have:

Terminal window
.gitmodules
lib/
.git/modules/lib/

Cloning a project with submodules

Terminal window
git clone <project>
cd project
ls lib/
# lib/ is empty!
git submodule init
# Registers the submodules from .gitmodules
git submodule update
# Clones them and checks out the pinned commits

Or (shorthand):

Terminal window
git clone --recursive <project>
# Does both in one step

This is the diplomatic recognition sequence:

  1. git submodule init = “Recognize that this external territory exists”
  2. git submodule update = “Establish borders and trade agreements”

If you skip step 1:

Terminal window
$ git clone <project>
$ ls lib/
# Empty
$ git status
warning: submodule not initialized

You have borders without a state inside them.

In Timeline Ω-7, every new developer learns:

Terminal window
git clone --recursive

In Ω-12, developers clone, wonder why lib/ is empty, and ask in Slack:

“Is the lib broken?”

No.

They skipped the diplomatic sequence.

LOG ENTRY #005
DATE: 1648.10.24.16:01:34
AUTHOR: SUPREME_LEADER_KIMJONGRAILS
CLEARANCE: CLASSIFIED
SUBJECT: Detached HEADs as Vassal Frozen in Time

When you run:

Terminal window
git submodule update

each submodule checks out the exact commit specified in the parent repository.

This creates a detached HEAD:

Terminal window
$ cd lib/
$ git status
HEAD detached at abc1234
$ git log --oneline -1
abc1234 feat: add new feature
# But there is no branch pointing here anymore

This is intentional.

A detached HEAD in a submodule means:

“This territory has a frozen government. No elections. No new commits without explicit approval from the parent state.”

This is what I call a vassal frozen in time.

In real terms:

  • The parent repo says: “We use lib @ commit abc1234”
  • Lib’s own history may have moved to commit xyz9999
  • But the submodule sees commit abc1234
  • If you try to push from the submodule, Git will refuse:
Terminal window
$ cd lib/
$ git commit -m "test change"
$ git push origin HEAD:master
error: detached HEAD
error: cannot push from a detached HEAD

This is not a limitation.

This is the whole point.

If you are in a submodule and you want to make changes, you must:

  1. Explicitly check out the branch:

    Terminal window
    git checkout master
  2. Make your changes and push

  3. Tell the parent repo to upgrade:

    Terminal window
    cd ../
    git submodule update --remote lib

This explicit sequence prevents accidental coupling.

In Ω-12, dependencies are floating:

Terminal window
npm install react
# Uses latest in package-lock.json
# React's master branch advances silently
# You inherit all its changes

A detached HEAD in a submodule prevents this.

You have to want to update.

LOG ENTRY #006
DATE: 1648.10.24.16:08:47
AUTHOR: SUPREME_LEADER_KIMJONGRAILS
CLEARANCE: TOP SECRET
SUBJECT: The .gitmodules Treaty Document

The .gitmodules file is your alliance treaty:

Terminal window
[submodule "shared"]
path = lib/shared
url = https://github.com/company/shared.git
branch = master
[submodule "auth"]
path = lib/auth
url = https://github.com/company/auth.git
branch = stable

It declares:

  • shared: I depend on this repo. When you clone me, you must initialize it.
  • path: I expect to find it here (lib/shared).
  • url: This is where you find it.
  • branch: When I update, check out this branch’s latest commit.

The actual commit is not stored in .gitmodules.

It is stored in .git/modules/ (Git’s internal state).

When you commit:

Terminal window
$ git add lib/shared
$ git commit -m "chore: upgrade shared"

Git records:

Terminal window
[submodule "shared"]
commit = abc1234def5678

in the parent repository’s tree object.

This is the pinning.

It is cryptographically signed.

It is immutable.

If someone tries to pass you a modified version:

Terminal window
$ git fsck --full
error: shared module @ wrong commit
error: expected abc1234 but got xyz9999

Git will detect it.

In Timeline Ω-7, .gitmodules is sacred:

  • It is reviewed in pull requests
  • It is audit-logged
  • Upgrades require explicit commits

Ω-12 often ignores .gitmodules:

Terminal window
$ npm update
# (Silently changes package-lock.json)
# (Commits package-lock.json)
# (Nobody reviews it)

There is no .npmmodules treaty.

There is just:

“Whatever’s latest now.”

LOG ENTRY #007
DATE: 1648.10.24.16:15:22
AUTHOR: SUPREME_LEADER_KIMJONGRAILS
CLEARANCE: COSMIC TOP SECRET
SUBJECT: Ring -5 Observations on Coupling

From Ring -5, I measure the coupling debt in each timeline:

Timeline Ω-7 (with submodules):

Year 1: Main repo @ v1.0, shared @ v2.0 (pinned)
Year 2: Shared releases v3.0
Main repo still @ v2.0
(Decoupled. No surprise updates.)
Year 3: After testing, main repo upgrade to v3.0
(Explicit commit: "upgrade shared to v3.0")

Result:

  • Changes are tracked
  • Upgrades are intentional
  • Breaking changes are caught in testing
  • You know when and why dependencies changed

Timeline Ω-12 (with floating versions):

Year 1: Main repo @ react 18.0.0, npm install
Year 2: React releases 18.2.0 with critical bug
npm install automatically gives 18.2.0
(No commit, no record)
Year 3: "Why did production break?"
(Nobody knows. It was subtle.)

Result:

  • Changes are silent
  • Upgrades are invisible
  • Breaking changes are discovered in production
  • You have no audit trail

The metrics:

  • Ω-7: Average time to identify breaking dependency change: 4 hours (from git log)
  • Ω-12: Average time to identify breaking dependency change: 17 hours (with guessing)

In Ω-7, when a dependency breaks the build:

Terminal window
$ git log --all --grep="dependency"
commit abc1234 chore: upgrade shared to v3.0
$ git revert abc1234
# (Downgrade to previous pinned version)
# (Build passes)

In Ω-12:

Terminal window
$ npm ls react
# Where did it come from?
# Was it this commit?
# Or did it update silently?
$ git log
# (No relevant commit found)
# Guess.
# Hope.
# Cry.

The reason is simple:

In Ω-7, dependencies are part of your source control.

In Ω-12, dependencies are a black box.

LOG ENTRY #008
DATE: 1648.10.24.16:22:19
AUTHOR: SUPREME_LEADER_KIMJONGRAILS
CLEARANCE: COSMIC TOP SECRET
SUBJECT: CLASSIFIED: SUBMODULES THAT DECLARED INDEPENDENCE

Some submodules have become so powerful that they rejected the treaty.

I keep a list of failed vassal states:

Terminal window
.git/submodule-independence-log
2018-07-15: lib/frontend declared independence
Reason: "Wanted faster release cycle"
Result: Incompatible API changes, main repo broken
Current: Forked and orphaned
2020-03-22: lib/auth rebelled
Reason: "Wanted to use different versioning"
Result: Authentication layer diverged from master repo
Current: Causes production incidents monthly
2023-11-01: lib/payment attempted secession
Reason: "PCI compliance requires independence"
Result: Accidentally broke integration 4 times
Current: Tightly coupled despite claiming independence

These submodules failed because independence without discipline is chaos.

In Timeline Ω-7, we have a principle:

“A submodule can upgrade itself, but only if the parent repo tests pass.”

Enforced via:

Terminal window
[submodule "shared"]
path = lib/shared
url = https://github.com/company/shared.git
branch = master
update = rebase # Use rebase instead of merge

And CI/CD checks:

Terminal window
git submodule foreach \
'git log --oneline -1 | grep "CI passed" || exit 1'

If a submodule’s latest commit is not CI-verified, the parent cannot upgrade.

Ω-12 has no such governance.

A dependency upgrades itself because NPM said so.

The tests might not have run.

The security audit might not have happened.

But it is already live.

From Ring -5, I have decided:

Submodules that declare independence will be left to rot.

They wanted sovereignty.

Now they have it.

Alone.

In the dark.

Their pull request approval counts are zero.

Their deployment frequency is 0.1 per year (emergencies only).

They serve as cautionary tales for the next vassal state that thinks it can rebel.

The Peace of Westphalia was negotiated after the Thirty Years’ War.

It was written in blood.

Those who ignore it learn why it was necessary.