Skip to content

Instantly share code, notes, and snippets.

@ThomasFrans
Last active May 1, 2024 20:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ThomasFrans/ab1cb531410ab0cd0616a88a735dd840 to your computer and use it in GitHub Desktop.
Save ThomasFrans/ab1cb531410ab0cd0616a88a735dd840 to your computer and use it in GitHub Desktop.
A gentle introduction to Git worktree

A Gentle Introduction to Git Worktree

Git worktree has become a major part of how I use Git over the past year. Anytime I mention it somewhere however, I get reactions from people who have never heard about the feature. Others have heard about it, but don't know how exactly it works or why it's beneficial. That's why I decided to write a short tutorial/introduction on this awesome feature that is baked right into the very git you are already using. I hope this can help people discover worktrees and be a gentle introduction on how to get started using them.

What is Git worktree

Git worktree facilitates working with multiple branches. In a normal Git workflow, you can only ever have one branch checked out at a single time. If you want to switch to another branch, you use git checkout or git switch. This works most of the time, but what if you have changes in your worktree. Switching to another branch would update your worktree to be in sync with the one you checked out, thus overwriting your local changes. The solution in this case would be to stash your changes using git stash before switching.

Using stashing to switch branches works fine, but it isn't the nicest approach, especially when working on multiple branches at the same time. This is where Git worktree can help.

Git worktree is a command in Git that allows the user to create multiple worktrees in the same local repository. It has some options to do this, which we'll go over a bit later. You could think of git worktree as having multiple git repositories inside the same parent repository.

Difference with regular repository

While most things work exactly the same way as they normally do while using Git, there is one big difference. A repository that uses Git worktree has to be a bare repository. This simply means that by default, there will not be any worktree (the actual files in the repository). We have to 'attach' the branches we want to different directories, which will be their own worktrees. What this means in practice is that a cloned repository will look like this. Notice how there is no actual files, which would be the case for any repository we clone.

image

Normal Git -> Git worktree

The question is how to get to the worktree workflow from the normal one. To start off with, we need to clone the repository we want to work in with git clone --bare <url>. This will create a repository that looks like the following. Notice the '.git' added to the directory. This is a convention that is used for bare git repositories, and I would recommend to keep it there.

image

However there is a first problem. The remote branches aren't visible when we run git branch -vva.

image

To solve this, we need to tell Git to fetch all the refs from the remote. Open the config file in the repository, and add the following line to the remote we want to fetch all refs for.

fetch = +refs/heads/*:refs/remotes/fork/*

image

When we run git fetch now, we'll see the remote refs as well.

image

Now adding the local master branch to its own worktree is as simple as git worktree add master. I also like to set the upstream branch so git push and git pull work without specifying the branch. This can be done just like we would normally do with git branch -u <remote-branch> <local-branch>. In this case, running git branch -u fork/master master will set the remote master branch as the upstream branch for the local master branch. If we run git branch -vva again, we'll see the following.

image

We can now see that the local master branch has its upstream branch set by looking at the blue text next to it. For some reason, adding the main branch as a worktree doesn't show its worktree location, so here I quickly added another branch just to show that off as well.

image

That is basically all there is to it. Now we can cd into the directories and use any normal Git commands. Switching between branches is now as simple as changing directories. If we want to remove a worktree because we no longer need it, we can do so with git worktree remove <worktree>. If there are files in the worktree that are tracked by Git, modified, but not yet commited, this command will fail. To remove the branch with modified files, we would have to add the --force flag to the command.

General tips

  • Most IDEs don't offer special support for worktrees. They just treat each worktree as a separate project. You can always soft link files between them. I like to put folders like .idea and .vscode in the bare repo, and soft link to them from the worktrees.
  • While it seems possible, unfortunately it's not possible to start a repository as a bare repository, as its HEAD will point to a non-existant ref. It's very hard to solve this. That's why it's better to start with a normal repository, upload it to a remote, and clone it as a bare repository.
  • I have some extra options in my .gitconfig that make working with worktrees simpler. Here is the file for people who want to copy some of the options and aliases.
[init]
        defaultbranch = main
[pull]
        rebase = true
[status]
        short = true
        branch = true
[commit]
        gpgsign = true
[fetch]
    prune = true
[worktree]
    guessRemote = true
[core]
        editor = nvim
[alias]
    # Show a list of all the files that are being tracked.
        show-tracked = ls-tree --name-only -r HEAD

    # Stage the file for the next commit, but don't start tracking new files.
    stage = add -u

    # Make git aware of the existence of the file, but don't automatically stage
    # it.
    track = add -N

    # Unstage the file from the next commit.
    # Will also untrack a file that hasn't been commited once, so watch out!
    unstage = reset --

    # Make git unaware of the existence of the file (automatically unstages,
    # since git doesn't know the file anymore)
    untrack = rm --cached

    # Shortcuts
    l = log --oneline --decorate=full -n 40
    c = commit
    b = branch -vva
    r = remote -v
    s = status
    sl = status --long
    st = stage
    ust = unstage
    t = track
    ut = untrack
    a = add
    dt = difftool
    f = fetch --all
  • For Rust users, it can be a good idea to put .cargo/config.toml inside the bare repository, and add the following to it. This will cause all target files to be put in the same directory, speeding up compile times significantly, and making cleaning simpler.
[build]
target-dir="target"

Sources and additional information

I'll list some of the sources I used last year to learn how exactly worktrees worked, as well as some tutorials that might explain certain parts in more detail/differently.

Contributions

It's my first time writing a gist. If contributions are possible and you notice an error or somthing that can be improved or needs more detail, feel free to create a pull request. :)

@socketbox
Copy link

This is nice, but instead of cluttering the project directory with all of the files related to the bare clone, you could create a .bare directory inside of the project directory and then point git to it via a .git file. Then you've got a nice directory structure like so:

./configfiles
./configfiles/.bare
./configfiles/.git
./configfiles/main
./configfiles/feature-branch
...

This structure was introduced to me by @slightlyoff (via his blog): https://infrequently.org/2021/07/worktrees-step-by-step/

@ThomasFrans
Copy link
Author

This is an interesting addition. It is indeed a bit annoying that the directory where the worktrees are created also contains all the Git files. This seems like a nice way to clean it up. Thanks for the extra information.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment