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