A common task in Git workflow is the need to switch/checkout branches and check work that has been done on another branch, running tests, perhaps contributing to code that is out for Pull/Merge Review. If you’ve work in progress on your own branch this means either making a commit or stashing the work to come back to at a later date. Neither of these are particularly problematic as you can git pop
stashed work to restore it or git commit --amend
, or git commit --fixup
and squash commits to maintain small atomic commits and avoid cluttering up the commit history with commits such as “Saving work to review another branch”. But, perhaps unsurprisingly, Git has another way of helping your workflow in this situation. Rather than having branches you can use “worktrees”.
Normally when you’ve git clone
’d a repository all configuration files for working with the repository are saved to the repository directory under .git
and all files in their current state on the main
branch are also copied to the repository directory. If we clone the pytest-examples directory we can look at its contents using tree -afHD -L 2
(this limits the depth as we don’t need to look deep inside the .git
or mypy
directories which contain lots of files).
git clone git@github.com:ns-rse/pytest-examples.git
cd pytest-examples
tree -afhD -L 2
[4.0K Mar 11 07:26] .
├── [ 52K Jan 5 11:26] ./.coverage
├── [4.0K Mar 11 07:26] ./.git
│ ├── [ 749 Jan 5 11:30] ./.git/COMMIT_EDITMSG
│ ├── [ 394 Jan 5 11:28] ./.git/COMMIT_EDITMSG~
│ ├── [ 479 Feb 17 14:08] ./.git/config
│ ├── [ 556 Feb 17 14:06] ./.git/config~
│ ├── [ 73 Jan 1 13:24] ./.git/description
│ ├── [ 222 Mar 11 07:26] ./.git/FETCH_HEAD
│ ├── [ 21 Mar 11 07:26] ./.git/HEAD
│ ├── [4.0K Jan 1 13:27] ./.git/hooks
│ ├── [1.3K Mar 11 07:26] ./.git/index
│ ├── [4.0K Jan 1 13:24] ./.git/info
│ ├── [4.0K Jan 1 13:24] ./.git/logs
│ ├── [4.0K Mar 11 07:26] ./.git/objects
│ ├── [ 41 Mar 11 07:26] ./.git/ORIG_HEAD
│ ├── [ 112 Jan 3 15:57] ./.git/packed-refs
│ ├── [4.0K Jan 1 13:24] ./.git/refs
│ └── [4.0K Jan 1 13:31] ./.git/rr-cache
├── [4.0K Jan 2 11:52] ./.github
│ └── [4.0K Jan 3 15:57] ./.github/workflows
├── [3.0K Jan 2 12:06] ./.gitignore
├── [1.0K Jan 1 13:24] ./LICENSE
├── [ 293 Jan 2 12:06] ./.markdownlint-cli2.yaml
├── [4.0K Jan 5 11:27] ./.mypy_cache
│ ├── [ 12K Jan 5 11:28] ./.mypy_cache/3.11
│ ├── [ 190 Jan 2 10:39] ./.mypy_cache/CACHEDIR.TAG
│ └── [ 34 Jan 2 10:39] ./.mypy_cache/.gitignore
├── [1.7K Mar 11 07:26] ./.pre-commit-config.yaml
├── [ 763 Jan 1 13:25] ./.pre-commit-config.yaml~
├── [ 18K Jan 2 12:06] ./.pylintrc
├── [4.8K Mar 11 07:26] ./pyproject.toml
├── [4.7K Jan 1 17:36] ./pyproject.toml~
├── [4.0K Jan 1 19:04] ./.pytest_cache
│ ├── [ 191 Jan 1 19:04] ./.pytest_cache/CACHEDIR.TAG
│ ├── [ 37 Jan 1 19:04] ./.pytest_cache/.gitignore
│ ├── [ 302 Jan 1 19:04] ./.pytest_cache/README.md
│ └── [4.0K Jan 1 19:04] ./.pytest_cache/v
├── [4.0K Mar 11 07:26] ./pytest_examples
│ ├── [1.3K Mar 11 07:26] ./pytest_examples/divide.py
│ ├── [ 179 Mar 11 07:26] ./pytest_examples/__init__.py
│ ├── [4.0K Jan 5 11:18] ./pytest_examples/__pycache__
│ ├── [ 491 Mar 11 07:26] ./pytest_examples/shapes.py
│ └── [ 390 Jan 2 13:34] ./pytest_examples/shapes.py~
├── [4.0K Jan 2 16:09] ./pytest_examples.egg-info
│ ├── [ 1 Jan 2 16:09] ./pytest_examples.egg-info/dependency_links.txt
│ ├── [3.1K Jan 2 16:09] ./pytest_examples.egg-info/PKG-INFO
│ ├── [ 481 Jan 2 16:09] ./pytest_examples.egg-info/requires.txt
│ ├── [ 446 Jan 2 16:09] ./pytest_examples.egg-info/SOURCES.txt
│ └── [ 16 Jan 2 16:09] ./pytest_examples.egg-info/top_level.txt
├── [ 602 Jan 3 15:57] ./README.md
├── [ 0 Jan 1 13:31] ./README.md~
├── [4.0K Jan 1 13:30] ./.ruff_cache
│ ├── [4.0K Jan 2 11:57] ./.ruff_cache/0.1.8
│ ├── [ 43 Jan 1 13:30] ./.ruff_cache/CACHEDIR.TAG
│ └── [ 1 Jan 1 13:30] ./.ruff_cache/.gitignore
├── [4.0K Mar 11 07:26] ./tests
│ ├── [ 681 Mar 11 07:26] ./tests/conftest.py
│ ├── [ 26 Jan 2 12:11] ./tests/conftest.py~
│ ├── [4.0K Jan 5 11:26] ./tests/__pycache__
│ ├── [1.7K Mar 11 07:26] ./tests/test_divide.py
│ ├── [1.6K Mar 11 07:26] ./tests/test_shapes.py
│ └── [ 0 Jan 2 13:36] ./tests/test_shapes.py~
└── [ 460 Jan 2 16:09] ./_version.py
21 directories, 43 files
Lets create the contributing
branch
git switch -c contributing
echo "# Contributing\n\nContributions to this repository are welcome via Pull Requests." > CONTRIBUTING.md
If we want to switch branches without making a commit but save our work in progress as we want to add more to the CONTRIBUTING.md
file later we can stash the changes with a message. We then switch to main
and create a new branch (citation
) for and add a CITATION.cff
file.
git stash -m "An example stash"
git switch main
git switch -c citation
echo "cff-version: 1.2.0\ntitle: Pytest Examples\ntype: software" > CITATION.cff
git add CITATION.cff
git commit -m "Adding CITATION.cff"
When we are ready to return to our contributing
branch we can switch and git pop
the work we stashed. By default the last stash is popped, but its possible to view all the stashes and select which you wish to pop and restore to the current branch.
git switch contributing
git pop
Worktrees rather than branches
Worktrees take a different approach to organising branches. They start with a --bare
clone of the repository which implies the --no-checkout
flag and means that the files that would normally be found under the <repository>/.git
directory are copied but are instead placed in the top level of the directory rather than under .git/
. No tracked files are copied as they may conflict with these files. You have all the information Git has about the history of the repository and the different commits and branches but none of the actual files.
NB If you don’t explicitly state a target directory to clone to it will be the repository name suffixed with .git
, i.e. in this example pytest-examples.git
. I recommend sticking with the convention of using the same repository name so will explicitly state it.
cd ..
mv pytest-examples pytest-examples-orig-clone
git clone --bare git@github.com:ns-rse/pytest-examples.git pytest-examples
cd pytest-examples
tree -afhD -L 2
[4.0K Mar 13 07:45] .
├── [ 129 Mar 13 07:45] ./config
├── [ 73 Mar 13 07:45] ./description
├── [ 21 Mar 13 07:45] ./HEAD
├── [4.0K Mar 13 07:45] ./hooks
│ ├── [ 478 Mar 13 07:45] ./hooks/applypatch-msg.sample
│ ├── [ 896 Mar 13 07:45] ./hooks/commit-msg.sample
│ ├── [4.6K Mar 13 07:45] ./hooks/fsmonitor-watchman.sample
│ ├── [ 189 Mar 13 07:45] ./hooks/post-update.sample
│ ├── [ 424 Mar 13 07:45] ./hooks/pre-applypatch.sample
│ ├── [1.6K Mar 13 07:45] ./hooks/pre-commit.sample
│ ├── [ 416 Mar 13 07:45] ./hooks/pre-merge-commit.sample
│ ├── [1.5K Mar 13 07:45] ./hooks/prepare-commit-msg.sample
│ ├── [1.3K Mar 13 07:45] ./hooks/pre-push.sample
│ ├── [4.8K Mar 13 07:45] ./hooks/pre-rebase.sample
│ ├── [ 544 Mar 13 07:45] ./hooks/pre-receive.sample
│ ├── [2.7K Mar 13 07:45] ./hooks/push-to-checkout.sample
│ ├── [2.3K Mar 13 07:45] ./hooks/sendemail-validate.sample
│ └── [3.6K Mar 13 07:45] ./hooks/update.sample
├── [4.0K Mar 13 07:45] ./info
│ └── [ 240 Mar 13 07:45] ./info/exclude
├── [4.0K Mar 13 07:45] ./objects
│ ├── [4.0K Mar 13 07:45] ./objects/info
│ └── [4.0K Mar 13 07:45] ./objects/pack
├── [ 249 Mar 13 07:45] ./packed-refs
└── [4.0K Mar 13 07:45] ./refs
├── [4.0K Mar 13 07:45] ./refs/heads
└── [4.0K Mar 13 07:45] ./refs/tags
9 directories, 19 files
What use is that? Well from this point you can instead of using git branch
use git worktree add <branch_name>
and it will create a directory with the name of the branch which holds all the files in their current state on that branch.
git worktree add main
Preparing worktree (checking out 'main')
HEAD is now at 2f7c382 Merge pull request #6 from ns-rse/ns-rse/tidy-print
tree -afhD -L 2 main/
[4.0K Mar 13 08:13] main
├── [ 64 Mar 13 08:13] main/.git
├── [4.0K Mar 13 08:13] main/.github
│ └── [4.0K Mar 13 08:13] main/.github/workflows
├── [3.0K Mar 13 08:13] main/.gitignore
├── [1.0K Mar 13 08:13] main/LICENSE
├── [ 293 Mar 13 08:13] main/.markdownlint-cli2.yaml
├── [1.7K Mar 13 08:13] main/.pre-commit-config.yaml
├── [ 18K Mar 13 08:13] main/.pylintrc
├── [4.8K Mar 13 08:13] main/pyproject.toml
├── [4.0K Mar 13 08:13] main/pytest_examples
│ ├── [1.3K Mar 13 08:13] main/pytest_examples/divide.py
│ ├── [ 179 Mar 13 08:13] main/pytest_examples/__init__.py
│ └── [ 491 Mar 13 08:13] main/pytest_examples/shapes.py
├── [ 602 Mar 13 08:13] main/README.md
└── [4.0K Mar 13 08:13] main/tests
├── [ 681 Mar 13 08:13] main/tests/conftest.py
├── [1.7K Mar 13 08:13] main/tests/test_divide.py
└── [1.6K Mar 13 08:13] main/tests/test_shapes.py
5 directories, 14 files
Each branch can have a worktree added for it and then when you want to switch between them its is simply a case of cd
ing into the worktree (/branch) you wish to work on. You use Git commands within the directory to apply them to that branch and Git keeps track of everything in the usual manner.
Lets create two worktree’s, the contributing
and citation
we created above when working with branches.
git worktree add contributing
git worktree add citation
You are now free to move between worktrees (/branches) and undertake work on each without having to git stash
or git commit
work in progress. We can add the CONTRIBUTING.md
to the contributing
worktree then jump to the citation
worktree and add the CITATION.cff
cd contributing
echo "# Contributing\n\nContributions to this repository are welcome via Pull Requests." > CONTRIBUTING.md
cd ../citation
echo "cff-version: 1.2.0\ntitle: Pytest Examples\ntype: software" > CITATION.cff
Neither branches have had the changes committed so Git will not show any differences between them, but we can use diff -qr
to compare the directories.
diff -qr contributing citation
Only in citation: CITATION.cff
Only in contributing: CONTRIBUTING.md
Files contributing/.git and citation/.git differ
If we commit the changes to each we can git diff
them.
cd contributing
git add CONTRIBUTING.md
git commit -m "Adding basic CONTRIBUTING.md"
cd ../citation
git add CITATION.cff
git commit -m "Adding basic CITATION.cff"
git diff citation contributing
CITATION.cff --- Text
1 cff-version: 1.2.0
2 title: Pytest Examples
3 type: software
CONTRIBUTING.md --- Text
1 # Contributing
2
3 Contributions to this repository are welcome via Pull Requests
NB The output of git diff
may depend on the difftool that you have configured, I use and recommend the brilliant difftastic
which has easy integration with Git.
Listing Worktrees
Just as you can git branch --list
you can git worktree list
git worktree list
/mnt/work/git/hub/ns-rse/pytest-examples (bare)
/mnt/work/git/hub/ns-rse/pytest-examples/citation 19ff076 [citation]
/mnt/work/git/hub/ns-rse/pytest-examples/contributing ad56b91 [contributing]
/mnt/work/git/hub/ns-rse/pytest-examples/main 2f7c382 [main]
Moving Worktrees
You can move worktrees to different directories, these do not even have to be within the bare repository that you cloned as Git keeps track of these in the worktrees/
directory which has a folder for each of the worktrees you create and the file gitdir
points to the location of that particular worktree.
cd pytest-examples # Move to the bare repository
tree -afhD -L 2 worktrees
[4.0K Mar 13 09:27] worktrees
├── [4.0K Mar 13 09:31] worktrees/citation
│ ├── [ 26 Mar 13 09:31] worktrees/citation/COMMIT_EDITMSG
│ ├── [ 6 Mar 13 09:27] worktrees/citation/commondir
│ ├── [ 55 Mar 13 09:27] worktrees/citation/gitdir
│ ├── [ 25 Mar 13 09:27] worktrees/citation/HEAD
│ ├── [1.4K Mar 13 09:31] worktrees/citation/index
│ ├── [4.0K Mar 13 09:27] worktrees/citation/logs
│ ├── [ 0 Mar 13 09:31] worktrees/citation/MERGE_RR
│ ├── [ 41 Mar 13 09:27] worktrees/citation/ORIG_HEAD
│ └── [4.0K Mar 13 09:27] worktrees/citation/refs
├── [4.0K Mar 13 09:30] worktrees/contributing
│ ├── [ 29 Mar 13 09:30] worktrees/contributing/COMMIT_EDITMSG
│ ├── [ 6 Mar 13 09:27] worktrees/contributing/commondir
│ ├── [ 59 Mar 13 09:27] worktrees/contributing/gitdir
│ ├── [ 29 Mar 13 09:27] worktrees/contributing/HEAD
│ ├── [1.4K Mar 13 09:30] worktrees/contributing/index
│ ├── [4.0K Mar 13 09:27] worktrees/contributing/logs
│ ├── [ 0 Mar 13 09:30] worktrees/contributing/MERGE_RR
│ ├── [ 41 Mar 13 09:27] worktrees/contributing/ORIG_HEAD
│ └── [4.0K Mar 13 09:27] worktrees/contributing/refs
└── [4.0K Mar 13 08:13] worktrees/main
├── [ 6 Mar 13 08:13] worktrees/main/commondir
├── [ 51 Mar 13 08:13] worktrees/main/gitdir
├── [ 21 Mar 13 08:13] worktrees/main/HEAD
├── [1.3K Mar 13 08:13] worktrees/main/index
├── [4.0K Mar 13 08:13] worktrees/main/logs
├── [ 41 Mar 13 08:13] worktrees/main/ORIG_HEAD
└── [4.0K Mar 13 08:13] worktrees/main/refs
10 directories, 19 files
If we look at the gitdir
file in each worktree
sub-directory we see where they point to.
cat worktrees/*/gitdir
/mnt/work/git/hub/ns-rse/pytest-examples/citation/.git
/mnt/work/git/hub/ns-rse/pytest-examples/contributing/.git
/mnt/work/git/hub/ns-rse/pytest-examples/main/.git
These mirror the locations reported by git worktree list
, albeit with .git
appended.
If you want to move a worktree you can do so, here we move citation
to ~/tmp
.
git worktree move citation ~/tmp
Removing worktrees
It’s simple to remove a worktree after the changes have been merged or it is no longer needed, make sure to “prune” the tree after having done so.
git worktree remove citation
git worktree prune
git worktree list
/mnt/work/git/hub/ns-rse/pytest-examples (bare)
/mnt/work/git/hub/ns-rse/pytest-examples/contributing ad56b91 [contributing]
/mnt/work/git/hub/ns-rse/pytest-examples/main 2f7c382 [main]
Conclusion
Git Worktrees are a useful way of structuring your Git workflows if you have to switch branches regularly. They avoid the need to stash work in progress or make commits. If you do choose to use worktrees as an alternative to branches be mindful that you should remove and prune them after you have finished with them, particularly if you have a large codebase.
Links
Reuse
Citation
@online{shephard2024,
author = {Shephard, Neil},
title = {Git - {Worktrees}},
date = {2024-03-13},
url = {https://blog.nshephard.dev/posts/git-worktrees/},
langid = {en}
}