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 }