Infrequently Noted

Alex Russell on browsers, standards, and the process of progress.

Git Worktrees Step-By-Step

Git Worktrees appear to solve a set of challenges I encounter when working on this blog:

  1. Maintenance branches for 11ty and other dependencies come and go with some frequency.
  2. Writing new posts on parallel branches isn't fluid when switching frequently.
  3. If I incidentally mix some build upgrades into a content PR, it can be difficult to extract and re-apply if developed in a single checkout.

Worktrees hold the promise of parallel working branch directories without separate backing checkouts. Tutorials I've found seemed to elide some critical steps, or required deeper Git knowledge than I suspect is common (I certainly didn't have it!).

After squinting at man pages for more time than I'd care to admit and making many mistakes along the way, here is a short recipe for setting up worktrees for a blog repo that, in theory, already exists at github.com/example/workit:

##
# Make a directory to hold a branches, including main
##

$ cd /projects/
$ mkdir workit
$ cd workit
$ pwd
# /projects/workit

##
# Next, make a "bare" checkout into `.bare/`
##

$ git clone --bare git@github.com:example/workit.git .bare
# Cloning into bare repository '.bare'...
# remote: Enumerating objects: 19601, done.
# remote: Counting objects: 100% (1146/1146), done.
# ...

##
# Tell Git that's where the goodies are via a `.git`
# file that points to it
##

$ echo "gitdir: ./.bare" > .git

##
# *Update* (2021-09-18): OPTIONAL
#
# If your repo is going to make use of Git LFS, at
# this point you should stop and edit `.bare/config`
# so that the `[remote "origin"]` section reads as:
#
# [remote "origin"]
# url = git@github.com:example/workit.git
# fetch = +refs/heads/*:refs/remotes/origin/*
#
# This ensures that new worktrees do not attempt to
# re-upload every resource on first push.
##

##
# Now we can use worktrees.
#
# Start by checking out main; will fetch repo history
# and may therefore be slow.
##

$ git worktree add main
# Preparing worktree (checking out 'main')
# ...
# Filtering content: 100% (1226/1226), 331.65 MiB | 1.17 MiB/s, done.
# HEAD is now at e74bc877 do stuff, also things

##
# From here on out, adding new branches will be fast
##

$ git worktree add test
# Preparing worktree (new branch 'test')
# Checking out files: 100% (2216/2216), done.
# HEAD is now at e74bc877 do stuff, also things

##
# Our directory structure should now look like
##

$ ls -la
# total 4
# drwxr-xr-x 1 slightlyoff eng 38 Jul 7 23:11 .
# drwxr-xr-x 1 slightlyoff eng 964 Jul 7 23:04 ..
# drwxr-xr-x 1 slightlyoff eng 144 Jul 7 23:05 .bare
# -rw-r--r-- 1 slightlyoff eng 16 Jul 7 23:05 .git
# drwxr-xr-x 1 slightlyoff eng 340 Jul 7 23:11 main
# drwxr-xr-x 1 slightlyoff eng 340 Jul 7 23:05 test

##
# We can work in `test` and `main` independently now
##

$ cd test
$ cat "yo" > test.txt
$ git add test.txt
$ git commit -m "1, 2, 3..." test.txt
# [test 2e3f30b9] 1, 2, 3...
# 1 file changed, 1 insertion(+)
# create mode 100644 test.txt

$ git push --set-upstream origin test
# ...

Thankfully, commands like git worktree list and git worktree remove are relatively WYSIWYG by comparison to the initial setup.

Perhaps everyone else understands .git file syntax and how it works with --bare checkouts, but I didn't. Hopefully some end-to-end exposition can help drive adoption of this incredibly useful feature.