commands/stack/reorder/stack_reorder.go (170 lines of code) (raw):
package reorder
import (
"errors"
"fmt"
"reflect"
"slices"
"strings"
"github.com/MakeNowJust/heredoc/v2"
"github.com/spf13/cobra"
gitlab "gitlab.com/gitlab-org/api/client-go"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/mr/mrutils"
"gitlab.com/gitlab-org/cli/internal/config"
"gitlab.com/gitlab-org/cli/internal/glrepo"
"gitlab.com/gitlab-org/cli/pkg/auth"
"gitlab.com/gitlab-org/cli/pkg/git"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"gitlab.com/gitlab-org/cli/pkg/text"
)
type Options struct {
LabClient *gitlab.Client
CurrentUser *gitlab.User
BaseRepo func() (glrepo.Interface, error)
Remotes func() (glrepo.Remotes, error)
Config func() (config.Config, error)
}
func NewCmdReorderStack(f *cmdutils.Factory, gr git.GitRunner, getText cmdutils.GetTextUsingEditor) *cobra.Command {
opts := &Options{
Remotes: f.Remotes,
Config: f.Config,
BaseRepo: f.BaseRepo,
}
stackSaveCmd := &cobra.Command{
Use: "reorder",
Short: `Reorder a stack of merge requests. (EXPERIMENTAL.)`,
Long: `Reorder how the current stack's merge requests are merged.
` + text.ExperimentalString,
Example: heredoc.Doc(`glab stack reorder`),
RunE: func(cmd *cobra.Command, args []string) error {
f.IO.StartSpinner("Reordering\n")
err := reorderFunc(f, getText, f.IO, opts)
if err != nil {
return fmt.Errorf("could not run stack reorder: %v", err)
}
f.IO.StopSpinner("%s Reordering complete\n", f.IO.Color().GreenCheck())
return nil
},
}
return stackSaveCmd
}
func reorderFunc(f *cmdutils.Factory, getText cmdutils.GetTextUsingEditor, iostream *iostreams.IOStreams, opts *Options) error {
iostream.StartSpinner("Reordering\n")
defer iostream.StopSpinner("")
title, err := git.GetCurrentStackTitle()
if err != nil {
return fmt.Errorf("error retrieving current stack title: %v", err)
}
ref, err := git.CurrentStackRefFromCurrentBranch(title)
if err != nil {
return fmt.Errorf("error checking for stack: %v", err)
}
stack, err := git.GatherStackRefs(title)
if err != nil {
return fmt.Errorf("error getting refs from file system: %v", err)
}
iostream.StopSpinner("")
// pausing the spinner in case it's a terminal based editor
branches, err := promptForOrder(f, getText, stack, ref.Branch)
if err != nil {
return fmt.Errorf("error getting new branch order: %v", err)
}
// resuming spinner
iostream.StartSpinner("Reordering\n")
updatedStack, err := matchBranchesToStack(stack, branches)
if err != nil {
return fmt.Errorf("error matching branches to stack: %v", err)
}
if reflect.DeepEqual(stack, updatedStack) {
return fmt.Errorf("no updates needed")
}
client, err := auth.GetAuthenticatedClient(f)
if err != nil {
return fmt.Errorf("error authorizing with GitLab: %v", err)
}
opts.LabClient = client
err = updateMRs(f, updatedStack, stack)
if err != nil {
return fmt.Errorf("error updating merge requests: %v", err)
}
iostream.StopSpinner("%s Reordering complete\n", f.IO.Color().GreenCheck())
return nil
}
func matchBranchesToStack(stack git.Stack, branches []string) (git.Stack, error) {
stackBranches := stack.Branches()
// need to clone the refs here so we don't modify the original stack
newStack := git.Stack{Title: stack.Title, Refs: make(map[string]git.StackRef)}
for index, branch := range branches {
// let's find a ref from the branch
ref, err := stack.RefFromBranch(branch)
if err != nil {
return git.Stack{}, fmt.Errorf("could not match branch to stack ref: %v", err)
}
var next string
var prev string
if index == 0 {
// first branch in the stack
prev = ""
} else {
// otherwise, get the previous ref
prevRef, err := stack.RefFromBranch(branches[index-1])
if err != nil {
return git.Stack{}, err
}
prev = prevRef.SHA
}
if index == len(branches)-1 {
// last branch in the stack
next = ""
} else {
// otherwise, get the next ref
nextRef, err := stack.RefFromBranch(branches[index+1])
if err != nil {
return git.Stack{}, err
}
next = nextRef.SHA
}
newRef := ref
newRef.Next = next
newRef.Prev = prev
newStack.Refs[newRef.SHA] = newRef
// update the stack file
err = git.UpdateStackRefFile(newStack.Title, newRef)
if err != nil {
return git.Stack{}, err
}
// and remove the branch from our list
stackBranches = slices.DeleteFunc(stackBranches,
func(branch string) bool {
return branch == ref.Branch
})
}
if len(stackBranches) > 0 {
return git.Stack{},
errors.New("missing one or more refs from the reordered list: " +
strings.Join(stackBranches, ", "))
}
return newStack, nil
}
func updateMRs(f *cmdutils.Factory, newStack git.Stack, oldStack git.Stack) error {
for _, ref := range newStack.Iter2() {
// if there is already an MR and the order has been adjusted
if ref.MR != "" &&
(ref.Next != oldStack.Refs[ref.SHA].Next ||
ref.Prev != oldStack.Refs[ref.SHA].Prev) {
client, err := f.HttpClient()
if err != nil {
return fmt.Errorf("error connecting to GitLab: %v", err)
}
mr, _, err := mrutils.MRFromArgsWithOpts(f, []string{ref.Branch}, nil, "opened")
if err != nil {
return fmt.Errorf("error getting merge request from GitLab: %v", err)
}
var previousBranch string
if ref.Prev == "" {
previousBranch, err = git.GetDefaultBranch(git.DefaultRemote)
if err != nil {
return fmt.Errorf("error getting default branch: %v", err)
}
} else {
previousBranch = newStack.Refs[ref.Prev].Branch
}
opts := gitlab.UpdateMergeRequestOptions{TargetBranch: &previousBranch}
_, err = api.UpdateMR(client, mr.ProjectID, mr.IID, &opts)
if err != nil {
return fmt.Errorf("error updating merge request on GitLab: %v", err)
}
}
}
return nil
}