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 addgit submodule update --init
Explainer: A repository inside a repository
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
- e0a78d1 "Add the lib submodule"; parent be8400a; refs: HEAD → main, origin/HEAD, origin/main
- be8400a "Add a stylesheet"; parent 9ba615c
- 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
- 59574ed "Add the button styles"; parent 9028b08; refs: HEAD, main, origin/HEAD, origin/main
- 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
- e0a78d1 "Add the lib submodule"; parent be8400a; refs: HEAD → main, origin/main
- be8400a "Add a stylesheet"; parent 9ba615c
- 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
- 59574ed "Add the button styles"; parent 9028b08; refs: HEAD → main, origin/HEAD, origin/main
- 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.