KaffeinLabs
The Spice Kaffein must flow

I can git subtree and so can you!

Git worktrees and subtrees are useful but not well known features. And knowing is half the battle!

Jun 18, 2025

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!

  1. 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?"
    
  2. 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
    
  3. 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
    
  4. 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"
    
  5. Remove git submodule references from repo config

     vim ~/Documents/personal_repos/kaffeinlabs-hugo/.git/config
     < yeet those submodule block references>
    
  6. 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:

  1. Make these changes and commit them into the current branch. Where they’ll get squashed into the feature on PR merge.
  2. 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.
  3. 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:

  1. Merge the feature branch to master (`git co master && git merge new-content)
  2. Remove the feature branch worktree (git worktree remove ../kl-new-content)
  3. 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!