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