After years of juggling between branches with git stash
, I finally made the switch to Git Worktrees. If you’ve ever found yourself in “stash hell” or accidentally lost work while switching branches, this post is for you.
Table of contents
Open Table of contents
What are Git Worktrees?
Think of Git Worktrees as having multiple working directories for the same repository, each checked out to a different folder. Instead of having one folder where you constantly switch between branches (and stash/unstash your work each time you do), you can keep a separate folder for each branch you’re working on.
Traditional workflow:
my-repo/ (switches between main, feature-a, feature-b)
├── git stash (your uncommitted work)
├── git checkout feature-a
├── git stash pop
├── work work work...
├── git stash
├── git checkout main
└── rinse and repeat
Worktrees workflow:
my-repo-main/ (always on main)
my-repo-feature-a/ (always on feature-a)
my-repo-feature-b/ (always on feature-b)
No more stashing. No more “oh crap, did I commit that change?” No more switching mental context when you jump between branches. Each worktree is its own little world. 🌏
Minor hurdles 🧩
Folder Structure
The first thing I realised when I started out is that worktrees can’t be subdirectories of your main repo. You can’t do this:
# ❌ This won't work
my-repo/
├── 📁 .git/
├── 📁 main/
└── 📁 feature-a/
└── 📁 feature-b/
Instead, the common convention is to create sibling directories:
# ✅ This is the way
my-repo/
├── 📁 main/
├──── 📁 .git/
├── 📁 feature-a/
└── 📁 feature-b/
Or alternatively, you could do your initial checkout to a bare repository (i.e. a repo without any working directory) and then create your worktrees from there.
# ✅ Surely there can be more than one way though right?
my-repo/
├── my-repo/
├──── 📁 hooks
├──── 📁 info
├──── 📁 logs
├──── 📁 refs
├──── 📁 worktrees
├──── config
├──── description
├──── HEAD
├──── packed-refs
├── feature-a/
└── feature-b/
Goodbye GitHub Desktop, Hello Lazygit
I have been, out of pure laziness, using GitHub Desktop for years… it’s nice and simple and does 95% of what I need… but it doesn’t support worktrees. At all.
After a cursory look at other options, I ended up going with Lazygit. This is a terminal-based Git UI that turns out to be fucking fantastic.
Honestly, even without any need to support worktrees, Lazygit has been a game-changer - check this out to get a feel for some of the things that it can do.
Dialing it in 🥷🏻
Creating Worktrees
To create a new worktree in Lazygit you press n
from the Worktree panel. This kicks off a wizard that asks you:
- Whether you want it to be detached or not
- The base ref to use for the new worktree
- The new worktree path
- A name to give the new branch (assuming it’s not detached)
While all of those things can be required information and I’m sure you can use them to do flexible and wonderful things, in my case I found that 99% of the time I’m following a convention that means I can create a new worktree with two pieces of information:
- What base ref do you want to branch from?
- What do you want to call the new branch + worktree?
Initially I was doing that from the command line outside of Lazygit.
Firstly, it turns out leaving Lazygit wasn’t necessary (you can press :
to execute arbitrary git commands from within Lazygit).
Secondly, it turns out you don’t have enter a custom git command to do this every time either. Lazygit has something called Custom Command Keybindings that you can use to define custom actions that can be performed from the various different panels (or “contexts”) in Lazygit.
Here are the two custom commands I added to my Lazygit config:
customCommands:
# Create worktree from current branch (when in worktrees view)
- key: "N"
context: "worktrees"
description: "Create new worktree from current branch"
prompts:
- type: "input"
title: "Create a new worktree from {{ .CheckedOutBranch.Name }} with what name?"
key: "Name"
initialValue: ""
command: "git worktree add ../{{ .Form.Name }} {{ .CheckedOutBranch.Name }} -b {{ .Form.Name }}"
# Create worktree from selected branch (when in branches view)
- key: "N"
context: "localBranches"
description: "Create new worktree from selected branch"
prompts:
- type: "input"
title: "Create a new worktree from {{ .SelectedLocalBranch.Name }} with what name?"
key: "Name"
initialValue: ""
command: "git worktree add ../{{ .Form.Name }} {{ .SelectedLocalBranch.Name }} -b {{ .Form.Name }}"
The first one of these can be accessed by pressing N
from the worktrees
context/panel in Lazygit. It asks you what you want to call the new worktree then creates a new worktree using the currently checked out branch (in the current worktree) as the base ref.
The second one lets you press N
from the localBranches
panel and does more or less the same, but uses the currenty selected local branch as the base ref instead (that way you don’t have to checkout the branch just to create a worktree/branch from it).
Submodules
The repository I use for my day job has loads of submodules… and Lazygit currently has a bug/limitation such that it can’t delete worktrees that contain submodules (issue #4125).
However, with a bit of help from the awesome Lazygit community, I managed to use a Custom Command Keybinding to workaround that as well:
customCommands:
- key: "D"
context: "worktrees"
description: "Force remove selected worktree"
prompts:
- type: "confirm"
title: "Force Remove Worktree"
body: "Are you sure you want to remove this worktree and any submodules?"
command: "git worktree remove -f {{ .SelectedWorktree.Path | quote }}"
loadingText: "Removing worktree..."
output: log
This uses git worktree remove -f
which handles submodules properly. There’s already a PR in progress to fix this in Lazygit itself, so this custom command might not be needed in the future.
Prettier Diffs with Delta
While we’re customizing Lazygit, why not make those diffs look gorgeous? I configured Delta as my custom pager:
git:
paging:
colorArg: always
pager: delta --dark --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"
You’ll need to install Delta first:
brew install git-delta
This gives you syntax highlighting, line numbers, and even clickable hyperlinks in your diffs. It’s close to the experience you get for diffs in GitHub Desktop, in your terminal.
Wrapping Up
Git Worktrees have definitely changed how I work with Git. The builds for the repo I’m working on can take quite a while and I often need to tweak stuff to get everything passing in CI (adding changelog entries etc.). Using Git Worktrees, it’s way easier to keep multiple plates spinning concurrently and I’m getting more done in each day.
The initial setup took a little bit of effort, especially if you’re coming from a GUI like GitHub Desktop. But once you’ve got Lazygit configured with these optimizations, you’ll wonder how you ever lived without worktrees.
Give it a try on your next project. Your future self (and your stash history) will thank you!