Branch Management With Git Worktree
Git
If you need to frequently switch between Git branches, the worktree
feature can be a useful way of working since it allows you to check out more than one branch at a time.
It avoids the necessity of either committing or stashing changes before switching to a different branch because each branch is managed in a new working tree - i.e. worktree branches exist as a separate directory trees on your filesytem.
You can add a new worktree at any path - I find it convenient to add .worktrees
to a global .gitignore
and add worktree branches to a .wortrees
subdirectory of the main project root.
Why Bother?
You’re deep in refactoring on a feature branch and you need to review a co-worker’s PR for a hotfix urgently.
You could add & commit your changes, but they are currently not in a satisfactory state and you are aiming to keep the project commit history as clean as possible. You could stash changes, change to the PR branch, and pop the stash when finished, but this is messy and can be confusing if you’re managing multiple stashes.
Adding the PR as a new worktree branch allows you to simply change directory into the new PR branch without worrying about disturbing complex ongoing changes in the branch you’re working on.
Basic Example: Add New Branch as a Worktree
git worktree add <path>
To add a hotfix
worktree, from the project root:
git worktree add .worktrees/hotfix
This creates a new branch named hotfix
- the final component of .worktrees/hotfix
- and checks it out at the .worktrees/hotfix
path.
Add a second worktree branch:
git worktree add .worktrees/feature-1
The project file structure now looks like this:
.
├── README.md
└── .worktrees
├── feature-1
│ └── README.md
└── hotfix
└── README.md
The top level README.md
is in the original branch.
At this point, running git branch
from the top level of the project shows:
dev + feature-1 + hotfix * master
This shows a branch dev
that is not a worktree, along with two worktree branches feature-1
and hotfix
marked with the +
symbol.
The current checked-out branch master
is marked with a *
.
If you move into hotfix
- cd .worktrees/hotfix
, git branch
will now show:
dev + feature-1 * hotfix + master
In each case, the current branch is colorised and marked with an asterisk. The plus symbol indicates that you can access/work on the branch by moving in to the relevant directory.
In the example above, the dev
branch does not have a linked worktree.
Work on Existing Branch in a New Worktree
To set up an existing branch as a worktree, run git worktree add <path> <branch>
. For example:
git worktree add .worktrees/my-existing-branch my-existing-branch
A new working tree is created at .worktrees/my-existing-branch
and my-existing-branch
is checked out at this location.
Remove Worktrees
Run git worktree remove worktree-name
- where worktree-name
is the final component of the worktree path. If you’re unsure of the worktree name, git worktree list
will list worktrees in the current repo:
git worktree list
/tmp/tmp.e4JD71KDKU 0f4b170 [master]
/tmp/tmp.e4JD71KDKU/.worktrees/feature-1 3471bba [feature-1]
/tmp/tmp.e4JD71KDKU/.worktrees/hotfix 565c127 [hotfix]
To remove the hotfix
worktree:
git worktree remove hotfix
The hotfix worktree is now removed, but the branch is still available should you still need it.
If you just delete the worktree directory without using git worktree remove
, don’t panic - Git has you covered. Garbage collection will eventually remove the metadata files for the (now non-existent) worktree.
Working on Branches
To work on a worktree branch, move into the correct directory. Make changes as required and use git add
and git commit
as usual. When you’re finished, simply return to the original directory.
Fetch a PR GitHub and Set Up as a Worktree
Save this script as an executable file named gitfetch-pr
in your PATH
:
#!/usr/bin/env bash
# /path/gitfetch-pr
set -euo pipefail
function usage {
cat <<- EOF
Provide the PR reference number from within a Git project:
$(basename $0) <PR reference number>
EOF
}
[[ $# != 1 ]] && { usage; exit 1; }
[[ -d "$PWD/.git" ]] || { echo "No .git directory detected."; usage; exit 2; }
[[ "${1}" =~ ^[0-9]+$ ]] || { echo "The PR reference should be an integer."; usage; exit 3; }
git fetch origin "pull/${1}/head:PR${1}"
echo "Done."
You can now run gitfetch-pr 42
to fetch the commit associated with pull request #42 and set this up as a branch PR42
.
To set this up as a worktree, run:
git worktree add .worktrees/PR42 PR42
You can view the PR42 commit by navigating to .worktrees/PR42
.
You could also set this up as a git alias rather than a shell script, as demonstrated in the following example.
Fetch & Checkout a GitLab Merge Request
This can be achieved with a git alias
. Add the following to ~/.gitconfig
:
[alias]
# Allows: `git mr origin 5` to fetch & checkout MR 5 from origin
mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
To use, run git mr origin 42
fetches the specified merge request into a local mr-upstream-42
branch.
To use this as a worktree:
git worktree add .worktrees/mr42 mr-origin-42
This adds the branch mr-origin-42
to the worktree directory .worktrees/mr42
.
Using Standard Branch Comparison
You can still compare across branches and pull specific hunks as you would usually do. For example, if working in the hotfix worktree you can view (and diffput
/diffget
) differences between the current file in the current branch and the same file in the feature-1 branch using vim fugitive:
:Gvdiff feature-1:%
Alternatively, open a split window in your editor, open the same file in a different worktree and pull changes using the editor’s fucntionality.
References
comments powered by Disqus