Expand description
This handles worktrees for repositories. Some considerations to take care of:
- Which branch to check out / create
- Which commit to check out
- Whether to track a remote branch, and which
There are a general rules. The main goal is to do the least surprising thing in each situation, and to never change existing setups (e.g. tracking, branch states) except when explicitly told to. In 99% of all cases, the workflow will be quite straightforward.
- The name of the worktree (and therefore the path) is always the same as the name of the branch.
- Never modify existing local branches
- Only modify tracking branches for existing local branches if explicitly requested
- By default, do not do remote operations. This means that we do no do any tracking setup (but of course, the local branch can already have a tracking branch set up, which will just be left alone)
- Be quite lax with finding a remote tracking branch (as using an existing branch is most likely preferred to creating a new branch)
There are a few different options that can be given:
- Explicit track (
--track
) and explicit no-track (--no-track
) - A configuration may specify to enable tracking a remote branch by default
- A configuration may specify a prefix for remote branches
§How to handle the local branch?
That one is easy: If a branch with the desired name already exists, all is well. If not, we create a new one.
§Which commit should be checked out?
The most imporant rule: If the local branch already existed, just leave it as it is. Only if a new branch is created do we need to answer the question which commit to set it to. Generally, we set the branch to whatever the “default” branch of the repository is (something like “main” or “master”). But there are a few cases where we can use remote branches to make the result less surprising.
First, if tracking is explicitly disabled, we still try to guess! But we
do ignore --track
, as this is how it’s done everywhere else.
As an example: If origin/foobar
exists and we run grm worktree add foobar --no-track
, we create a new worktree called foobar
that’s on the same
state as origin/foobar
(but we will not set up tracking, see below).
If tracking is explicitly requested to a certain state, we use that remote branch. If it exists, easy. If not, no more guessing!
Now, it’s important to select the correct remote. In the easiest case, there
is only one remote, so we just use that one. If there is more than one
remote, we check whether there is a default remote configured via
track.default_remote
. If yes, we use that one. If not, we have to do the
selection process below for each of them. If only one of them returns
some branch to track, we use that one. If more than one remote returns
information, we only use it if it’s identical for each. Otherwise we bail,
as there is no point in guessing.
The commit selection process looks like this:
-
If a prefix is specified in the configuration, we look for
{remote}/{prefix}/{worktree_name}
-
We look for
{remote}/{worktree_name}
(yes, this means that even when a prefix is configured, we use a branch without a prefix if one with prefix does not exist)
Note that we may select different branches for different remotes when prefixes is used. If remote1 has a branch with a prefix and remote2 only has a branch without a prefix, we select them both when a prefix is used. This could lead to the following situation:
- There is
origin/prefix/foobar
andremote2/foobar
, with different states - You set
track.default_prefix = "prefix"
(and no default remote!) - You run
grm worktree add
prefix/foobar` - Instead of just picking
origin/prefix/foobar
, grm will complain because it also selectedremote2/foobar
.
This is just emergent behavior of the logic above. Fixing it would require additional logic for that edge case. I assume that it’s just so rare to get that behavior that it’s acceptable for now.
Now we either have a commit, we aborted, or we do not have commit. In the last case, as stated above, we check out the “default” branch.
§The remote tracking branch
First, the only remote operations we do is branch creation! It’s
unfortunately not possible to defer remote branch creation until the first
git push
, which would be ideal. The remote tracking branch has to already
exist, so we have to do the equivalent of git push --set-upstream
during
worktree creation.
Whether (and which) remote branch to track works like this:
-
If
--no-track
is given, we never track a remote branch, except when branch already has a tracking branch. So we’d be done already! -
If
--track
is given, we always track this branch, regardless of anything else. If the branch exists, cool, otherwise we create it.
If neither is given, we only set up tracking if requested in the
configuration file (track.default = true
)
The rest of the process is similar to the commit selection above. The only
difference is the remote selection. If there is only one, we use it, as
before. Otherwise, we try to use default_remote
from the configuration, if
available. If not, we do not set up a remote tracking branch. It works like
this:
-
If a prefix is specified in the configuration, we use
{remote}/{prefix}/{worktree_name}
-
If no prefix is specified in the configuration, we use
{remote}/{worktree_name}
Now that we have a remote, we use the same process as above:
- If a prefix is specified in the configuration, we use for
{remote}/{prefix}/{worktree_name}
- We use for
{remote}/{worktree_name}
All this means that in some weird situation, you may end up with the state of a remote branch while not actually tracking that branch. This can only happen in repositories with more than one remote. Imagine the following:
The repository has two remotes (remote1
and remote2
) which have the
exact same remote state. But there is no default_remote
in the
configuration (or no configuration at all). There is a remote branch
foobar
. As both remote1/foobar
and remote2/foobar
as the same, the new
worktree will use that as the state of the new branch. But as grm
cannot
tell which remote branch to track, it will not set up remote tracking. This
behavior may be a bit confusing, but first, there is no good way to resolve
this, and second, the situation should be really rare (when having multiple
remotes, you would generally have a default_remote
configured).
§Implementation
To reduce the chance of bugs, the implementation uses the typestate pattern. Here are the states we are moving through linearily:
- Init
- A local branch name is set
- A local commit to set the new branch to is selected
- A remote tracking branch is selected
- The new branch is created with all the required settings
Don’t worry about the lifetime stuff: There is only one single lifetime, as everything (branches, commits) is derived from the single repo::Repo instance
§Testing
There are two types of input to the tests:
- The parameters passed to
grm
, either via command line or via configuration file - The circumstances in the repository and remotes
§Parameters
- The name of the worktree
- Whether it contains slashes or not
- Whether it is invalid
--track
and--no-track
- Whether there is a configuration file and what it contains
- Whether
track.default
is enabled or disabled - Whether
track.default_remote_prefix
is there or missing - Whether
track.default_remote
is there or missing- Whether that remote exists or not
- Whether
§Situations
§The local branch
- Whether the branch already exists
- Whether the branch has a remote tracking branch and whether it differs
from the desired tracking branch (i.e.
--track
or config)
§Remotes
- How many remotes there are, if any
- If more than two remotes exist, whether their desired tracking branch differs
§The remote tracking branch branch
- Whether a remote branch with the same name as the worktree exists
- Whether a remote branch with the same name as the worktree plus prefix exists
§Outcomes
We have to check the following afterwards:
- Does the worktree exist in the correct location?
- Does the local branch have the same name as the worktree?
- Does the local branch have the correct commit?
- Does the local branch track the correct remote branch?
- Does that remote branch also exist?