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