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 time.Sleep(5 * time.Second)
81
82 var q *query.Query
83 var err error
84
85 if len(args) >= 1 {
86 q, err = query.Parse(strings.Join(args, " "))
87
88 if err != nil {
89 return err
90 }
91 } else {
92 err = completeQuery(&opts)
93 if err != nil {
94 return err
95 }
96 q = &opts.query
97 }
98
99 allIds := env.backend.QueryBugs(q)
100
101 bugExcerpt := make([]*cache.BugExcerpt, len(allIds))
102 for i, id := range allIds {
103 b, err := env.backend.ResolveBugExcerpt(id)
104 if err != nil {
105 return err
106 }
107 bugExcerpt[i] = b
108 }
109
110 switch opts.outputFormat {
111 case "org-mode":
112 return lsOrgmodeFormatter(env, bugExcerpt)
113 case "plain":
114 return lsPlainFormatter(env, bugExcerpt)
115 case "json":
116 return lsJsonFormatter(env, bugExcerpt)
117 case "default":
118 return lsDefaultFormatter(env, bugExcerpt)
119 default:
120 return fmt.Errorf("unknown format %s", opts.outputFormat)
121 }
122}
123
124type JSONBugExcerpt struct {
125 Id string `json:"id"`
126 HumanId string `json:"human_id"`
127 CreateTime JSONTime `json:"create_time"`
128 EditTime JSONTime `json:"edit_time"`
129
130 Status string `json:"status"`
131 Labels []bug.Label `json:"labels"`
132 Title string `json:"title"`
133 Actors []JSONIdentity `json:"actors"`
134 Participants []JSONIdentity `json:"participants"`
135 Author JSONIdentity `json:"author"`
136
137 Comments int `json:"comments"`
138 Metadata map[string]string `json:"metadata"`
139}
140
141func lsJsonFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
142 jsonBugs := make([]JSONBugExcerpt, len(bugExcerpts))
143 for i, b := range bugExcerpts {
144 jsonBug := JSONBugExcerpt{
145 Id: b.Id.String(),
146 HumanId: b.Id.Human(),
147 CreateTime: NewJSONTime(b.CreateTime(), b.CreateLamportTime),
148 EditTime: NewJSONTime(b.EditTime(), b.EditLamportTime),
149 Status: b.Status.String(),
150 Labels: b.Labels,
151 Title: b.Title,
152 Comments: b.LenComments,
153 Metadata: b.CreateMetadata,
154 }
155
156 if b.AuthorId != "" {
157 author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
158 if err != nil {
159 return err
160 }
161 jsonBug.Author = NewJSONIdentityFromExcerpt(author)
162 } else {
163 jsonBug.Author = NewJSONIdentityFromLegacyExcerpt(&b.LegacyAuthor)
164 }
165
166 jsonBug.Actors = make([]JSONIdentity, len(b.Actors))
167 for i, element := range b.Actors {
168 actor, err := env.backend.ResolveIdentityExcerpt(element)
169 if err != nil {
170 return err
171 }
172 jsonBug.Actors[i] = NewJSONIdentityFromExcerpt(actor)
173 }
174
175 jsonBug.Participants = make([]JSONIdentity, len(b.Participants))
176 for i, element := range b.Participants {
177 participant, err := env.backend.ResolveIdentityExcerpt(element)
178 if err != nil {
179 return err
180 }
181 jsonBug.Participants[i] = NewJSONIdentityFromExcerpt(participant)
182 }
183
184 jsonBugs[i] = jsonBug
185 }
186 jsonObject, _ := json.MarshalIndent(jsonBugs, "", " ")
187 env.out.Printf("%s\n", jsonObject)
188 return nil
189}
190
191func lsDefaultFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
192 for _, b := range bugExcerpts {
193 var name string
194 if b.AuthorId != "" {
195 author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
196 if err != nil {
197 return err
198 }
199 name = author.DisplayName()
200 } else {
201 name = b.LegacyAuthor.DisplayName()
202 }
203
204 var labelsTxt strings.Builder
205 for _, l := range b.Labels {
206 lc256 := l.Color().Term256()
207 labelsTxt.WriteString(lc256.Escape())
208 labelsTxt.WriteString(" ◼")
209 labelsTxt.WriteString(lc256.Unescape())
210 }
211
212 // truncate + pad if needed
213 labelsFmt := text.TruncateMax(labelsTxt.String(), 10)
214 titleFmt := text.LeftPadMaxLine(b.Title, 50-text.Len(labelsFmt), 0)
215 authorFmt := text.LeftPadMaxLine(name, 15, 0)
216
217 comments := fmt.Sprintf("%4d 💬", b.LenComments)
218 if b.LenComments > 9999 {
219 comments = " ∞ 💬"
220 }
221
222 env.out.Printf("%s %s\t%s\t%s\t%s\n",
223 colors.Cyan(b.Id.Human()),
224 colors.Yellow(b.Status),
225 titleFmt+labelsFmt,
226 colors.Magenta(authorFmt),
227 comments,
228 )
229 }
230 return nil
231}
232
233func lsPlainFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
234 for _, b := range bugExcerpts {
235 env.out.Printf("%s [%s] %s\n", b.Id.Human(), b.Status, b.Title)
236 }
237 return nil
238}
239
240func timeToOrgmode(time time.Time) string {
241 return time.Format("[2006-01-02 Mon 15:05]")
242}
243
244func lsOrgmodeFormatter(env *Env, bugExcerpts []*cache.BugExcerpt) error {
245 env.out.Println("#+TODO: OPEN | CLOSED")
246
247 for _, b := range bugExcerpts {
248 status := strings.ToUpper(b.Status.String())
249
250 var title string
251 if link, ok := b.CreateMetadata["github-url"]; ok {
252 title = fmt.Sprintf("[[%s][%s]]", link, b.Title)
253 } else {
254 title = b.Title
255 }
256
257 var name string
258 if b.AuthorId != "" {
259 author, err := env.backend.ResolveIdentityExcerpt(b.AuthorId)
260 if err != nil {
261 return err
262 }
263 name = author.DisplayName()
264 } else {
265 name = b.LegacyAuthor.DisplayName()
266 }
267
268 labels := b.Labels
269 var labelsString string
270 if len(labels) > 0 {
271 var labelsStrings = make([]string, len(labels))
272 // see https://orgmode.org/manual/Tags.html
273 var orgTagRe = regexp.MustCompile("[^[:alpha:]_@]")
274 for i, l := range labels {
275 var tag = orgTagRe.ReplaceAllString(l.String(), "_")
276 labelsStrings[i] = tag
277 }
278 labelsString = fmt.Sprintf(":%s:", strings.Join(labelsStrings, ":"))
279 } else {
280 labelsString = ""
281 }
282
283 env.out.Printf("* %-6s %s %s %s: %s %s\n",
284 status,
285 b.Id.Human(),
286 timeToOrgmode(b.CreateTime()),
287 name,
288 title,
289 labelsString,
290 )
291
292 env.out.Printf("** Last Edited: %s\n", timeToOrgmode(b.EditTime()))
293
294 env.out.Printf("** Actors:\n")
295 for _, element := range b.Actors {
296 actor, err := env.backend.ResolveIdentityExcerpt(element)
297 if err != nil {
298 return err
299 }
300
301 env.out.Printf(": %s %s\n",
302 actor.Id.Human(),
303 actor.DisplayName(),
304 )
305 }
306
307 env.out.Printf("** Participants:\n")
308 for _, element := range b.Participants {
309 participant, err := env.backend.ResolveIdentityExcerpt(element)
310 if err != nil {
311 return err
312 }
313
314 env.out.Printf(": %s %s\n",
315 participant.Id.Human(),
316 participant.DisplayName(),
317 )
318 }
319 }
320
321 return nil
322}
323
324// Finish the command flags transformation into the query.Query
325func completeQuery(opts *lsOptions) error {
326 for _, str := range opts.statusQuery {
327 status, err := bug.StatusFromString(str)
328 if err != nil {
329 return err
330 }
331 opts.query.Status = append(opts.query.Status, status)
332 }
333
334 for _, no := range opts.noQuery {
335 switch no {
336 case "label":
337 opts.query.NoLabel = true
338 default:
339 return fmt.Errorf("unknown \"no\" filter %s", no)
340 }
341 }
342
343 switch opts.sortBy {
344 case "id":
345 opts.query.OrderBy = query.OrderById
346 case "creation":
347 opts.query.OrderBy = query.OrderByCreation
348 case "edit":
349 opts.query.OrderBy = query.OrderByEdit
350 default:
351 return fmt.Errorf("unknown sort flag %s", opts.sortBy)
352 }
353
354 switch opts.sortDirection {
355 case "asc":
356 opts.query.OrderDirection = query.OrderAscending
357 case "desc":
358 opts.query.OrderDirection = query.OrderDescending
359 default:
360 return fmt.Errorf("unknown sort direction %s", opts.sortDirection)
361 }
362
363 return nil
364}