ls.go

  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}