feat(stack): add a command to delete a stack

Why

glab stack sync removes a stack's layers as their merge requests are merged or closed, but the stack's own directory under .git/stacked stays behind. glab stack switch builds its list by reading those directories, so a finished stack keeps showing up in the selector with nothing but a BASE_BRANCH file left in it.

There is no command for this, so the way out is to know the storage layout:

rm -r .git/stacked/<stack-name>

What

glab stack delete <stack-name> removes that directory. With no argument it offers the same picker as glab stack switch.

$ glab stack delete
? Choose a stack to delete: cool-new-feature
Stack cool-new-feature still has 2 layers.
Deleting a stack removes its local metadata. Branches and merge requests are not affected.

? Delete stack cool-new-feature? Yes!
Deleted stack cool-new-feature.

It asks first unless -y is passed, and says what is at stake before asking, since "delete" could reasonably be read as deleting the branches too. --yes matches the spelling used by securefile remove, mr merge and the other commands that guard a destructive step.

Deleting the stack you are currently on also unsets glab.currentstack. That is kept in Git config rather than in the stack directory, so it survives the delete and would otherwise name a stack that is no longer there.

Two helpers in internal/git/stacked.go:

  • UnsetLocalConfig, next to the existing SetLocalConfig.
  • RemoveStackRefDir, which rejects "", ., .. and any name containing a path separator. The name is already checked against GetStacks() before it gets there, but this is the one place that calls os.RemoveAll, and stacked/.. resolves to the .git directory itself.

The issue also suggests stack sync could delete a stack automatically after removing its final layer. I left that out. It changes what an existing command does to your metadata, and the explicit command covers abandoned stacks either way, which sync cannot detect. Happy to add it here if you would rather have both.

Verification

check result
go build ./... ok
go test ./internal/commands/stack/... ./internal/git/... ok, all packages
golangci-lint run on both trees 0 issues
make gen-docs then git status clean, generated pages committed

Ten command tests and two in internal/git. Every one of them runs under a context deadline set with cmd.SetContext, which is worth explaining: IOStreams.Confirm waits on input that never arrives rather than reading end of input, so a prompt that fires when it should not blocks the test binary until the suite is killed. Without the deadline the report reads panic: test timed out and never names the prompt. A non-TTY IOStreams does not help here.

Mutation-tested, each reverted afterwards:

mutation result
confirmation runs regardless of --yes 4 tests FAIL in 10s each, deleting stack failed: timeout
drop the name check in RemoveStackRefDir FAIL in 0.3s, name "" should be rejected
never unset glab.currentstack FAIL in 0.09s, glab.currentstack should have been unset

Closes #8534 (closed)

Edited by Maksym Tymoshyk

Merge request reports

Loading
Loading