Module grm::worktree

source ·
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 and remote2/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 selected remote2/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:

  1. The parameters passed to grm, either via command line or via configuration file
  2. 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

§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?

Constants§

Functions§