Context
I’m re-working my hugo site and theme. The content and presentation logic are theoretically separated between my blog repo and theme repo. I have the theme added into the blog repo as a git submodule which under normal (non-major refactoring) circumstances works well enough.
Since I’m actively refactoring, that means quickly iterating locally with changes to both the content and presentation logic.
So In practice that means that I am modifying files both in the main blog repo and in the theme submodule directly.
Problem 1
I can’t commit the files that I’m modifying directly in the theme submodule. And doing the inner-loop-expanding and error prone process of “work on a batch of changes, copy the changed files to the theme repo, commit them to a working branch and push to remote, then pull in updated submodule reference in the main repo” isn’t going to fucking happen.
So my files contain a huge blob of stacked changes and those changes aren’t grouped logically at all. Which means if I get interrupted at all (hello life!) and lose my train of thought I can’t just run a git diff
or git status
to pick up where I left off. Or if I find that what I’m doing isn’t working and need to revert to a known working state, welp that’s gonna be tough.
It feels like it’s 2003 again with adding suffixes to backup copies of files like -orig
and -1
and .backup.new.1.timscopy
(not like I would ever do such a thing).
I want to iterate and easily modify my forked theme directly adjacent to my blog content, but also make it simple to pull in theme updates from upstream as patches.
Solution 1
TLDR;
The magic of git subtrees.
Pulling from theme repo (note this must be run from the top dir of the repo):
cd ~/Documents/personal_repos/kaffeinlabs-hugo
git fetch --all
git subtree pull --prefix kaffeinlabs/themes/archie-fork archie-fork master
Pushing to theme repo:
git subtree push --prefix kaffeinlabs/themes/archie-fork archie-fork master --squash
ANTLILS; (Actually Not Too Long I Like Stories)
Soo….since my theme is a private, modified fork I might as well ditch the pain that is git submodules and merge this theme content into my main site repo. But I want to retain the git history attached to this theme for investigative purposes.
Remember, I also happen to have a bunch of in-flight work right now that I don’t want to lose!
Copy over the modified theme files into the theme repo dir one last time.
cd ~/Documents/personal_repos/kaffeinlabs-hugo cp -av ./* ~/Documents/personal_repos/archie git add . git commit -m "Lots of updates here. Useful message, aint it?"
Create a new completely private archie-fork theme repo
cd ~/Documents/personal_repos/archie gh repo create archie-fork --private --source=. --remote=upstream git push upstream
Add the private archie-fork repo as a remote in my kaffeinlabs repo
cd ~/Documents/personal_repos/kaffeinlabs-hugo git remote add -f archie-fork git@lowkey.github.com:lowkeyliesmyth/archie-fork.git
Remove git submodules and content from kaffeinlabs repo
cd ~/Documents/personal_repos/kaffeinlabs-hugo rm .gitmodules rm -rf kaffeinlabs/themes/* git add . git commit -m "Clear out submodules"
Remove git submodule references from repo config
vim ~/Documents/personal_repos/kaffeinlabs-hugo/.git/config < yeet those submodule block references>
Add the theme remote as a subtree. This will pull in the archie-fork theme’s git history into the kaffeinlabs blog repo, and the content stored into the archie-fork folder.
git subtree add --prefix kaffeinlabs/themes/archie-fork archie-fork master
So now I can manage and commit changes to the archie-fork theme directly in the kaffeinlabs blog repo. But pushing and pulling theme updates now requires using a different set of git commands.
Pulling from theme repo (note this must be run from the top dir of the repo):
cd ~/Documents/personal_repos/kaffeinlabs-hugo
git fetch --all
git subtree pull --prefix kaffeinlabs/themes/archie-fork archie-fork master
Pushing to theme repo:
git subtree push --prefix kaffeinlabs/themes/archie-fork archie-fork master
The first time I set this up I actually made a mistake initially by adding the subtree to the kaffeinlabs/themes/archie
directory at first. Easy enough to fix with a git mv kaffeinlabs/themes/archie kaffeinlabs/themes/archie-fork
and a hugo config.yaml update to point to the new archie-fork
dir.
No big deal, right?
WRONG!
It turns out that this broke my ability to push any changes in the subtree to the upstream archie-fork theme repo:
at 20:50:07 ❯ git subtree push --prefix kaffeinlabs/themes/archie-fork archie-fork master
git push using: archie-fork master
To github.com:lowkeyliesmyth/archie-fork.git
! [rejected] ff5b3ce81696b0c04a56641553f4e2cec860c4fc -> master (non-fast-forward)
error: failed to push some refs to 'lowkey.github.com:lowkeyliesmyth/archie-fork.git'
hint: Updates were rejected because a pushed branch tip is behind its remote
hint: counterpart. If you want to integrate the remote changes, use 'git pull'
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Whaaaaaat? Ok…that’s not great.
I’m sure there’s some magic git-fu that I could do to fix this, but after a bit futzing I just gave up and reset my local branch to the state it was in just before the subtree import.
So I had to (re-)import the theme repo subtree, which required me to overwrite my blog repo’s remote branch with a force-push but that’s not a big deal since I know my local copy is both correct and up to date. Now we’re back in business!
git reset HEAD~3
git subtree add --prefix kaffeinlabs/themes/archie-fork archie-fork master
git push -f
Since I have pulled in the theme repo’s history during the initial repo merge, subsequent pulls to/from the theme subtree have to include the --squash
flag to prevent commits touching the subtree from being fully duplicated in my git history graph. Moving forward, the command to push looks like this:
Pushing to theme repo:
git subtree push --prefix kaffeinlabs/themes/archie-fork archie-fork master --squash
Note that even when I correctly include this --squash
flag when pushing subtree updates to the subtree remote, I still get these unnecessary squash and merge commits smearing my repo’s history graph.
* e4064ee (HEAD -> reorg-posts, origin/reorg-posts) <2025-02-16 21:04:24 -0800> (40 seconds ago) [lowkey] Merge commit 'f6937ef9ecffcba57731d91bf8189de95788f8b3' into reorg-posts
|\
| * f6937ef <2025-02-16 21:00:32 -0800> (5 minutes ago) [lowkey] Squashed 'kaffeinlabs/themes/archie-fork/' changes from dd97cd0..e64e467
Problem 2
I’m in the middle of working on some feature or functionality that I intentionally started to work on and stumble across something unrelated that I didn’t expect and now also want to fix.
Some minor UI element spacing is off, or I see a color that looks like trash in dark mode. Whatever.
I want to fix that minor problem while it’s at the forefront of my mind and I still have the energy after work at the end of the day, but my options aren’t great:
- Make these changes and commit them into the current branch. Where they’ll get squashed into the feature on PR merge.
- Make the changes but don’t commit them. They’ll get squashed into the feature branch in a gross mega-commit or on PR merge.
- Git stash my current WIP, switch back to master, create a new feature branch, make these changes, commit and merge them in, switch back to my original feature branch and pop the stack back onto HEAD. Yeah right.
Solution 2
TLDR;
The magic of git worktrees.
cd ~/Documents/personal_repos/kaffeinlabs-hugo
git worktree add -b new-content ../kl-new-content
ANTLLRI; (Actually Not Too Long Let’s Read It)
After running the commands above I now have a full copy snapshot of this branch, but at a different OS filesystem folder! (~/Documents/personal_repos/kl-new-content
):
at 21:24:24 ❯ git worktree list
/Users/lowkey/Documents/personal_repos/kaffeinlabs-hugo e4064ee [feat-foobar]
/Users/lowkey/Documents/personal_repos/kl-new-content 5485148 [new-content]
Now I can keep working on my site presentation logic changes in my “main” directory in a feature branch (kaffeinlabs-hugo:feat-foobar) and when inspiration hits then I can write and publish content in complete isolation in a separate directory branch! (kl-new-content:new-content)
What about when I’m done and it’s time to merge my worktree branch back to master? At a high level the worktree cleanup flow looks like this:
- Merge the feature branch to master (`git co master && git merge new-content)
- Remove the feature branch worktree (
git worktree remove ../kl-new-content
) - Delete the feature branch (
git br -D new-content
)
Helpful References
Below are a few of the helpful references I found while working through the details. You may find them useful as well!