rickub/clipublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/rickub/cli.git
git clone ssh://git@rickub.com/rickub/cli.git
issue.go · 445 lines · 10.6 KBGo Blame HistoryRaw
Initial import of the rickub CLI as a standalone public project 1a1d430 Olivier Girardot 9h ago1package cmd
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 "github.com/spf13/cobra"
9)
10
11var (
12 issueRepo string
13 issueState string
14 issueTitle string
15 issueBody string
16 issueLabels string
17 issueMilestone string
18 issueAssignee string
19 issueRemove bool
20)
21
22func init() {
23 issueCmd := &cobra.Command{
24 Use: "issue",
25 Aliases: []string{"issues"},
26 Short: "Work with issues (and labels)",
27 }
28 issueCmd.PersistentFlags().StringVarP(&issueRepo, "repo", "R", "", "target repo as owner/repo (default: current git remote)")
29
30 listCmd := &cobra.Command{
31 Use: "list",
32 Short: "List issues",
33 Args: cobra.NoArgs,
34 RunE: runIssueList,
35 }
36 listCmd.Flags().StringVar(&issueState, "state", "open", "open | closed | all")
37 addPaging(listCmd)
38
39 viewCmd := &cobra.Command{
40 Use: "view <number>",
41 Short: "Show an issue with its comments",
42 Args: cobra.ExactArgs(1),
43 RunE: runIssueView,
44 }
45
46 createCmd := &cobra.Command{
47 Use: "create",
48 Short: "Open an issue",
49 Args: cobra.NoArgs,
50 RunE: runIssueCreate,
51 }
52 createCmd.Flags().StringVarP(&issueTitle, "title", "t", "", "title (required)")
53 createCmd.Flags().StringVarP(&issueBody, "body", "b", "", "description body ('-' reads stdin)")
54
55 closeCmd := &cobra.Command{
56 Use: "close <number>",
57 Short: "Close an issue",
58 Args: cobra.ExactArgs(1),
59 RunE: func(cmd *cobra.Command, args []string) error { return runIssueState(cmd, args, "closed") },
60 }
61
62 reopenCmd := &cobra.Command{
63 Use: "reopen <number>",
64 Short: "Reopen a closed issue",
65 Args: cobra.ExactArgs(1),
66 RunE: func(cmd *cobra.Command, args []string) error { return runIssueState(cmd, args, "open") },
67 }
68
69 commentCmd := &cobra.Command{
70 Use: "comment <number>",
71 Short: "Comment on an issue",
72 Args: cobra.ExactArgs(1),
73 RunE: runIssueComment,
74 }
75 commentCmd.Flags().StringVarP(&issueBody, "body", "b", "", "comment body ('-' reads stdin)")
76
77 labelCmd := &cobra.Command{
78 Use: "label <number>",
79 Short: "Replace an issue's labels (by name, comma-separated; --clear empties)",
80 Args: cobra.ExactArgs(1),
81 RunE: runIssueLabel,
82 }
83 labelCmd.Flags().StringVar(&issueLabels, "labels", "", "comma-separated label names")
84 labelCmd.Flags().BoolVar(&issueRemove, "clear", false, "remove all labels")
85
86 milestoneCmd := &cobra.Command{
87 Use: "milestone <number>",
88 Short: "Assign an issue to a milestone (by title or id; --clear removes)",
89 Args: cobra.ExactArgs(1),
90 RunE: runIssueMilestone,
91 }
92 milestoneCmd.Flags().StringVar(&issueMilestone, "milestone", "", "milestone title or id")
93 milestoneCmd.Flags().BoolVar(&issueRemove, "clear", false, "remove the milestone")
94
95 assignCmd := &cobra.Command{
96 Use: "assign <number>",
97 Short: "Add or remove an assignee (--remove)",
98 Args: cobra.ExactArgs(1),
99 RunE: runIssueAssign,
100 }
101 assignCmd.Flags().StringVar(&issueAssignee, "user", "", "user handle")
102 assignCmd.Flags().BoolVar(&issueRemove, "remove", false, "remove instead of add")
103
104 labelsCmd := &cobra.Command{
105 Use: "labels",
106 Short: "List the repo's labels",
107 Args: cobra.NoArgs,
108 RunE: runIssueLabelsList,
109 }
110
111 issueCmd.AddCommand(listCmd, viewCmd, createCmd, closeCmd, reopenCmd, commentCmd, labelCmd, milestoneCmd, assignCmd, labelsCmd)
112 rootCmd.AddCommand(issueCmd)
113}
114
115// issueNumber parses a positive issue number argument.
116func issueNumber(arg string) (int, error) {
117 n, err := strconv.Atoi(arg)
118 if err != nil || n <= 0 {
119 return 0, fmt.Errorf("invalid issue number %q", arg)
120 }
121 return n, nil
122}
123
124// labelNames splits a comma-separated --labels value.
125func labelNames(s string) []string {
126 if strings.TrimSpace(s) == "" {
127 return []string{}
128 }
129 parts := strings.Split(s, ",")
130 out := make([]string, 0, len(parts))
131 for _, p := range parts {
132 if name := strings.TrimSpace(p); name != "" {
133 out = append(out, name)
134 }
135 }
136 return out
137}
138
139func runIssueList(cmd *cobra.Command, _ []string) error {
140 client, err := newClient()
141 if err != nil {
142 return err
143 }
144 owner, repo, err := resolveRepo(issueRepo)
145 if err != nil {
146 return err
147 }
148 page, err := client.ListIssues(cmd.Context(), owner, repo, issueState, pageFlag, perPageFlag)
149 if err != nil {
150 return err
151 }
152 if flagJSON {
153 return printJSON(cmd.OutOrStdout(), page)
154 }
155 if len(page.Items) == 0 {
156 fmt.Fprintln(cmd.OutOrStdout(), "No issues.")
157 return nil
158 }
159 tw := newTabw(cmd.OutOrStdout())
160 fmt.Fprintln(tw, "#\tSTATE\tLABELS\tMILESTONE\tTITLE")
161 for _, i := range page.Items {
162 var labels, milestone string
163 if len(i.Labels) > 0 {
164 names := make([]string, len(i.Labels))
165 for j, l := range i.Labels {
166 names[j] = l.Name
167 }
168 labels = strings.Join(names, ",")
169 }
170 if i.Milestone != nil {
171 milestone = i.Milestone.Title
172 }
173 fmt.Fprintf(tw, "%d\t%s\t%s\t%s\t%s\n", i.Number, i.State, dash(labels), dash(milestone), i.Title)
174 }
175 tw.Flush()
176 printPageFooter(cmd, page.Page)
177 return nil
178}
179
180func runIssueView(cmd *cobra.Command, args []string) error {
181 client, err := newClient()
182 if err != nil {
183 return err
184 }
185 owner, repo, err := resolveRepo(issueRepo)
186 if err != nil {
187 return err
188 }
189 n, err := issueNumber(args[0])
190 if err != nil {
191 return err
192 }
193 i, err := client.GetIssue(cmd.Context(), owner, repo, n)
194 if err != nil {
195 return err
196 }
197 if flagJSON {
198 return printJSON(cmd.OutOrStdout(), i)
199 }
200 out := cmd.OutOrStdout()
201 state := i.State
202 if i.Milestone != nil {
203 state += " · " + i.Milestone.Title
204 }
205 fmt.Fprintf(out, "Issue #%d %s [%s]\n", i.Number, i.Title, state)
206 fmt.Fprintf(out, "by %s · %s\n", dash(i.Author), humanTime(i.CreatedAt))
207 if len(i.Labels) > 0 {
208 names := make([]string, len(i.Labels))
209 for j, l := range i.Labels {
210 names[j] = l.Name
211 }
212 fmt.Fprintf(out, "labels: %s\n", strings.Join(names, ", "))
213 }
214 if len(i.Assignees) > 0 {
215 handles := make([]string, len(i.Assignees))
216 for j, a := range i.Assignees {
217 handles[j] = a.Handle
218 }
219 fmt.Fprintf(out, "assignees: %s\n", strings.Join(handles, ", "))
220 }
221 fmt.Fprintln(out)
222 if i.Body != "" {
223 fmt.Fprintln(out, i.Body)
224 }
225 for _, c := range i.Comments {
226 fmt.Fprintf(out, "\n--- %s (%s) ---\n%s\n", c.Author, humanTime(c.CreatedAt), c.Body)
227 }
228 return nil
229}
230
231// bodyOrStdin resolves a --body flag value, reading stdin when it is "-".
232func bodyOrStdin(cmd *cobra.Command, body string) (string, error) {
233 if body != "-" {
234 return body, nil
235 }
236 var sb strings.Builder
237 buf := make([]byte, 32*1024)
238 in := cmd.InOrStdin()
239 for {
240 n, err := in.Read(buf)
241 sb.Write(buf[:n])
242 if err != nil {
243 break
244 }
245 }
246 return strings.TrimRight(sb.String(), "\n"), nil
247}
248
249func runIssueCreate(cmd *cobra.Command, _ []string) error {
250 if issueTitle == "" {
251 return fmt.Errorf("--title is required")
252 }
253 body, err := bodyOrStdin(cmd, issueBody)
254 if err != nil {
255 return err
256 }
257 client, err := newClient()
258 if err != nil {
259 return err
260 }
261 owner, repo, err := resolveRepo(issueRepo)
262 if err != nil {
263 return err
264 }
265 i, err := client.CreateIssue(cmd.Context(), owner, repo, issueTitle, body)
266 if err != nil {
267 return err
268 }
269 if flagJSON {
270 return printJSON(cmd.OutOrStdout(), i)
271 }
272 fmt.Fprintf(cmd.OutOrStdout(), "Opened issue #%d: %s\n", i.Number, i.Title)
273 return nil
274}
275
276func runIssueState(cmd *cobra.Command, args []string, state string) error {
277 client, err := newClient()
278 if err != nil {
279 return err
280 }
281 owner, repo, err := resolveRepo(issueRepo)
282 if err != nil {
283 return err
284 }
285 n, err := issueNumber(args[0])
286 if err != nil {
287 return err
288 }
289 i, err := client.SetIssueState(cmd.Context(), owner, repo, n, state)
290 if err != nil {
291 return err
292 }
293 if flagJSON {
294 return printJSON(cmd.OutOrStdout(), i)
295 }
296 fmt.Fprintf(cmd.OutOrStdout(), "Issue #%d is now %s.\n", i.Number, i.State)
297 return nil
298}
299
300func runIssueComment(cmd *cobra.Command, args []string) error {
301 body, err := bodyOrStdin(cmd, issueBody)
302 if err != nil {
303 return err
304 }
305 if strings.TrimSpace(body) == "" {
306 return fmt.Errorf("--body is required")
307 }
308 client, err := newClient()
309 if err != nil {
310 return err
311 }
312 owner, repo, err := resolveRepo(issueRepo)
313 if err != nil {
314 return err
315 }
316 n, err := issueNumber(args[0])
317 if err != nil {
318 return err
319 }
320 c, err := client.CommentIssue(cmd.Context(), owner, repo, n, body)
321 if err != nil {
322 return err
323 }
324 if flagJSON {
325 return printJSON(cmd.OutOrStdout(), c)
326 }
327 fmt.Fprintf(cmd.OutOrStdout(), "Commented on issue #%d.\n", n)
328 return nil
329}
330
331func runIssueLabel(cmd *cobra.Command, args []string) error {
332 client, err := newClient()
333 if err != nil {
334 return err
335 }
336 owner, repo, err := resolveRepo(issueRepo)
337 if err != nil {
338 return err
339 }
340 n, err := issueNumber(args[0])
341 if err != nil {
342 return err
343 }
344 names := []string{}
345 if !issueRemove {
346 names = labelNames(issueLabels)
347 }
348 if err := client.SetIssueLabels(cmd.Context(), owner, repo, n, names); err != nil {
349 return err
350 }
351 if len(names) == 0 {
352 fmt.Fprintf(cmd.OutOrStdout(), "Cleared labels on issue #%d.\n", n)
353 } else {
354 fmt.Fprintf(cmd.OutOrStdout(), "Set labels on issue #%d: %s\n", n, strings.Join(names, ", "))
355 }
356 return nil
357}
358
359func runIssueMilestone(cmd *cobra.Command, args []string) error {
360 client, err := newClient()
361 if err != nil {
362 return err
363 }
364 owner, repo, err := resolveRepo(issueRepo)
365 if err != nil {
366 return err
367 }
368 n, err := issueNumber(args[0])
369 if err != nil {
370 return err
371 }
372 ref := issueMilestone
373 if issueRemove {
374 ref = ""
375 }
376 if err := client.SetIssueMilestone(cmd.Context(), owner, repo, n, ref); err != nil {
377 return err
378 }
379 if ref == "" {
380 fmt.Fprintf(cmd.OutOrStdout(), "Removed the milestone from issue #%d.\n", n)
381 } else {
382 fmt.Fprintf(cmd.OutOrStdout(), "Set issue #%d's milestone to %s.\n", n, ref)
383 }
384 return nil
385}
386
387func runIssueAssign(cmd *cobra.Command, args []string) error {
388 if issueAssignee == "" {
389 return fmt.Errorf("--user is required")
390 }
391 client, err := newClient()
392 if err != nil {
393 return err
394 }
395 owner, repo, err := resolveRepo(issueRepo)
396 if err != nil {
397 return err
398 }
399 n, err := issueNumber(args[0])
400 if err != nil {
401 return err
402 }
403 op := "add"
404 if issueRemove {
405 op = "remove"
406 }
407 if err := client.SetIssueAssignee(cmd.Context(), owner, repo, n, op, issueAssignee); err != nil {
408 return err
409 }
410 verb := "Assigned"
411 if issueRemove {
412 verb = "Unassigned"
413 }
414 fmt.Fprintf(cmd.OutOrStdout(), "%s %s on issue #%d.\n", verb, issueAssignee, n)
415 return nil
416}
417
418func runIssueLabelsList(cmd *cobra.Command, _ []string) error {
419 client, err := newClient()
420 if err != nil {
421 return err
422 }
423 owner, repo, err := resolveRepo(issueRepo)
424 if err != nil {
425 return err
426 }
427 labels, err := client.ListLabels(cmd.Context(), owner, repo)
428 if err != nil {
429 return err
430 }
431 if flagJSON {
432 return printJSON(cmd.OutOrStdout(), labels)
433 }
434 if len(labels) == 0 {
435 fmt.Fprintln(cmd.OutOrStdout(), "No labels.")
436 return nil
437 }
438 tw := newTabw(cmd.OutOrStdout())
439 fmt.Fprintln(tw, "NAME\tCOLOR")
440 for _, l := range labels {
441 fmt.Fprintf(tw, "%s\t#%s\n", l.Name, l.Color)
442 }
443 tw.Flush()
444 return nil
445}