Git Worktrees Step-By-Step
Git Worktrees appear to solve a set of challenges I encounter when working on this blog:
- Maintenance branches for 11ty and other dependencies come and go with some frequency.
- Writing new posts on parallel branches isn't fluid when switching frequently.
- 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.