Software developers are oftentimes subject to “context switching”. In essence, this means that they need to switch to some other task before they are able to finish the current one. This, of course, can have a multitude of reasons. Common ones include:
- Creating a hotfix for some pressing production issues
- A colleague is off work, and they must continue their issue that is of higher priority
- Their PR was finally reviewed, and they need to adjust a few things
- They rewrite part of an application and want to open both the old and the new version in their IDE without cloning the repository again into some temporary folder
Git Worktrees
Thankfully, almost everyone nowadays is using some form of version control system like git, which allows to switch contexts (or, in this case, “branches”) without throwing away our work altogether. Most developers would do one of two things in a situation like this:
- Stash all changes: having to unstash them again later, which makes switching often tedious
- Make a work-in-progress commit: which may litter their commit history with broken states or mean that, depending on their workflow, they have to remember to squash their commits into a working feature commit later on.
In this article, we will show you a third option of switching branches that doesn’t involve neither stashing nor committing. Let’s face it, the easiest and most convenient solution would be if we could just check out multiple commits at the same time without them interfering with one another, right?
That’s exactly what Git Worktrees allow us to do! Before delve into how it works in detail, let us first look at what a “git repo” actually consists of.
What Happens When We Clone a Repository?
Whenever we clone a repository the “standard” way via
git clone <repo-url>
We actually end up with two things. The first and most obvious one is often called the repository but, actually this is our working tree. It includes all the files and directories we have put under version control. It is set to track our main/master branch by default.
cd <repo-name>
C:/working/directory/repo-name (main -> origin)
ls
-> ... README.md src/ ...
If we look at everything git generated, we will also find a .git directory.
ls -a
-> ./ ../ ... .git/ .gitignore README.md src ...
This is what actually comprises our repository. It includes all the necessary files and configuration to make version control work and will record all the changes we make to our code base. Now, if we type:
git worktree list
It should output something along the lines of:
C:/working/directory/repo-name <commit-ish> [main]
As we can see, by cloning the repository, git has already created a worktree for the branch main
for us. Let’s next look at how we can add more worktrees.
Quick, We Need a Hotfix!
Imagine we are working on a new footer component for our fancy web-app
C:/working/directory/repo-name/src/components (feature/add-footer-component -> origin)
ls
-> header-component.ts main-form-component.ts
C:/working/directory/repo-name/src/components (feature/add-footer-component -> origin)
vim footer-component.ts
# ... hack hack hack ...
# ... miraculously exit vim ...
C:/working/directory/repo-name/src/components (feature/add-footer-component -> origin)
ls
-> header-component.ts main-form-component.ts footer.component.ts
Suddenly, we get a call from our boss telling us that an important hotfix is needed for production. Someone made a typo in the header component that managed to slip through all stages of QA. Instead of the usual “Oof, this fix will take at least 2 weeks” we confidently respond: “No big deal, boss” and type:
C:/working/directory/repo-name/src/components
git worktree add -b emergency-typo-fix ../../../temp main
cd ../../../temp
What we did there was not magic but checking out a new (-b)
branch called emergency-typo-fix
from main
, putting it in the /temp
directory (one level up from the root directory!) and switching to that directory.
Since we checked out the new branch from the current main
our unfinished footer component will not be in our new working tree, but it will still exist in our other working tree.
C:/working/directory/temp
ls
-> header-component.ts main-form-component.ts
vim header-component.ts
# fix typo -> save -> commit -> set upstream and push
cd -
C:/working/directory/repo-name/src/components (feature/add-footer-component -> origin)
ls
-> header-component.ts main-form-component.ts footer.component.ts
To instead checkout an existing remote branch locally, which will automatically track it, use:
git worktree add ../feature-directory featureX
-> Preparing worktree (new branch 'featureX')
-> Branch 'featureX' set up to track remote branch 'featureX' from 'origin'.
-> HEAD is now at <commit-ish> <commit-message>
For tracking information about our branches, we can always use:
git branch -vv
# add -a for listing remote branches too
# for even more info, use: git remote show origin
And if you were wondering: yes, we can still checkout other branches with git checkout
from a worktree, under the condition that they are not already checked out in another worktree.
What Happens When We Delete a Worktree Directory Manually?
Now, we have saved the day with our hotfix but are left with a worktree and a branch that we no longer need. To get rid of the worktree type:
git worktree remove /path/to/worktree
This will remove the directory as well as the information of the worktree. If we manually delete the directory of the worktree, the reference to the worktree in .git/worktrees
would still exist, and it would continue to appear in git worktree list
. If this ever happens, just type git worktree prune
, which removes stale references to worktrees that no longer exist.
Removing a worktree doesn’t remove the branch it tracked, so to get rid of it as well, we still need to execute
git branch -D <branch>
If we want to avoid forgetting this, we could, combine both commands
git worktree remove /path/to/worktree && git branch -D <branch>
How To Fix Issues When We Manually Delete Or Move Worktrees?
We have already covered what happens when we delete a worktree directory manually and how the prune
keyword will help us remove the stale reference, but what happens when we move or rename a worktree directory?
For example:
git worktree add -b urgent-fix ../urgent-fox main
We accidentally spelled ‘fox’ instead of ‘fix’ for the directory, so we quickly rename it:
mv ../urgent-fox ../urgent-fix
cd ../urgent-fix
But upon typing git worktree list
we still see
C:/working/directory/urgent-fox 123xyz [urgent-fix]
That is because our gitdir
did not update itself when renaming the directory. Instead, we should have used
git worktree move ../urgent-fox ../urgent-fix
but it’s too late now, so how can we “repair” this damage? Simple. From the worktree that is out of sync, type:
git worktree repair
and we should see something like
repair: gitdir incorrect: C:/working/directory/main-worktree/.git/worktrees/urgent-fox/gitdir
The gitdir
for this worktree that resides in our main worktree’s .git
directory was updated with the new location of the worktree. Now git worktree list
should list our repaired worktree location.
If instead we have moved or renamed our main worktree though, this means all other worktrees will not work anymore, since every reference will be wrong. We will get fatal: not a git repository
errors. Again git worktree repair
(run from our main worktree) fixes this issue and re-links all worktrees.
In a Nutshell
We have learned that we can create worktrees for as many branches (or commits) as we want. Each will be placed in its own directory, leaving our current changes untouched and allowing us to switch between branches without any interference as many times as we like.
We should now also know the differences between a git repository
and a worktree
as well as how to create and remove worktrees. In addition, we covered how to fix some of the more common issues that occur when we manually delete or move worktrees instead of using the appropriate commands for them.
What you may have already noticed during this showcase is that the folder structure of worktrees can seem a bit weird. We ended up with something like this.
- hotfix
- ... source files ...
- .git (file)
- awesome-web-app-repo
- ... source files ...
- .git/ (dir)
- worktrees/
- hotfix/
- ...
- ...
- some-other-repo
Instead, most people would probably want something more “clean” like:
- awesome-web-app-repo
- .git/
- worktrees/
- main/
- hotfix/
- ...
- main
- ... source files ...
- .git
- hotfix
- ... source files ...
- .git
- ...
- some-other-repo
For this result, we advise you to have a look at bare repositories which are awesome in their own right but can be a bit of a hassle to set up for remote tracking.
Read more Insights about Software Product Engineering.
