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

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  
     8  	"github.com/MakeNowJust/heredoc/v2"
     9  	"github.com/rsteube/carapace"
    10  	"github.com/spf13/cobra"
    11  	"github.com/spf13/pflag"
    12  	gitlab "github.com/xanzy/go-gitlab"
    13  	"github.com/zaquestion/lab/internal/action"
    14  	lab "github.com/zaquestion/lab/internal/gitlab"
    15  )
    16  
    17  var issueEditCmd = &cobra.Command{
    18  	Use:     "edit [remote] <id>[:<comment_id>]",
    19  	Aliases: []string{"update"},
    20  	Short:   "Edit or update an issue",
    21  	Example: heredoc.Doc(`
    22  		lab issue edit 14
    23  		lab issue edit 14:2065489
    24  		lab issue edit 14 -a johndoe --unassign jackdoe
    25  		lab issue edit 14 -m "new title"
    26  		lab issue edit 14 -m "new title" -m "new desc"
    27  		lab issue edit 14 -l new_label --unlabel old_label
    28  		lab issue edit --milestone "NewYear"
    29  		lab issue edit --force-linebreak
    30  		lab issue edit --delete-note 14:2065489`),
    31  	Args:             cobra.MinimumNArgs(1),
    32  	PersistentPreRun: labPersistentPreRun,
    33  	Run: func(cmd *cobra.Command, args []string) {
    34  
    35  		rn, idString, err := parseArgsRemoteAndProject(args)
    36  		if err != nil {
    37  			log.Fatal(err)
    38  		}
    39  
    40  		var (
    41  			issueNum   int = 0
    42  			commentNum int = 0
    43  		)
    44  
    45  		if strings.Contains(idString, ":") {
    46  			ids := strings.Split(idString, ":")
    47  			issueNum, _ = strconv.Atoi(ids[0])
    48  			commentNum, _ = strconv.Atoi(ids[1])
    49  		} else {
    50  			issueNum, _ = strconv.Atoi(idString)
    51  		}
    52  
    53  		issue, err := lab.IssueGet(rn, issueNum)
    54  		if err != nil {
    55  			log.Fatal(err)
    56  		}
    57  
    58  		deleteNote, err := cmd.Flags().GetBool("delete-note")
    59  		if err != nil {
    60  			log.Fatal(err)
    61  		}
    62  		if deleteNote {
    63  			discussions, err := lab.IssueListDiscussions(rn, int(issueNum))
    64  			if err != nil {
    65  				log.Fatal(err)
    66  			}
    67  
    68  			discussion := ""
    69  		findDiscussionID:
    70  			for _, d := range discussions {
    71  				for _, n := range d.Notes {
    72  					if n.ID == commentNum {
    73  						discussion = d.ID
    74  						break findDiscussionID
    75  					}
    76  				}
    77  			}
    78  
    79  			// delete the note
    80  			err = lab.IssueDeleteNote(rn, issueNum, discussion, commentNum)
    81  			if err != nil {
    82  				log.Fatal(err)
    83  			}
    84  			return
    85  		}
    86  
    87  		linebreak, err := cmd.Flags().GetBool("force-linebreak")
    88  		if err != nil {
    89  			log.Fatal(err)
    90  		}
    91  
    92  		// Edit a comment on the Issue
    93  		if commentNum != 0 {
    94  			replyNote(rn, false, issueNum, commentNum, true, false, "", linebreak, false, nil)
    95  			return
    96  		}
    97  
    98  		var labelsChanged bool
    99  		// get the labels to add
   100  		addLabelTerms, err := cmd.Flags().GetStringSlice("label")
   101  		if err != nil {
   102  			log.Fatal(err)
   103  		}
   104  		addLabels, err := mapLabels(rn, addLabelTerms)
   105  		if err != nil {
   106  			log.Fatal(err)
   107  		}
   108  		if len(addLabels) > 0 {
   109  			labelsChanged = true
   110  		}
   111  
   112  		// get the labels to remove
   113  		rmLabelTerms, err := cmd.Flags().GetStringSlice("unlabel")
   114  		if err != nil {
   115  			log.Fatal(err)
   116  		}
   117  		rmLabels, err := mapLabels(rn, rmLabelTerms)
   118  		if err != nil {
   119  			log.Fatal(err)
   120  		}
   121  		if len(rmLabels) > 0 {
   122  			labelsChanged = true
   123  		}
   124  
   125  		// get the assignees to add
   126  		assignees, err := cmd.Flags().GetStringSlice("assign")
   127  		if err != nil {
   128  			log.Fatal(err)
   129  		}
   130  
   131  		// get the assignees to remove
   132  		unassignees, err := cmd.Flags().GetStringSlice("unassign")
   133  		if err != nil {
   134  			log.Fatal(err)
   135  		}
   136  
   137  		currentAssignees := issueGetCurrentAssignees(issue)
   138  		assigneeIDs, assigneesChanged, err := getUpdateUsers(currentAssignees, assignees, unassignees)
   139  		if err != nil {
   140  			log.Fatal(err)
   141  		}
   142  
   143  		milestoneName, err := cmd.Flags().GetString("milestone")
   144  		if err != nil {
   145  			log.Fatal(err)
   146  		}
   147  		updateMilestone := cmd.Flags().Lookup("milestone").Changed
   148  		milestoneID := -1
   149  
   150  		if milestoneName != "" {
   151  			ms, err := lab.MilestoneGet(rn, milestoneName)
   152  			if err != nil {
   153  				log.Fatal(err)
   154  			}
   155  			milestoneID = ms.ID
   156  		}
   157  
   158  		// get all of the "message" flags
   159  		msgs, err := cmd.Flags().GetStringArray("message")
   160  		if err != nil {
   161  			log.Fatal(err)
   162  		}
   163  
   164  		title := issue.Title
   165  		body := issue.Description
   166  
   167  		// We only consider opening the editor to edit the title and body on
   168  		// -m, when --force-linebreak is used alone, or when no other flag is
   169  		// passed. However, it's common to set --force-linebreak through the
   170  		// config file, so we need to check if it's being set through the CLI
   171  		// or config file.
   172  		var openEditor bool
   173  		if len(msgs) > 0 || cmd.Flags().NFlag() == 0 {
   174  			openEditor = true
   175  		} else if linebreak && cmd.Flags().NFlag() == 1 {
   176  			cmd.Flags().Visit(func(f *pflag.Flag) {
   177  				if f.Name == "force-linebreak" {
   178  					openEditor = true
   179  					return
   180  				}
   181  			})
   182  		}
   183  
   184  		if openEditor {
   185  			title, body, err = editDescription(issue.Title, issue.Description, msgs, "")
   186  			if err != nil {
   187  				log.Fatal(err)
   188  			}
   189  			if title == "" {
   190  				log.Fatal("aborting: empty issue title")
   191  			}
   192  
   193  			if linebreak {
   194  				body = textToMarkdown(body)
   195  			}
   196  		}
   197  
   198  		abortUpdate := title == issue.Title && body == issue.Description && !labelsChanged && !assigneesChanged && !updateMilestone
   199  		if abortUpdate {
   200  			log.Fatal("aborting: no changes")
   201  		}
   202  
   203  		opts := &gitlab.UpdateIssueOptions{
   204  			Title:       &title,
   205  			Description: &body,
   206  		}
   207  
   208  		if labelsChanged {
   209  			// empty arrays are just ignored
   210  			opts.AddLabels = addLabels
   211  			opts.RemoveLabels = rmLabels
   212  		}
   213  
   214  		if assigneesChanged {
   215  			opts.AssigneeIDs = assigneeIDs
   216  		}
   217  
   218  		if updateMilestone {
   219  			opts.MilestoneID = &milestoneID
   220  		}
   221  
   222  		issueURL, err := lab.IssueUpdate(rn, issueNum, opts)
   223  		if err != nil {
   224  			log.Fatal(err)
   225  		}
   226  		fmt.Println(issueURL)
   227  	},
   228  }
   229  
   230  // issueGetCurrentAssignees returns a string slice of the current assignees'
   231  // usernames
   232  func issueGetCurrentAssignees(issue *gitlab.Issue) []string {
   233  	currentAssignees := make([]string, len(issue.Assignees))
   234  	if len(issue.Assignees) > 0 && issue.Assignees[0].Username != "" {
   235  		for i, a := range issue.Assignees {
   236  			currentAssignees[i] = a.Username
   237  		}
   238  	}
   239  	return currentAssignees
   240  }
   241  
   242  func init() {
   243  	issueEditCmd.Flags().StringArrayP("message", "m", []string{}, "use the given <msg>; multiple -m are concatenated as separate paragraphs")
   244  	issueEditCmd.Flags().StringSliceP("label", "l", []string{}, "add the given label(s) to the issue")
   245  	issueEditCmd.Flags().StringSliceP("unlabel", "", []string{}, "remove the given label(s) from the issue")
   246  	issueEditCmd.Flags().StringSliceP("assign", "a", []string{}, "add an assignee by username")
   247  	issueEditCmd.Flags().StringSliceP("unassign", "", []string{}, "remove an assignee by username")
   248  	issueEditCmd.Flags().String("milestone", "", "set milestone")
   249  	issueEditCmd.Flags().Bool("force-linebreak", false, "append 2 spaces to the end of each line to force markdown linebreaks")
   250  	issueEditCmd.Flags().Bool("delete-note", false, "delete the given note; must be provided in <issueID>:<noteID> format")
   251  	issueEditCmd.Flags().SortFlags = false
   252  
   253  	issueCmd.AddCommand(issueEditCmd)
   254  
   255  	carapace.Gen(issueEditCmd).FlagCompletion(carapace.ActionMap{
   256  		"label": carapace.ActionMultiParts(",", func(c carapace.Context) carapace.Action {
   257  			project, _, err := parseArgsRemoteAndProject(c.Args)
   258  			if err != nil {
   259  				return carapace.ActionMessage(err.Error())
   260  			}
   261  			return action.Labels(project).Invoke(c).Filter(c.Parts).ToA()
   262  		}),
   263  		"milestone": carapace.ActionCallback(func(c carapace.Context) carapace.Action {
   264  			project, _, err := parseArgsRemoteAndProject(c.Args)
   265  			if err != nil {
   266  				return carapace.ActionMessage(err.Error())
   267  			}
   268  			return action.Milestones(project, action.MilestoneOpts{Active: true})
   269  		}),
   270  	})
   271  
   272  	carapace.Gen(issueEditCmd).PositionalCompletion(
   273  		action.Remotes(),
   274  		action.Issues(issueList),
   275  	)
   276  }