ls.go

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