diff --git a/bug/snapshot.go b/bug/snapshot.go index 46618ebddc77750dcd81ac1f79ff2e16ca929a8a..83b94416fe8616a2e29d3f1370e53c51aecb893e 100644 --- a/bug/snapshot.go +++ b/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 { diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index fd06e51b192e8a2997def3ebae321e0e01904272..a50d8c66d1587a4218464a320e006a5a906daebc 100644 --- a/cache/bug_excerpt.go +++ b/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(), } diff --git a/cache/filter.go b/cache/filter.go index 022a8ff25a058824ac21f59d36ba28c8f07c1824..b6872508c8d8418c9ae9cb00c740659e426e216a 100644 --- a/cache/filter.go +++ b/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 } diff --git a/cache/filter_test.go b/cache/filter_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a47d2ad731f5dff308a844df5addb1bdf28ca6c3 --- /dev/null +++ b/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)) + }) + } +} diff --git a/cache/query.go b/cache/query.go index 6ffa65103cf607ee32cd772797c76ca56b1fc06b..39815d32d14112a3a7e02bdb031162317fd15047 100644 --- a/cache/query.go +++ b/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 { diff --git a/cache/query_test.go b/cache/query_test.go index 29d2f585a4f436ddbd1e71b5a2772f2adfcd399c..f34b3e6ad7fb8a3255a500a6a2dbbe06822adcd9 100644 --- a/cache/query_test.go +++ b/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}, } diff --git a/commands/ls.go b/commands/ls.go index 75b7ceaf6ea61167e987d99844b340a4a9ad6321..75819f1a733f7d098264b921954344d6575ad69e 100644 --- a/commands/ls.go +++ b/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", diff --git a/doc/man/git-bug-ls.1 b/doc/man/git-bug-ls.1 index eb985fd219ae98bfec832b331f12a09b41f8847a..7b3a0aa64b60c39ac031718380142008857bb244 100644 --- a/doc/man/git-bug-ls.1 +++ b/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] diff --git a/doc/md/git-bug_ls.md b/doc/md/git-bug_ls.md index 18ac5d61142ac7e82dd1b3cabbe138fe7691c300..9ab71884851250686d64f78338bce8a721451ca2 100644 --- a/doc/md/git-bug_ls.md +++ b/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") diff --git a/doc/queries.md b/doc/queries.md index 93135070c3f70d4890f4fa1d72623bfb0bff3b79..224b59a0e32065b118eba422e77efdd81cc55c91 100644 --- a/doc/queries.md +++ b/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. diff --git a/input/input.go b/input/input.go index c36b9046db15192717b920f0f5c73dc8491827e0..789373b0855f0a536496791175afbfc808022a7d 100644 --- a/input/input.go +++ b/input/input.go @@ -193,6 +193,7 @@ const queryTemplate = `%s # # - status:open, status:closed # - author: +# - title: # - label:<label> # - no:label # diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index a0ac545c17fb7569a7e949865251042a7011105d..ec8c64eafc568b24fa8e31512dbfdc05e74f5130 100644 --- a/misc/bash_completion/git-bug +++ b/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=")