370 lines
13 KiB
Markdown
370 lines
13 KiB
Markdown
# Git Worktrees
|
|
|
|
## Why?
|
|
|
|
The default workflow when using git is having your repository in a single directory.
|
|
Then, you can check out a certain reference (usually a branch), which will update
|
|
the files in the directory to match the state of that reference. Most of the time,
|
|
this is exactly what you need and works perfectly. But especially when you're using
|
|
with branches a lot, you may notice that there is a lot of work required to make
|
|
everything run smootly.
|
|
|
|
Maybe you experienced the following: You're working on a feature branch. Then,
|
|
for some reason, you have to change branches (maybe to investigate some issue).
|
|
But you get the following:
|
|
|
|
```
|
|
error: Your local changes to the following files would be overwritten by checkout
|
|
```
|
|
|
|
Now you can create a temporary commit or stash your changes. In any case, you have
|
|
some mental overhead before you can work on something else. Especially with stashes,
|
|
you'll have to remember to do a `git stash pop` before resuming your work (I
|
|
cannot count the number of times where is "rediscovered" some code hidden in some
|
|
old stash I forgot about.
|
|
|
|
And even worse: If you're currently in the process of resolving merge conflicts or an
|
|
interactive rebase, there is just no way to "pause" this work to check out a
|
|
different branch.
|
|
|
|
Sometimes, it's crucial to have an unchanging state of your repository until some
|
|
long-running process finishes. I'm thinking of Ansible and Terraform runs. I'd
|
|
rather not change to a different branch while ansible or Terraform are running as
|
|
I have no idea how those tools would behave (and I'm not too eager to find out).
|
|
|
|
In any case, Git Worktrees are here for the rescue:
|
|
|
|
## What are git worktrees?
|
|
|
|
[Git Worktrees](https://git-scm.com/docs/git-worktree) allow you to have multiple
|
|
independent checkouts of your repository on different directories. You can have
|
|
multiple directories that correspond to different references in your repository.
|
|
Each worktree has it's independent working tree (duh) and index, so there is no
|
|
to run into conflicts. Changing to a different branch is just a `cd` away (if
|
|
the worktree is already set up).
|
|
|
|
## Worktrees in GRM
|
|
|
|
GRM exposes an opinionated way to use worktrees in your repositories. Opinionated,
|
|
because there is a single invariant that makes reasoning about your worktree
|
|
setup quite easy:
|
|
|
|
**The branch inside the worktree is always the same as the directory name of the worktree.**
|
|
|
|
In other words: If you're checking out branch `mybranch` into a new worktree, the
|
|
worktree directory will be named `mybranch`.
|
|
|
|
GRM can be used with both "normal" and worktree-enabled repositories. But note
|
|
that a single repository can be either the former or the latter. You'll have to
|
|
decide during the initial setup which way you want to go for that repository.
|
|
|
|
If you want to clone your repository in a worktree-enabled way, specify
|
|
`worktree_setup = true` for the repository in your `config.toml`:
|
|
|
|
```toml
|
|
[[trees.repos]]
|
|
name = "git-repo-manager"
|
|
worktree_setup = true
|
|
```
|
|
|
|
Now, when you run a `grm sync`, you'll notice that the directory of the repository
|
|
is empty! Well, not totally, there is a hidden directory called `.git-main-working-tree`.
|
|
This is where the repository actually "lives" (it's a bare checkout).
|
|
|
|
Note that there are few specific things you can configure for a certain
|
|
workspace. This is all done in an optional `grm.toml` file right in the root
|
|
of the worktree. More on that later.
|
|
|
|
### Creating a new worktree
|
|
|
|
To actually work, you'll first have to create a new worktree checkout. All
|
|
worktree-related commands are available as subcommands of `grm worktree` (or
|
|
`grm wt` for short):
|
|
|
|
```
|
|
$ grm wt add mybranch
|
|
[✔] Worktree mybranch created
|
|
```
|
|
|
|
You'll see that there is now a directory called `mybranch` that contains a checkout
|
|
of your repository, using the branch `mybranch`
|
|
|
|
```bash
|
|
$ cd ./mybranch && git status
|
|
On branch mybranch
|
|
nothing to commit, working tree clean
|
|
```
|
|
|
|
You can work in this repository as usual. Make changes, commit them, revert them,
|
|
whatever you're up to :)
|
|
|
|
Just note that you *should* not change the branch inside the worktree
|
|
directory. There is nothing preventing you from doing so, but you will notice
|
|
that you'll run into problems when trying to remove a worktree (more on that
|
|
later). It may also lead to confusing behaviour, as there can be no two
|
|
worktrees that have the same branch checked out. So if you decide to use the
|
|
worktree setup, go all in, let `grm` manage your branches and bury `git branch`
|
|
(and `git checkout -b`).
|
|
|
|
You will notice that there is no tracking branch set up for the new branch. You
|
|
can of course set up one manually after creating the worktree, but there is an
|
|
easier way, using the `--track` flag during creation. Let's create another
|
|
worktree. Go back to the root of the repository, and run:
|
|
|
|
```bash
|
|
$ grm wt add mybranch2 --track origin/mybranch2
|
|
[✔] Worktree mybranch2 created
|
|
```
|
|
|
|
You'll see that this branch is now tracking `mybranch` on the `origin` remote:
|
|
|
|
```bash
|
|
$ cd ./mybranch2 && git status
|
|
On branch mybranch
|
|
|
|
Your branch is up to date with 'origin/mybranch2'.
|
|
nothing to commit, working tree clean
|
|
```
|
|
|
|
The behaviour of `--track` differs depending on the existence of the remote branch:
|
|
|
|
* If the remote branch already exists, `grm` uses it as the base of the new
|
|
local branch.
|
|
* If the remote branch does not exist (as in our example), `grm` will create a
|
|
new remote tracking branch, using the default branch (either `main` or `master`)
|
|
as the base
|
|
|
|
Often, you'll have a workflow that uses tracking branches by default. It would
|
|
be quite tedious to add `--track` every single time. Luckily, the `grm.toml` file
|
|
supports defaults for the tracking behaviour. See this for an example:
|
|
|
|
```toml
|
|
[track]
|
|
default = true
|
|
default_remote = "origin"
|
|
```
|
|
|
|
This will set up a tracking branch on `origin` that has the same name as the local
|
|
branch.
|
|
|
|
Sometimes, you might want to have a certain prefix for all your tracking branches.
|
|
Maybe to prevent collissions with other contributors. You can simply set
|
|
`default_remote_prefix` in `grm.toml`:
|
|
|
|
```toml
|
|
[track]
|
|
default = true
|
|
default_remote = "origin"
|
|
default_remote_prefix = "myname"
|
|
```
|
|
|
|
When using branch `my-feature-branch`, the remote tracking branch would be
|
|
`origin/myname/my-feature-branch` in this case.
|
|
|
|
Note that `--track` overrides any configuration in `grm.toml`. If you want to
|
|
disable tracking, use `--no-track`.
|
|
|
|
### Showing the status of your worktrees
|
|
|
|
There is a handy little command that will show your an overview over all worktrees
|
|
in a repository, including their status (i.e. changes files). Just run the following
|
|
in the root of your repository:
|
|
|
|
```
|
|
$ grm wt status
|
|
╭───────────┬────────┬──────────┬──────────────────╮
|
|
│ Worktree ┆ Status ┆ Branch ┆ Remote branch │
|
|
╞═══════════╪════════╪══════════╪══════════════════╡
|
|
│ mybranch ┆ ✔ ┆ mybranch ┆ │
|
|
│ mybranch2 ┆ ✔ ┆ mybranch ┆ origin/mybranch2 │
|
|
╰───────────┴────────┴──────────┴──────────────────╯
|
|
```
|
|
|
|
The "Status" column would show any uncommitted changes (new / modified / deleted
|
|
files) and the "Remote branch" would show differences to the remote branch (e.g.
|
|
if there are new pushes to the remote branch that are not yet incorporated into
|
|
your local branch).
|
|
|
|
|
|
### Deleting worktrees
|
|
|
|
If you're done with your worktrees, use `grm wt delete` to delete them. Let's
|
|
start with `mybranch2`:
|
|
|
|
```
|
|
$ grm wt delete mybranch2
|
|
[✔] Worktree mybranch2 deleted
|
|
```
|
|
|
|
Easy. On to `mybranch`:
|
|
|
|
```
|
|
$ grm wt delete mybranch
|
|
[!] Changes in worktree: No remote tracking branch for branch mybranch found. Refusing to delete
|
|
```
|
|
|
|
Hmmm. `grm` tells you:
|
|
|
|
"Hey, there is no remote branch that you could have pushed
|
|
your changes to. I'd rather not delete work that you cannot recover."
|
|
|
|
Note that `grm` is very cautious here. As your repository will not be deleted,
|
|
you could still recover the commits via [`git-reflog`](https://git-scm.com/docs/git-reflog).
|
|
But better safe then sorry! Note that you'd get a similar error message if your
|
|
worktree had any uncommitted files, for the same reason. Now you can either
|
|
commit & push your changes, or your tell `grm` that you know what you're doing:
|
|
|
|
```
|
|
$ grm wt delete mybranch --force
|
|
[✔] Worktree mybranch deleted
|
|
```
|
|
|
|
If you just want to delete all worktrees that do not contain any changes, you
|
|
can also use the following:
|
|
|
|
```
|
|
$ grm wt clean
|
|
```
|
|
|
|
Note that this will not delete the default branch of the repository. It can of
|
|
course still be delete with `grm wt delete` if neccessary.
|
|
|
|
### Persistent branches
|
|
|
|
You most likely have a few branches that are "special", that you don't want to
|
|
clean up and that are the usual target for feature branches to merge into. GRM
|
|
calls them "persistent branches" and treats them a bit differently:
|
|
|
|
* Their worktrees will never be deleted by `grm wt clean`
|
|
* If the branches in other worktrees are merged into them, they will be cleaned
|
|
up, even though they may not be in line with their upstream. Same goes for
|
|
`grm wt delete`, which will not require a `--force` flag. Note that of
|
|
course, actual changes in the worktree will still block an automatic cleanup!
|
|
* As soon as you enable persistent branches, non-persistent branches will only
|
|
ever cleaned up when merged into a persistent branch.
|
|
|
|
To elaborate: This is mostly relevant for a feature-branch workflow. Whenever a
|
|
feature branch is merged, it can usually be thrown away. As merging is usually
|
|
done on some remote code management platform (GitHub, GitLab, ...), this means
|
|
that you usually keep a branch around until it is merged into one of the "main"
|
|
branches (`master`, `main`, `develop`, ...)
|
|
|
|
Enable persistent branches by setting the following in the `grm.toml` in the
|
|
worktree root:
|
|
|
|
```toml
|
|
persistent_branches = [
|
|
"master",
|
|
"develop",
|
|
]
|
|
```
|
|
|
|
Note that setting persistent branches will disable any detection of "default"
|
|
branches. The first entry will be considered your repositories' default branch.
|
|
|
|
### Converting an existing repository
|
|
|
|
It is possible to convert an existing directory to a worktree setup, using `grm
|
|
wt convert`. This command has to be run in the root of the repository you want
|
|
to convert:
|
|
|
|
```
|
|
grm wt convert
|
|
[✔] Conversion successful
|
|
```
|
|
|
|
This command will refuse to run if you have any changes in your repository.
|
|
Commit them and try again!
|
|
|
|
Afterwards, the directory is empty, as there are no worktrees checked out yet.
|
|
Now you can use the usual commands to set up worktrees.
|
|
|
|
### Working with remotes
|
|
|
|
To fetch all remote references from all remotes in a worktree setup, you can
|
|
use the following command:
|
|
|
|
```
|
|
grm wt fetch
|
|
[✔] Fetched from all remotes
|
|
```
|
|
|
|
This is equivalent to running `git fetch --all` in any of the worktrees.
|
|
|
|
Often, you may want to pull all remote changes into your worktrees. For this,
|
|
use the `git pull` equivalent:
|
|
|
|
```
|
|
grm wt pull
|
|
[✔] master: Done
|
|
[✔] my-cool-branch: Done
|
|
```
|
|
|
|
This will refuse when there are local changes, or if the branch cannot be fast
|
|
forwarded. If you want to rebase your local branches, use the `--rebase` switch:
|
|
|
|
```
|
|
grm wt pull --rebase
|
|
[✔] master: Done
|
|
[✔] my-cool-branch: Done
|
|
```
|
|
|
|
As noted, this will fail if there are any local changes in your worktree. If you
|
|
want to stash these changes automatically before the pull (and unstash them
|
|
afterwards), use the `--stash` option.
|
|
|
|
This will rebase your changes onto the upstream branch. This is mainly helpful
|
|
for persistent branches that change on the remote side.
|
|
|
|
There is a similar rebase feature that rebases onto the **default** branch instead:
|
|
|
|
```
|
|
grm wt rebase
|
|
[✔] master: Done
|
|
[✔] my-cool-branch: Done
|
|
```
|
|
|
|
This is super helpful for feature branches. If you want to incorporate changes
|
|
made on the remote branches, use `grm wt rebase` and all your branches will
|
|
be up to date. If you want to also update to remote tracking branches in one go,
|
|
use the `--pull` flag, and `--rebase` if you want to rebase instead of aborting
|
|
on non-fast-forwards:
|
|
|
|
```
|
|
grm wt rebase --pull --rebase
|
|
[✔] master: Done
|
|
[✔] my-cool-branch: Done
|
|
```
|
|
|
|
"So, what's the difference between `pull --rebase` and `rebase --pull`? Why the
|
|
hell is there a `--rebase` flag in the `rebase` command?"
|
|
|
|
Yes, it's kind of weird. Remember that `pull` only ever updates each worktree
|
|
to their remote branch, if possible. `rebase` rabases onto the **default** branch
|
|
instead. The switches to `rebase` are just convenience, so you do not have to
|
|
run two commands.
|
|
|
|
* `rebase --pull` is the same as `pull` && `rebase`
|
|
* `rebase --pull --rebase` is the same as `pull --rebase` && `rebase`
|
|
|
|
I understand that the UX is not the most intuitive. If you can think of an
|
|
improvement, please let me know (e.g. via an GitHub issue)!
|
|
|
|
As with `pull`, `rebase` will also refuse to run when there are changes in your
|
|
worktree. And you can also use the `--stash` option to stash/unstash changes
|
|
automatically.
|
|
|
|
### Manual access
|
|
|
|
GRM isn't doing any magic, it's just git under the hood. If you need to have access
|
|
to the underlying git repository, you can always do this:
|
|
|
|
```
|
|
$ git --git-dir ./.git-main-working-tree [...]
|
|
```
|
|
|
|
This should never be required (whenever you have to do this, you can consider
|
|
this a bug in GRM and open an [issue](https://github.com/hakoerber/git-repo-manager/issues/new),
|
|
but it may help in a pinch.
|
|
|