Merge pull request #100 from sladyn98/faster_ls

Michael Muré created

`git bug ls` should be faster

Change summary

bug/snapshot.go              |  8 --------
cache/bug_excerpt.go         |  8 ++++++--
cache/filter.go              | 15 +++++++++++++++
cache/filter_test.go         | 34 ++++++++++++++++++++++++++++++++++
cache/query.go               |  4 ++++
cache/query_test.go          |  3 +++
commands/ls.go               | 31 +++++++++++++++----------------
doc/man/git-bug-ls.1         |  4 ++++
doc/md/git-bug_ls.md         |  1 +
doc/queries.md               | 10 ++++++++++
input/input.go               |  1 +
misc/bash_completion/git-bug |  3 +++
12 files changed, 96 insertions(+), 26 deletions(-)

Detailed changes

bug/snapshot.go 🔗

@@ -34,14 +34,6 @@ func (snap *Snapshot) HumanId() string {
 	return FormatHumanID(snap.id)
 }
 
-// Deprecated:should be moved in UI code
-func (snap *Snapshot) Summary() string {
-	return fmt.Sprintf("C:%d L:%d",
-		len(snap.Comments)-1,
-		len(snap.Labels),
-	)
-}
-
 // Return the last time a bug was modified
 func (snap *Snapshot) LastEditTime() time.Time {
 	if len(snap.Operations) == 0 {

cache/bug_excerpt.go 🔗

@@ -23,8 +23,10 @@ type BugExcerpt struct {
 	CreateUnixTime    int64
 	EditUnixTime      int64
 
-	Status bug.Status
-	Labels []bug.Label
+	Status      bug.Status
+	Labels      []bug.Label
+	Title       string
+	LenComments int
 
 	// If author is identity.Bare, LegacyAuthor is set
 	// If author is identity.Identity, AuthorId is set and data is deported
@@ -50,6 +52,8 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
 		EditUnixTime:      snap.LastEditUnix(),
 		Status:            snap.Status,
 		Labels:            snap.Labels,
+		Title:             snap.Title,
+		LenComments:       len(snap.Comments),
 		CreateMetadata:    b.FirstOp().AllMetadata(),
 	}
 

cache/filter.go 🔗

@@ -55,6 +55,16 @@ func LabelFilter(label string) Filter {
 	}
 }
 
+// TitleFilter return a Filter that match if the title contains the given query
+func TitleFilter(query string) Filter {
+	return func(repo *RepoCache, excerpt *BugExcerpt) bool {
+		return strings.Contains(
+			strings.ToLower(excerpt.Title),
+			strings.ToLower(query),
+		)
+	}
+}
+
 // NoLabelFilter return a Filter that match the absence of labels
 func NoLabelFilter() Filter {
 	return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
@@ -67,6 +77,7 @@ type Filters struct {
 	Status    []Filter
 	Author    []Filter
 	Label     []Filter
+	Title     []Filter
 	NoFilters []Filter
 }
 
@@ -88,6 +99,10 @@ func (f *Filters) Match(repoCache *RepoCache, excerpt *BugExcerpt) bool {
 		return false
 	}
 
+	if match := f.andMatch(f.Title, repoCache, excerpt); !match {
+		return false
+	}
+
 	return true
 }
 

cache/filter_test.go 🔗

@@ -0,0 +1,34 @@
+package cache
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestTitleFilter(t *testing.T) {
+	tests := []struct {
+		name  string
+		title string
+		query string
+		match bool
+	}{
+		{name: "complete match", title: "hello world", query: "hello world", match: true},
+		{name: "partial match", title: "hello world", query: "hello", match: true},
+		{name: "no match", title: "hello world", query: "foo", match: false},
+		{name: "cased title", title: "Hello World", query: "hello", match: true},
+		{name: "cased query", title: "hello world", query: "Hello", match: true},
+
+		// Those following tests should work eventually but are left for a future iteration.
+
+		// {name: "cased accents", title: "ÑOÑO", query: "ñoño", match: true},
+		// {name: "natural language matching", title: "Århus", query: "Aarhus", match: true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			filter := TitleFilter(tt.query)
+			excerpt := &BugExcerpt{Title: tt.title}
+			assert.Equal(t, tt.match, filter(nil, excerpt))
+		})
+	}
+}

cache/query.go 🔗

@@ -60,6 +60,10 @@ func ParseQuery(query string) (*Query, error) {
 			f := LabelFilter(qualifierQuery)
 			result.Label = append(result.Label, f)
 
+		case "title":
+			f := TitleFilter(qualifierQuery)
+			result.Label = append(result.Title, f)
+
 		case "no":
 			err := result.parseNoFilter(qualifierQuery)
 			if err != nil {

cache/query_test.go 🔗

@@ -22,6 +22,9 @@ func TestQueryParse(t *testing.T) {
 		{"label:hello", true},
 		{`label:"Good first issue"`, true},
 
+		{"title:titleOne", true},
+		{`title:"Bug titleTwo"`, true},
+
 		{"sort:edit", true},
 		{"sort:unknown", false},
 	}

commands/ls.go 🔗

@@ -5,7 +5,6 @@ import (
 	"strings"
 
 	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/colors"
 	"github.com/MichaelMure/git-bug/util/interrupt"
 	"github.com/spf13/cobra"
@@ -14,6 +13,7 @@ import (
 var (
 	lsStatusQuery   []string
 	lsAuthorQuery   []string
+	lsTitleQuery    []string
 	lsLabelQuery    []string
 	lsNoQuery       []string
 	lsSortBy        string
@@ -45,30 +45,22 @@ func runLsBug(cmd *cobra.Command, args []string) error {
 	allIds := backend.QueryBugs(query)
 
 	for _, id := range allIds {
-		b, err := backend.ResolveBug(id)
+		b, err := backend.ResolveBugExcerpt(id)
 		if err != nil {
 			return err
 		}
 
-		snapshot := b.Snapshot()
-
-		var author identity.Interface
-
-		if len(snapshot.Comments) > 0 {
-			create := snapshot.Comments[0]
-			author = create.Author
-		}
-
 		// truncate + pad if needed
-		titleFmt := fmt.Sprintf("%-50.50s", snapshot.Title)
-		authorFmt := fmt.Sprintf("%-15.15s", author.DisplayName())
+		titleFmt := fmt.Sprintf("%-50.50s", b.Title)
+		authorFmt := fmt.Sprintf("%-15.15s", b.LegacyAuthor.Name)
 
-		fmt.Printf("%s %s\t%s\t%s\t%s\n",
+		fmt.Printf("%s %s\t%s\t%s\tC:%d L:%d\n",
 			colors.Cyan(b.HumanId()),
-			colors.Yellow(snapshot.Status),
+			colors.Yellow(b.Status),
 			titleFmt,
 			colors.Magenta(authorFmt),
-			snapshot.Summary(),
+			b.LenComments,
+			len(b.Labels),
 		)
 	}
 
@@ -87,6 +79,11 @@ func lsQueryFromFlags() (*cache.Query, error) {
 		query.Status = append(query.Status, f)
 	}
 
+	for _, title := range lsTitleQuery {
+		f := cache.TitleFilter(title)
+		query.Title = append(query.Title, f)
+	}
+
 	for _, author := range lsAuthorQuery {
 		f := cache.AuthorFilter(author)
 		query.Author = append(query.Author, f)
@@ -156,6 +153,8 @@ func init() {
 		"Filter by author")
 	lsCmd.Flags().StringSliceVarP(&lsLabelQuery, "label", "l", nil,
 		"Filter by label")
+	lsCmd.Flags().StringSliceVarP(&lsTitleQuery, "title", "t", nil,
+		"Filter by title")
 	lsCmd.Flags().StringSliceVarP(&lsNoQuery, "no", "n", nil,
 		"Filter by absence of something. Valid values are [label]")
 	lsCmd.Flags().StringVarP(&lsSortBy, "by", "b", "creation",

doc/man/git-bug-ls.1 🔗

@@ -34,6 +34,10 @@ You can pass an additional query to filter and order the list. This query can be
 \fB\-l\fP, \fB\-\-label\fP=[]
     Filter by label
 
+.PP
+\fB\-t\fP, \fB\-\-title\fP=[]
+    Filter by title
+
 .PP
 \fB\-n\fP, \fB\-\-no\fP=[]
     Filter by absence of something. Valid values are [label]

doc/md/git-bug_ls.md 🔗

@@ -29,6 +29,7 @@ git bug ls --status closed --by creation
   -s, --status strings     Filter by status. Valid values are [open,closed]
   -a, --author strings     Filter by author
   -l, --label strings      Filter by label
+  -t, --title strings      Filter by title
   -n, --no strings         Filter by absence of something. Valid values are [label]
   -b, --by string          Sort the results by a characteristic. Valid values are [id,creation,edit] (default "creation")
   -d, --direction string   Select the sorting direction. Valid values are [asc,desc] (default "asc")

doc/queries.md 🔗

@@ -42,6 +42,16 @@ You can filter based on the bug's label.
 | `label:LABEL` | `label:prod` matches bugs with the label `prod`                           |
 |               | `label:"Good first issue"` matches bugs with the label `Good first issue` |
 
+### Filtering by title
+
+You can filter based on the bug's title.
+
+| Qualifier     | Example                                                                        |
+| ---           | ---                                                                            |
+| `title:TITLE` | `title:Critical` matches bugs with a title containing `Critical`               |
+|               | `title:"Typo in string"` matches bugs with a title containing `Typo in string` |
+
+
 ### Filtering by missing feature
 
 You can filter bugs based on the absence of something.

input/input.go 🔗

@@ -193,6 +193,7 @@ const queryTemplate = `%s
 #
 # - status:open, status:closed
 # - author:<query>
+# - title:<title>
 # - label:<label>
 # - no:label
 #

misc/bash_completion/git-bug 🔗

@@ -535,6 +535,9 @@ _git-bug_ls()
     flags+=("--label=")
     two_word_flags+=("-l")
     local_nonpersistent_flags+=("--label=")
+    flags+=("--title=")
+    two_word_flags+=("-t")
+    local_nonpersistent_flags+=("--title=")
     flags+=("--no=")
     two_word_flags+=("-n")
     local_nonpersistent_flags+=("--no=")