Lesson 2 of 5 in Power Tools

Submodules

Story: Some records belong to another bureau, Agent. We don't copy them into ours. We note exactly which edition we use.

Projects often use code that lives in another repository: a shared library, a theme, a set of styles that several websites use. You could copy its files into your project, but then every fix to them has to be copied again, by hand.

A submodule puts the other repository inside yours, in a folder of its own, pinned to one exact commit. Your repository records which commit to use. The other one keeps its own history.

Adding a submodule

git submodule add <url> <folder> clones the repository into the folder:

$ git submodule add https://git.example/team/lib.git lib
Cloning into '/home/learner/project/lib'...

Leave the folder out, and git names it after the repository, which is lib here too. Two things are now staged:

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

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   .gitmodules
	new file:   lib

.gitmodules is a small text file that says where the submodule lives and where to clone it from:

$ cat .gitmodules
[submodule "lib"]
	path = lib
	url = https://git.example/team/lib.git

Commit them both. The commit doesn't store lib's files. For lib, it stores one commit id: the commit that lib has checked out. That's the pin. Everyone who uses your commit gets the library at exactly that commit, even after the library moves on.

A repository inside a repository

lib is a complete repository of its own. cd lib, and git commands work on the library, not on your website:

$ git log --oneline
59574ed (HEAD -> main, origin/main, origin/HEAD) Add the button styles
9028b08 Add a README

cd .. takes you back to your website. In the Live Graph, a submodule gets its own band, indented under yours. Dotted lines join each of your commits that changes the pin to the commit of lib it pins.

Cloning a project with submodules

git clone brings .gitmodules and the pin, but not the library: in a fresh clone, lib is an empty folder. git submodule update --init fills it:

$ git submodule update --init
Submodule 'lib' (https://git.example/team/lib.git) registered for path 'lib'
Cloning into '/home/learner/copy/lib'...
Submodule path 'lib': checked out '59574ed84d354b03badb42c55725022b572475d6'

--init sets up each submodule the first time, and update checks it out at the pinned commit. Inside lib, HEAD is detached, because the pin is a commit, not a branch:

$ git status
HEAD detached at 59574ed
nothing to commit, working tree clean

That's normal for a submodule.

Moving the pin

When the library has a newer commit you want, get it inside lib, then commit the new pin in your website. Here lib is on its main branch, so git pull brings in a teammate's new commit:

$ git pull
From https://git.example/team/lib
   59574ed..24c54ad  main       -> origin/main
Updating 59574ed..24c54ad
Fast-forward
 buttons.css | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

On a detached HEAD, git pull has no branch to update. Run git fetch, then git switch --detach origin/main instead.

Back in your website, git status says that lib has a different commit checked out from the one you pinned:

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

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

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

git diff shows the pin as a single line, from the old commit to the new one:

$ git diff
diff --git a/lib b/lib
index 59574ed..24c54ad 160000
--- a/lib
+++ b/lib
@@ -1 +1 @@
-Subproject commit 59574ed84d354b03badb42c55725022b572475d6
+Subproject commit 24c54ad71057e98f9fa19c93815e812e2d92f6eb

git add lib stages the new pin, and git commit -a does it too. Then commit, and push, like any other change.

When the pin moves without you

modified: lib (new commits) can mean the opposite, too. When you pull a teammate's commit that moved the pin, or switch to a branch that pins another commit, git moves the pin but leaves lib where it was: neither git pull nor git switch checks out a submodule. (git switch lists lib with an M to warn you.) Now lib is behind:

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

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

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

Don't git add lib here: that would pin the old commit again and undo your teammate's change. Run git submodule update --init, which checks lib out at the pinned commit:

$ git submodule update --init
Submodule path 'lib': checked out '24c54ad71057e98f9fa19c93815e812e2d92f6eb'

So before you commit a new pin, ask which way it went: did you move lib on purpose, or did the pin move without you?

Changes inside a submodule

If you edit a file inside lib, your website's git status says so:

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

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
  (commit or discard the untracked or modified content in submodules)
	modified:   lib (modified content)

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

New files show as (untracked content). Your website can't commit them: git add . and git commit -a only ever record which commit lib has checked out. Either commit the change inside lib, push it to the library's repository and then commit the new pin, or throw it away with git restore inside lib. git submodule update won't overwrite them: if it needs to change a file you've edited, it stops and changes nothing.

On your own computer

git clone --recurse-submodules clones a project and its submodules in one go. git submodule update --remote moves each submodule to the newest commit of its branch for you. git submodule status lists the submodules and their pins. This Terminal has submodule add and submodule update, with --init.

Command What it does
git submodule add <url> <folder> Clones a repository into the folder and stages it as a submodule, with .gitmodules. Commit it to pin it.
git submodule update --init Clones any submodule that isn't there yet, and checks each one out at the commit your current commit pins.
git add <folder> Stages a new pin: the commit the submodule has checked out now.

New commands

  • git submodule add
  • git submodule update --init

Explainer: A repository inside a repository

copy: your repo, you are herelib: submodule at lib in copyproject: your repolib: submodule at lib in project9ba615c Add the home page9ba615cbe8400a Add a stylesheetbe8400ae0a78d1 Add the lib submodulee0a78d1HEAD → mainorigin/HEADorigin/main9028b08 Add a README9028b0859574ed Add the button styles59574edHEADmainorigin/HEADorigin/main9ba615c Add the home page9ba615cbe8400a Add a stylesheetbe8400ae0a78d1 Add the lib submodulee0a78d1HEAD → mainorigin/main9028b08 Add a README9028b0859574ed Add the button styles59574edHEAD → mainorigin/HEADorigin/main
The repository at the end of the Explainer.
Graph as text

copy: your repo, you are here

  • HEAD → main at e0a78d1 "Add the lib submodule"

Branches and tags

  • main at e0a78d1 "Add the lib submodule", tracks origin/main
  • origin/HEAD at e0a78d1 "Add the lib submodule"
  • origin/main at e0a78d1 "Add the lib submodule"

Commits, newest first

  1. e0a78d1 "Add the lib submodule"; parent be8400a; refs: HEAD → main, origin/HEAD, origin/main
  2. be8400a "Add a stylesheet"; parent 9ba615c
  3. 9ba615c "Add the home page"; no parent

lib: submodule at lib in copy

  • HEAD detached at 59574ed "Add the button styles"

Branches and tags

  • main at 59574ed "Add the button styles", tracks origin/main
  • origin/HEAD at 59574ed "Add the button styles"
  • origin/main at 59574ed "Add the button styles"

Commits, newest first

  1. 59574ed "Add the button styles"; parent 9028b08; refs: HEAD, main, origin/HEAD, origin/main
  2. 9028b08 "Add a README"; no parent

project: your repo

  • HEAD → main at e0a78d1 "Add the lib submodule"

Branches and tags

  • main at e0a78d1 "Add the lib submodule", tracks origin/main
  • origin/main at e0a78d1 "Add the lib submodule"

Commits, newest first

  1. e0a78d1 "Add the lib submodule"; parent be8400a; refs: HEAD → main, origin/main
  2. be8400a "Add a stylesheet"; parent 9ba615c
  3. 9ba615c "Add the home page"; no parent

lib: submodule at lib in project

  • HEAD → main at 59574ed "Add the button styles"

Branches and tags

  • main at 59574ed "Add the button styles", tracks origin/main
  • origin/HEAD at 59574ed "Add the button styles"
  • origin/main at 59574ed "Add the button styles"

Commits, newest first

  1. 59574ed "Add the button styles"; parent 9028b08; refs: HEAD → main, origin/HEAD, origin/main
  2. 9028b08 "Add a README"; no parent

Not shown: team/lib.git, origin, teammate.

Challenge 1 of 4

Story: Borrow the bureau's button kit, Agent, and note exactly which edition you took.

Your team keeps its button styles in a repository of its own, at https://git.example/team/lib.git. Add it to your website as a submodule, in a folder called lib, and commit it.

Unlocks after Rewriting History.

Terminal

Unlocks after Rewriting History.

This Lesson can be played once Rewriting History is complete. Its text and Explainer are open to everyone.