feat(mr): add --worktree flag to mr checkout
Description
Reviewing a merge request with glab mr checkout switches the branch of the current working directory, so anything in progress has to be stashed or committed first, and switching back afterwards is on the reviewer. That is exactly what Git worktrees exist to avoid.
This MR adds --worktree <path> to glab mr checkout. Everything up to the checkout is unchanged: the merge request is resolved, the source branch is fetched into a local branch, and branch.<name>.remote and branch.<name>.merge are configured so the branch pushes back to the right place. The final step runs git worktree add <path> <branch> instead of git checkout <branch>.
git worktree add refuses a branch that is already checked out in any worktree, and skipping the fetch when the merge request head is already local made that the common case rather than an edge case. So validate() reads git worktree list --porcelain before anything has side effects. If the branch is already in the requested worktree, the command reports that and stops; if it is checked out somewhere else, it fails naming that path and points at --branch.
That guard is also what makes the divergence path safe. guardReset decided from the symbolic-ref of the process's own worktree, so a reviewer who happened to be standing on the merge request branch could get git reset --hard in the current directory, and otherwise git branch -f was refused for a branch held by another worktree. In worktree mode it now returns early: the branch is known to be checked out nowhere, so git branch -f moves the ref and the worktree add that follows picks up the new head. The confirmation prompt stays, since local commits are still discarded, but says the current directory is untouched.
--set-upstream-to names the branch explicitly: git branch --set-upstream-to <upstream> <branch>. This is required for worktree mode, where the current branch is not the one just checked out. It is not a bug fix. On the existing path git checkout <branch> runs immediately above with its error checked, so the current branch already was the target and the two-argument form was correct there.
-w is deliberately not claimed as a shorthand. It is --web in seven commands across the CLI, including the sibling mr view and mr create, so taking it here would make it mean something different from every other mr subcommand and rule out --web ever using it on mr checkout.
Failures from git worktree add, such as the path already existing, are surfaced with the path and branch named so the message is actionable.
Related Issues
Resolves #8217 (closed)
The issue also suggests a default path convention, for example ../mr-<id>, when --worktree is passed without a value. That part is deferred and not implemented here, so the issue can be closed on this scope or a follow-up opened for it. Cobra's mechanism for an optional value is NoOptDefVal, and setting it stops --worktree ../review-123 from parsing, leaving only --worktree=../review-123. That space-separated form is the syntax used in the issue's own example and throughout this MR, so the trade is not worth it. If the convention is wanted later, a second positional argument is probably a better fit.
How has this been tested?
Unit tests in internal/commands/mr/checkout/mr_checkout_test.go:
go test ./internal/commands/mr/checkout/ -run 'TestMrCheckout_Worktree|TestMrCheckout_SetUpstreamTo'TestMrCheckout_Worktree drives the mocked Git runner and asserts the exact commands issued:
--worktree ../review-mr-123runs the same fetch andbranch.*config as a normal checkout, thengit worktree add ../review-mr-123 feat-new-mrand nogit checkout.--worktree ../review-mr-123 --set-upstream-to upstream/mainfollows the worktree add withgit branch --set-upstream-to upstream/main feat-new-mr, naming the branch.- A failing
git worktree addreturnscould not add worktree "../review-mr-123" for branch "feat-new-mr", wrapping git's error. - Merge request head already local:
rev-parse --verify refs/heads/feat-new-mrreturns the head SHA, the fetch is skipped, and the worktree is still added. - Branch checked out in another worktree: the early error is returned, and the subtest carries no fetch or config expectations at all. gomock's strictness is what proves the guard runs before any side effects.
- Branch already in the requested worktree: the command reports it and stops without error.
- Divergence with
--worktree:git branch -f feat-new-mr FETCH_HEADis issued rather thangit reset --hard, andsymbolic-refis never consulted.
TestMrCheckout_SetUpstreamTo expects the branch name on the existing non-worktree path. The whole mr/checkout package passes, along with go vet and gofmt.
Manually, against a real repository:
- Make an uncommitted change in the current worktree.
glab mr checkout <iid> --worktree ../review-<iid>- The new directory contains the merge request branch checked out;
git worktree listshows both; the original directory is still on its branch with the uncommitted change intact. - Run the same command again: it reports that the branch is already checked out at that path and stops.
glab mr checkout <iid> --worktree ../elsewhere: fails naming../review-<iid>and suggesting--branch, without fetching.- Without
--worktree,glab mr checkout <iid>still switches the current directory as before.
Differences
Before: glab mr checkout always ran git checkout <branch> in the current working directory. Reviewing meant stashing or committing first and switching back afterwards.
After: With --worktree <path>, the merge request branch is fetched and configured exactly as before, then checked out into a new worktree at <path>, leaving the current directory untouched. That now holds on the divergence path too, which moves the branch ref instead of resetting a working tree. If the branch is already checked out elsewhere, the command says where before doing anything. Without the flag, behaviour is unchanged apart from --set-upstream-to naming its branch, which is equivalent on that path.
Checklist
- Flag registered as
--worktreeonly, with no shorthand, and a help string matching the other flags on the command. - The branch is checked for an existing worktree in
validate(), before the fetch and thebranch.*config writes. - Divergence in worktree mode moves the branch ref rather than resetting any working tree, and the prompt says so.
-
--set-upstream-tonames the branch, which worktree mode requires and the existing path is unaffected by. - Errors from
git worktree addare wrapped with the path and branch so the message is actionable. - Default behaviour (no
--worktree) is unchanged. - Long description covers the one-worktree-per-branch rule and
git worktree remove <path>cleanup, regenerated intodocs/source/mr/checkout.md. - Unit tests cover the worktree add, upstream by name, the failure path, the head-already-local and already-checked-out guards, and divergence resolving to
branch -f. - Commit follows the conventional commit format used in this repository.
- No breaking change; no changelog entry needed beyond the conventional commit type.