1package commands
2
3import (
4 "encoding/json"
5 "fmt"
6 "regexp"
7 "strings"
8 "time"
9
10 text "github.com/MichaelMure/go-term-text"
11 "github.com/spf13/cobra"
12
13 "github.com/MichaelMure/git-bug/bug"
14 "github.com/MichaelMure/git-bug/cache"
15 "github.com/MichaelMure/git-bug/query"
16 "github.com/MichaelMure/git-bug/util/colors"
17)
18
19type lsOptions struct {
20 query query.Query
21
22 statusQuery []string
23 noQuery []string
24 sortBy string
25 sortDirection string
26 outputFormat string
27}
28
29func newLsCommand() *cobra.Command {
30 env := newEnv()
31 options := lsOptions{}
32
33 cmd := &cobra.Command{
34 Use: "ls [<query>]",
35 Short: "List bugs.",
36 Long: `Display a summary of each bugs.
37
38You can pass an additional query to filter and order the list. This query can be expressed either with a simple query language or with flags.`,
39 Example: `List open bugs sorted by last edition with a query:
40git bug ls status:open sort:edit-desc
41
42List closed bugs sorted by creation with flags:
43git bug ls --status closed --by creation
44`,
45 PreRunE: loadBackend(env),
46 PostRunE: closeBackend(env),
47 RunE: func(cmd *cobra.Command, args []string) error {
48 return runLs(env, options, args)
49 },
50 }
51
52 flags := cmd.Flags()
53 flags.SortFlags = false
54
55 flags.StringSliceVarP(&options.statusQuery, "status", "s", nil,
56 "Filter by status. Valid values are [open,closed]")
57 flags.StringSliceVarP(&options.query.Author, "author", "a", nil,
58 "Filter by author")
59 flags.StringSliceVarP(&options.query.Participant, "participant", "p", nil,
60 "Filter by participant")
61 flags.StringSliceVarP(&options.query.Actor, "actor", "A", nil,
62 "Filter by actor")
63 flags.StringSliceVarP(&options.query.Label, "label", "l", nil,
64 "Filter by label")
65 flags.StringSliceVarP(&options.query.Title, "title", "t", nil,
66 "Filter by title")
67 flags.StringSliceVarP(&options.noQuery, "no", "n", nil,
68 "Filter by absence of something. Valid values are [label]")
69 flags.StringVarP(&options.sortBy, "by", "b", "creation",
70 "Sort the results by a characteristic. Valid values are [id,creation,edit]")
71 flags.StringVarP(&options.sortDirection, "direction", "d", "asc",
72 "Select the sorting direction. Valid values are [asc,desc]")
73 flags.StringVarP(&options.outputFormat, "format", "f", "default",
74 "Select the output formatting style. Valid values are [default,plain,json,org-mode]")
75
76 return cmd
77}
78
79func runLs(env *Env, opts lsOptions, args []string) error {
80 var q *query.Query
81 var err error
82
83 if len(args) >= 1 {
84 q, err = query.Parse(strings.Join(args, " "))
85
86 if err != nil {
87 return err
88 }
89 } else {
90 err = completeQuery(&opts)
91 if err != nil {
92 return err
93 }
94 q = &opts.query
95 }
96
97 allIds := env.backend.QueryBugs(q)
98
99 bugExcerpt := make([]*cache.BugExcerpt, len(allIds))
100 for i, id := range allIds {
101 b, err := env.backend.ResolveBugExcerpt(id)
102 if err != nil {
103 return err
104 }
105 bugExcerpt[i] = b
106 }
107
108 switch opts.outputFormat {
109 case "org-mode":
110 return lsOrgmodeFormatter(env, bugExcerpt)
111 case "plain":
112 return lsPlainFormatter(env, bugExcerpt)
113 case "json":
114 return lsJsonFormatter(env, bugExcerpt)
115 case "default":
116 return lsDefaultFormatter(env, bugExcerpt)
117 default:
118 return fmt.Errorf("unknown format %s", opts.outputFormat)
119 }
120}
121
122type JSONBugExcerpt struct {
123 Id string `json:"id"`
124 HumanId string `json:"human_id"`
125 CreateTime JSONTime `json:"create_time"`
126 EditTime JSONTime `json:"edit_time"`
127
128 Status string `json:"status"`
129 Labels []bug.Label `json:"labels"`
130 Title string `json:"title"`
131 Actors []JSONIdentity `json:"actors"`
132 Participants []JSONIdentity `json:"participants"`
133 Author JSONIdentity `json:"author"`
134
135 Comments int `json:"comments"`
136 Metadata map[string]string `json:"metadata"`
137}
138
139func lsJsonFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
140 jsonBugs := make([]JSONBugExcerpt, len(bugExcerpts))
141 for i, b := range bugExcerpts {
142 jsonBug := JSONBugExcerpt{
143 Id: b.Id.String(),
144 HumanId: b.Id.Human(),
145 CreateTime: NewJSONTime(b.CreateTime(), b.CreateLamportTime),
146 EditTime: NewJSONTime(b.EditTime(), b.EditLamportTime),
147 Status: b.Status.String(),
148 Labels: b.Labels,
149 Title: b.Title,
150 Comments: b.LenComments,
151 Metadata: b.CreateMetadata,
152 }
153
154 if b.AuthorId != "" {
155 author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
156 if err != nil {
157 return err
158 }
159 jsonBug.Author = NewJSONIdentityFromExcerpt(author)
160 } else {
161 jsonBug.Author = NewJSONIdentityFromLegacyExcerpt(&b.LegacyAuthor)
162 }
163
164 jsonBug.Actors = make([]JSONIdentity, len(b.Actors))
165 for i, element := range b.Actors {
166 actor, err := env.backend.ResolveIdentityExcerpt(element)
167 if err != nil {
168 return err
169 }
170 jsonBug.Actors[i] = NewJSONIdentityFromExcerpt(actor)
171 }
172
173 jsonBug.Participants = make([]JSONIdentity, len(b.Participants))
174 for i, element := range b.Participants {
175 participant, err := env.backend.ResolveIdentityExcerpt(element)
176 if err != nil {
177 return err
178 }
179 jsonBug.Participants[i] = NewJSONIdentityFromExcerpt(participant)
180 }
181
182 jsonBugs[i] = jsonBug
183 }
184 jsonObject, _ := json.MarshalIndent(jsonBugs, "", " ")
185 env.out.Printf("%s\n", jsonObject)
186 return nil
187}
188
189func lsDefaultFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
190 for _, b := range bugExcerpts {
191 var name string
192 if b.AuthorId != "" {
193 author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
194 if err != nil {
195 return err
196 }
197 name = author.DisplayName()
198 } else {
199 name = b.LegacyAuthor.DisplayName()
200 }
201
202 var labelsTxt strings.Builder
203 for _, l := range b.Labels {
204 lc256 := l.Color().Term256()
205 labelsTxt.WriteString(lc256.Escape())
206 labelsTxt.WriteString(" ◼")
207 labelsTxt.WriteString(lc256.Unescape())
208 }
209
210 // truncate + pad if needed
211 labelsFmt := text.TruncateMax(labelsTxt.String(), 10)
212 titleFmt := text.LeftPadMaxLine(strings.TrimSpace(b.Title), 50-text.Len(labelsFmt), 0)
213 authorFmt := text.LeftPadMaxLine(name, 15, 0)
214
215 comments := fmt.Sprintf("%4d 💬", b.LenComments)
216 if b.LenComments > 9999 {
217 comments = " ∞ 💬"
218 }
219
220 env.out.Printf("%s %s\t%s\t%s\t%s\n",
221 colors.Cyan(b.Id.Human()),
222 colors.Yellow(b.Status),
223 titleFmt+labelsFmt,
224 colors.Magenta(authorFmt),
225 comments,
226 )
227 }
228 return nil
229}
230
231func lsPlainFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
232 for _, b := range bugExcerpts {
233 env.out.Printf("%s [%s] %s\n", b.Id.Human(), b.Status, strings.TrimSpace(b.Title))
234 }
235 return nil
236}
237
238func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
239 // see https://orgmode.org/manual/Tags.html
240 orgTagRe := regexp.MustCompile("[^[:alpha:]_@]")
241 formatTag := func(l bug.Label) string {
242 return orgTagRe.ReplaceAllString(l.String(), "_")
243 }
244
245 formatTime := func(time time.Time) string {
246 return time.Format("[2006-01-02 Mon 15:05]")
247 }
248
249 env.out.Println("#+TODO: OPEN | CLOSED")
250
251 for _, b := range bugExcerpts {
252 status := strings.ToUpper(b.Status.String())
253
254 var title string
255 if link, ok := b.CreateMetadata["github-url"]; ok {
256 title = fmt.Sprintf("[[%s][%s]]", link, b.Title)
257 } else {
258 title = b.Title
259 }
260
261 var name string
262 if b.AuthorId != "" {
263 author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
264 if err != nil {
265 return err
266 }
267 name = author.DisplayName()
268 } else {
269 name = b.LegacyAuthor.DisplayName()
270 }
271
272 var labels strings.Builder
273 labels.WriteString(":")
274 for i, l := range b.Labels {
275 if i > 0 {
276 labels.WriteString(":")
277 }
278 labels.WriteString(formatTag(l))
279 }
280 labels.WriteString(":")
281
282 env.out.Printf("* %-6s %s %s %s: %s %s\n",
283 status,
284 b.Id.Human(),
285 formatTime(b.CreateTime()),
286 name,
287 title,
288 labels.String(),
289 )
290
291 env.out.Printf("** Last Edited: %s\n", formatTime(b.EditTime()))
292
293 env.out.Printf("** Actors:\n")
294 for _, element := range b.Actors {
295 actor, err := env.backend.ResolveIdentityExcerpt(element)
296 if err != nil {
297 return err
298 }
299
300 env.out.Printf(": %s %s\n",
301 actor.Id.Human(),
302 actor.DisplayName(),
303 )
304 }
305
306 env.out.Printf("** Participants:\n")
307 for _, element := range b.Participants {
308 participant, err := env.backend.ResolveIdentityExcerpt(element)
309 if err != nil {
310 return err
311 }
312
313 env.out.Printf(": %s %s\n",
314 participant.Id.Human(),
315 participant.DisplayName(),
316 )
317 }
318 }
319
320 return nil
321}
322
323// Finish the command flags transformation into the query.Query
324func completeQuery(opts *lsOptions) error {
325 for _, str := range opts.statusQuery {
326 status, err := bug.StatusFromString(str)
327 if err != nil {
328 return err
329 }
330 opts.query.Status = append(opts.query.Status, status)
331 }
332
333 for _, no := range opts.noQuery {
334 switch no {
335 case "label":
336 opts.query.NoLabel = true
337 default:
338 return fmt.Errorf("unknown \"no\" filter %s", no)
339 }
340 }
341
342 switch opts.sortBy {
343 case "id":
344 opts.query.OrderBy = query.OrderById
345 case "creation":
346 opts.query.OrderBy = query.OrderByCreation
347 case "edit":
348 opts.query.OrderBy = query.OrderByEdit
349 default:
350 return fmt.Errorf("unknown sort flag %s", opts.sortBy)
351 }
352
353 switch opts.sortDirection {
354 case "asc":
355 opts.query.OrderDirection = query.OrderAscending
356 case "desc":
357 opts.query.OrderDirection = query.OrderDescending
358 default:
359 return fmt.Errorf("unknown sort direction %s", opts.sortDirection)
360 }
361
362 return nil
363}