github.com/zaquestion/lab@v0.25.1/cmd/mr_edit.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strings"
     7  
     8  	"github.com/MakeNowJust/heredoc/v2"
     9  	"github.com/pkg/errors"
    10  	"github.com/rsteube/carapace"
    11  	"github.com/spf13/cobra"
    12  	"github.com/spf13/pflag"
    13  	gitlab "github.com/xanzy/go-gitlab"
    14  	"github.com/zaquestion/lab/internal/action"
    15  	lab "github.com/zaquestion/lab/internal/gitlab"
    16  )
    17  
    18  var mrEditCmd = &cobra.Command{
    19  	Use:     "edit [remote] <id>[:<comment_id>]",
    20  	Aliases: []string{"update"},
    21  	Short:   "Edit or update an MR",
    22  	Example: heredoc.Doc(`
    23  		lab mr edit 2
    24  		lab mr edit 2:5684032
    25  		lab mr edit 3 remote -m "new title"
    26  		lab mr edit 5 upstream -m "new title" -m "new desc"
    27  		lab mr edit 7 -l new_label --unlabel old_label
    28  		lab mr edit 11 upstream -a johndoe -a janedoe
    29  		lab mr edit 17 upstream --unassign johndoe
    30  		lab mr edit 13 upstream --milestone "summer"
    31  		lab mr edit 19 origin --target-brnch other_brnch
    32  		lab mr edit 23 upstream -F test_file
    33  		lab mr edit 29 upstream -F test_file --force-linebreak
    34  		lab mr edit 31 upstream --draft
    35  		lab mr edit 37 upstream --ready
    36  		lab mr edit 41 upstream -r johndoe -r janedoe
    37  		lab mr edit 43 upstream --unreview johndoe
    38  		lab mr edit --delete-note 2:5684032`),
    39  	PersistentPreRun: labPersistentPreRun,
    40  	Run: func(cmd *cobra.Command, args []string) {
    41  		commentNum, branchArgs, err := filterCommentArg(args)
    42  		if err != nil {
    43  			log.Fatal(err)
    44  		}
    45  
    46  		rn, id, err := parseArgsWithGitBranchMR(branchArgs)
    47  		if err != nil {
    48  			log.Fatal(err)
    49  		}
    50  		mrNum := int(id)
    51  
    52  		if mrNum == 0 {
    53  			fmt.Println("Error: Cannot determine MR id.")
    54  			os.Exit(1)
    55  		}
    56  
    57  		mr, err := lab.MRGet(rn, mrNum)
    58  		if err != nil {
    59  			log.Fatal(err)
    60  		}
    61  
    62  		deleteNote, err := cmd.Flags().GetBool("delete-note")
    63  		if err != nil {
    64  			log.Fatal(err)
    65  		}
    66  		if deleteNote {
    67  			discussions, err := lab.MRListDiscussions(rn, int(mrNum))
    68  			if err != nil {
    69  				log.Fatal(err)
    70  			}
    71  
    72  			discussion := ""
    73  		findDiscussionID:
    74  			for _, d := range discussions {
    75  				for _, n := range d.Notes {
    76  					if n.ID == commentNum {
    77  						discussion = d.ID
    78  						break findDiscussionID
    79  					}
    80  				}
    81  			}
    82  
    83  			// delete the note
    84  			err = lab.MRDeleteNote(rn, mrNum, discussion, commentNum)
    85  			if err != nil {
    86  				log.Fatal(err)
    87  			}
    88  			return
    89  		}
    90  
    91  		linebreak, err := cmd.Flags().GetBool("force-linebreak")
    92  		if err != nil {
    93  			log.Fatal(err)
    94  		}
    95  
    96  		// Edit a comment on the MR
    97  		if commentNum != 0 {
    98  			replyNote(rn, true, mrNum, commentNum, true, true, "", linebreak, false, nil)
    99  			return
   100  		}
   101  
   102  		var labelsChanged bool
   103  		// get the labels to add
   104  		addLabelTerms, err := cmd.Flags().GetStringSlice("label")
   105  		if err != nil {
   106  			log.Fatal(err)
   107  		}
   108  		addLabels, err := mapLabels(rn, addLabelTerms)
   109  		if err != nil {
   110  			log.Fatal(err)
   111  		}
   112  		if len(addLabels) > 0 {
   113  			labelsChanged = true
   114  		}
   115  
   116  		// get the labels to remove
   117  		rmLabelTerms, err := cmd.Flags().GetStringSlice("unlabel")
   118  		if err != nil {
   119  			log.Fatal(err)
   120  		}
   121  		rmLabels, err := mapLabels(rn, rmLabelTerms)
   122  		if err != nil {
   123  			log.Fatal(err)
   124  		}
   125  		if len(rmLabels) > 0 {
   126  			labelsChanged = true
   127  		}
   128  
   129  		// get the assignees to add
   130  		assignees, err := cmd.Flags().GetStringSlice("assign")
   131  		if err != nil {
   132  			log.Fatal(err)
   133  		}
   134  
   135  		// get the assignees to remove
   136  		unassignees, err := cmd.Flags().GetStringSlice("unassign")
   137  		if err != nil {
   138  			log.Fatal(err)
   139  		}
   140  
   141  		// get the reviewers to add
   142  		reviewers, err := cmd.Flags().GetStringSlice("review")
   143  		if err != nil {
   144  			log.Fatal(err)
   145  		}
   146  
   147  		// get the reviewers to remove
   148  		unreviewers, err := cmd.Flags().GetStringSlice("unreview")
   149  		if err != nil {
   150  			log.Fatal(err)
   151  		}
   152  
   153  		filename, err := cmd.Flags().GetString("file")
   154  		if err != nil {
   155  			log.Fatal(err)
   156  		}
   157  
   158  		draft, err := cmd.Flags().GetBool("draft")
   159  		if err != nil {
   160  			log.Fatal(err)
   161  		}
   162  
   163  		ready, err := cmd.Flags().GetBool("ready")
   164  		if err != nil {
   165  			log.Fatal(err)
   166  		}
   167  
   168  		if draft && ready {
   169  			log.Fatal("--draft and --ready cannot be used together")
   170  		}
   171  
   172  		currentAssignees := mrGetCurrentAssignees(mr)
   173  		assigneeIDs, assigneesChanged, err := getUpdateUsers(currentAssignees, assignees, unassignees)
   174  		if err != nil {
   175  			log.Fatal(err)
   176  		}
   177  
   178  		currentReviewers := mrGetCurrentReviewers(mr)
   179  		reviewerIDs, reviewersChanged, err := getUpdateUsers(currentReviewers, reviewers, unreviewers)
   180  		if err != nil {
   181  			log.Fatal(err)
   182  		}
   183  
   184  		milestoneName, err := cmd.Flags().GetString("milestone")
   185  		if err != nil {
   186  			log.Fatal(err)
   187  		}
   188  		updateMilestone := cmd.Flags().Lookup("milestone").Changed
   189  		milestoneID := -1
   190  
   191  		if milestoneName != "" {
   192  			ms, err := lab.MilestoneGet(rn, milestoneName)
   193  			if err != nil {
   194  				log.Fatal(err)
   195  			}
   196  			milestoneID = ms.ID
   197  		}
   198  
   199  		targetBranchName, err := cmd.Flags().GetString("target-branch")
   200  		if err != nil {
   201  			log.Fatal(err)
   202  		}
   203  
   204  		targetBranchChanged := false
   205  		if targetBranchName != "" {
   206  			targetBranchName, err = getBranchName(rn, targetBranchName)
   207  			if err != nil {
   208  				log.Fatal(err)
   209  			}
   210  
   211  			if targetBranchName != mr.TargetBranch {
   212  				targetBranchChanged = true
   213  			}
   214  		}
   215  
   216  		// get all of the "message" flags
   217  		msgs, err := cmd.Flags().GetStringSlice("message")
   218  		if err != nil {
   219  			log.Fatal(err)
   220  		}
   221  
   222  		title := mr.Title
   223  		body := mr.Description
   224  
   225  		if len(msgs) > 0 && filename != "" {
   226  			log.Fatal("option -F cannot be combined with -m")
   227  		}
   228  
   229  		// We only consider opening the editor to edit the title and body on
   230  		// -m, -F, when --force-linebreak is used alone, or when no other flag
   231  		// is passed. However, it's common to set --force-linebreak through the
   232  		// config file, so we need to check if it's being set through the CLI
   233  		// or config file.
   234  		var openEditor bool
   235  		if len(msgs) > 0 || filename != "" || cmd.Flags().NFlag() == 0 {
   236  			openEditor = true
   237  		} else if linebreak && cmd.Flags().NFlag() == 1 {
   238  			cmd.Flags().Visit(func(f *pflag.Flag) {
   239  				if f.Name == "force-linebreak" {
   240  					openEditor = true
   241  					return
   242  				}
   243  			})
   244  		}
   245  
   246  		if openEditor {
   247  			title, body, err = editDescription(mr.Title, mr.Description, msgs, filename)
   248  			if err != nil {
   249  				log.Fatal(err)
   250  			}
   251  			if title == "" {
   252  				log.Fatal("aborting: empty mr title")
   253  			}
   254  
   255  			if linebreak {
   256  				body = textToMarkdown(body)
   257  			}
   258  		}
   259  
   260  		isWIP := hasPrefix(title, "wip:") ||
   261  			hasPrefix(title, "[wip]")
   262  		isDraft := hasPrefix(title, "draft:") ||
   263  			hasPrefix(title, "[draft]") ||
   264  			hasPrefix(title, "(draft)")
   265  
   266  		if ready {
   267  			if isWIP {
   268  				if title[0] == '[' {
   269  					title = strings.TrimPrefix(title, title[0:5])
   270  				} else {
   271  					title = strings.TrimPrefix(title, title[0:4])
   272  				}
   273  			} else if isDraft {
   274  				if title[0] == '(' || title[0] == '[' {
   275  					title = strings.TrimPrefix(title, title[0:7])
   276  				} else {
   277  					title = strings.TrimPrefix(title, title[0:6])
   278  				}
   279  			}
   280  		}
   281  
   282  		if draft {
   283  			if isWIP {
   284  				log.Fatal("the use of \"WIP\" terminology is deprecated, use \"Draft\" instead")
   285  			}
   286  
   287  			if !isDraft {
   288  				title = "Draft: " + title
   289  			}
   290  		}
   291  
   292  		abortUpdate := (title == mr.Title && body == mr.Description &&
   293  			!labelsChanged && !assigneesChanged && !updateMilestone &&
   294  			!targetBranchChanged && !reviewersChanged)
   295  		if abortUpdate {
   296  			log.Fatal("aborting: no changes")
   297  		}
   298  
   299  		opts := &gitlab.UpdateMergeRequestOptions{
   300  			Title:       &title,
   301  			Description: &body,
   302  		}
   303  
   304  		if labelsChanged {
   305  			// empty arrays are just ignored
   306  			opts.AddLabels = addLabels
   307  			opts.RemoveLabels = rmLabels
   308  		}
   309  
   310  		if assigneesChanged {
   311  			opts.AssigneeIDs = assigneeIDs
   312  		}
   313  
   314  		if reviewersChanged {
   315  			opts.ReviewerIDs = reviewerIDs
   316  		}
   317  
   318  		if updateMilestone {
   319  			opts.MilestoneID = &milestoneID
   320  		}
   321  
   322  		if targetBranchChanged {
   323  			opts.TargetBranch = &targetBranchName
   324  		}
   325  
   326  		mrURL, err := lab.MRUpdate(rn, int(mrNum), opts)
   327  		if err != nil {
   328  			log.Fatal(err)
   329  		}
   330  		fmt.Println(mrURL)
   331  	},
   332  }
   333  
   334  // mrGetCurrentAssignees returns a string slice of the current assignees'
   335  // usernames
   336  func mrGetCurrentAssignees(mr *gitlab.MergeRequest) []string {
   337  	currentAssignees := make([]string, len(mr.Assignees))
   338  	if len(mr.Assignees) > 0 && mr.Assignees[0].Username != "" {
   339  		for i, a := range mr.Assignees {
   340  			currentAssignees[i] = a.Username
   341  		}
   342  	}
   343  	return currentAssignees
   344  }
   345  
   346  // mrGetCurrentReviewers returns a string slice of the current reviewers'
   347  // usernames
   348  func mrGetCurrentReviewers(mr *gitlab.MergeRequest) []string {
   349  	currentReviewers := make([]string, len(mr.Reviewers))
   350  	if len(mr.Reviewers) > 0 && mr.Reviewers[0].Username != "" {
   351  		for i, a := range mr.Reviewers {
   352  			currentReviewers[i] = a.Username
   353  		}
   354  	}
   355  	return currentReviewers
   356  }
   357  
   358  // getBranchName considers the possible ambiguity of different branch names
   359  func getBranchName(project, branch string) (string, error) {
   360  	opts := &gitlab.ListBranchesOptions{
   361  		Search: &branch,
   362  	}
   363  
   364  	projectBranches, err := lab.BranchList(project, opts)
   365  	if err != nil {
   366  		return "", err
   367  	}
   368  
   369  	// Branch API accepts a search parameter, so we may get the answer
   370  	// right away, however, the search term may match as a substring, so we
   371  	// also need to check for multiple branch names and their ambiguity
   372  	var match string
   373  
   374  	switch len(projectBranches) {
   375  	case 0:
   376  		return "", errors.Errorf("Branch '%s' not found\n", branch)
   377  	case 1:
   378  		match = projectBranches[0].Name
   379  	default:
   380  		branchNames := make([]string, len(projectBranches))
   381  		for _, branch := range projectBranches {
   382  			branchNames = append(branchNames, branch.Name)
   383  		}
   384  
   385  		// Handle term ambiguity for multiple matched branch names
   386  		matches, err := matchTerms([]string{branch}, branchNames)
   387  		if err != nil {
   388  			return "", errors.Errorf("Branch %s\n", err.Error())
   389  		}
   390  
   391  		// we only asked for a single term
   392  		match = matches[0]
   393  	}
   394  
   395  	return match, nil
   396  }
   397  
   398  func init() {
   399  	mrEditCmd.Flags().StringSliceP("message", "m", []string{}, "use the given <msg>; multiple -m are concatenated as separate paragraphs")
   400  	mrEditCmd.Flags().StringSliceP("label", "l", []string{}, "add the given label(s) to the merge request")
   401  	mrEditCmd.Flags().StringSliceP("unlabel", "", []string{}, "remove the given label(s) from the merge request")
   402  	mrEditCmd.Flags().StringSliceP("assign", "a", []string{}, "add an assignee by username")
   403  	mrEditCmd.Flags().StringSliceP("unassign", "", []string{}, "remove an assignee by username")
   404  	mrEditCmd.Flags().String("milestone", "", "set milestone")
   405  	mrEditCmd.Flags().StringP("target-branch", "t", "", "edit MR target branch")
   406  	mrEditCmd.Flags().StringP("file", "F", "", "use the given file as the description")
   407  	mrEditCmd.Flags().Bool("force-linebreak", false, "append 2 spaces to the end of each line to force markdown linebreaks")
   408  	mrEditCmd.Flags().Bool("draft", false, "mark the merge request as draft")
   409  	mrEditCmd.Flags().Bool("ready", false, "mark the merge request as ready")
   410  	mrEditCmd.Flags().StringSliceP("review", "r", []string{}, "add an reviewer by username")
   411  	mrEditCmd.Flags().StringSliceP("unreview", "", []string{}, "remove an reviewer by username")
   412  	mrEditCmd.Flags().Bool("delete-note", false, "delete the given note; must be provided in <mrID>:<noteID> format")
   413  	mrEditCmd.Flags().SortFlags = false
   414  
   415  	mrCmd.AddCommand(mrEditCmd)
   416  
   417  	carapace.Gen(mrEditCmd).FlagCompletion(carapace.ActionMap{
   418  		"label": carapace.ActionMultiParts(",", func(c carapace.Context) carapace.Action {
   419  			project, _, err := parseArgsRemoteAndProject(c.Args)
   420  			if err != nil {
   421  				return carapace.ActionMessage(err.Error())
   422  			}
   423  			return action.Labels(project).Invoke(c).Filter(c.Parts).ToA()
   424  
   425  		}),
   426  		"milestone": carapace.ActionCallback(func(c carapace.Context) carapace.Action {
   427  			project, _, err := parseArgsRemoteAndProject(c.Args)
   428  			if err != nil {
   429  				return carapace.ActionMessage(err.Error())
   430  			}
   431  			return action.Milestones(project, action.MilestoneOpts{Active: true})
   432  		}),
   433  	})
   434  
   435  	carapace.Gen(mrEditCmd).PositionalCompletion(
   436  		action.Remotes(),
   437  		action.MergeRequests(mrList),
   438  	)
   439  }