aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2020-03-14 16:47:38 +0100
committerMichael Muré <batolettre@gmail.com>2020-03-28 17:13:27 +0100
commit5e4dc87ffec7f87bbf3ebfcf256777ad773e8450 (patch)
tree04553cfb7ab8ea279c7415586ce1d0fe5c819996
parent58abc6b0a35b679ac0c34579ff1cb53c8fa71af4 (diff)
downloadgit-bug-5e4dc87ffec7f87bbf3ebfcf256777ad773e8450.tar.gz
cache: replace the all-in-one query parser by a complete one with AST/lexer/parser
-rw-r--r--cache/filter.go47
-rw-r--r--cache/query.go172
-rw-r--r--cache/query_test.go41
-rw-r--r--cache/repo_cache.go20
-rw-r--r--cache/repo_cache_test.go5
-rw-r--r--cache/sorting.go18
-rw-r--r--commands/ls.go57
-rw-r--r--graphql/resolvers/repo.go13
-rw-r--r--query/ast/ast.go45
-rw-r--r--query/lexer.go71
-rw-r--r--query/lexer_test.go45
-rw-r--r--query/parser.go100
-rw-r--r--query/parser_test.go98
-rw-r--r--termui/bug_table.go8
-rw-r--r--termui/termui.go5
15 files changed, 451 insertions, 294 deletions
diff --git a/cache/filter.go b/cache/filter.go
index ebe774ac..021962a7 100644
--- a/cache/filter.go
+++ b/cache/filter.go
@@ -5,6 +5,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/query/ast"
)
// resolver has the resolving functions needed by filters.
@@ -17,15 +18,10 @@ type resolver interface {
type Filter func(excerpt *BugExcerpt, resolver resolver) bool
// StatusFilter return a Filter that match a bug status
-func StatusFilter(query string) (Filter, error) {
- status, err := bug.StatusFromString(query)
- if err != nil {
- return nil, err
- }
-
+func StatusFilter(status bug.Status) Filter {
return func(excerpt *BugExcerpt, resolver resolver) bool {
return excerpt.Status == status
- }, nil
+ }
}
// AuthorFilter return a Filter that match a bug author
@@ -116,8 +112,8 @@ func NoLabelFilter() Filter {
}
}
-// Filters is a collection of Filter that implement a complex filter
-type Filters struct {
+// Matcher is a collection of Filter that implement a complex filter
+type Matcher struct {
Status []Filter
Author []Filter
Actor []Filter
@@ -127,8 +123,35 @@ type Filters struct {
NoFilters []Filter
}
+// compileMatcher transform an ast.Filters into a specialized matcher
+// for the cache.
+func compileMatcher(filters ast.Filters) *Matcher {
+ result := &Matcher{}
+
+ for _, value := range filters.Status {
+ result.Status = append(result.Status, StatusFilter(value))
+ }
+ for _, value := range filters.Author {
+ result.Author = append(result.Author, AuthorFilter(value))
+ }
+ for _, value := range filters.Actor {
+ result.Actor = append(result.Actor, ActorFilter(value))
+ }
+ for _, value := range filters.Participant {
+ result.Participant = append(result.Participant, ParticipantFilter(value))
+ }
+ for _, value := range filters.Label {
+ result.Label = append(result.Label, LabelFilter(value))
+ }
+ for _, value := range filters.Title {
+ result.Title = append(result.Title, TitleFilter(value))
+ }
+
+ return result
+}
+
// Match check if a bug match the set of filters
-func (f *Filters) Match(excerpt *BugExcerpt, resolver resolver) bool {
+func (f *Matcher) Match(excerpt *BugExcerpt, resolver resolver) bool {
if match := f.orMatch(f.Status, excerpt, resolver); !match {
return false
}
@@ -161,7 +184,7 @@ func (f *Filters) Match(excerpt *BugExcerpt, resolver resolver) bool {
}
// Check if any of the filters provided match the bug
-func (*Filters) orMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool {
+func (*Matcher) orMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool {
if len(filters) == 0 {
return true
}
@@ -175,7 +198,7 @@ func (*Filters) orMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver
}
// Check if all of the filters provided match the bug
-func (*Filters) andMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool {
+func (*Matcher) andMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool {
if len(filters) == 0 {
return true
}
diff --git a/cache/query.go b/cache/query.go
deleted file mode 100644
index 967c18d6..00000000
--- a/cache/query.go
+++ /dev/null
@@ -1,172 +0,0 @@
-package cache
-
-import (
- "fmt"
- "strings"
- "unicode"
-)
-
-type Query struct {
- Filters
- OrderBy
- OrderDirection
-}
-
-// Return an identity query with default sorting (creation-desc)
-func NewQuery() *Query {
- return &Query{
- OrderBy: OrderByCreation,
- OrderDirection: OrderDescending,
- }
-}
-
-// ParseQuery parse a query DSL
-//
-// Ex: "status:open author:descartes sort:edit-asc"
-//
-// Supported filter qualifiers and syntax are described in docs/queries.md
-func ParseQuery(query string) (*Query, error) {
- fields := splitQuery(query)
-
- result := &Query{
- OrderBy: OrderByCreation,
- OrderDirection: OrderDescending,
- }
-
- sortingDone := false
-
- for _, field := range fields {
- split := strings.Split(field, ":")
- if len(split) != 2 {
- return nil, fmt.Errorf("can't parse \"%s\"", field)
- }
-
- qualifierName := split[0]
- qualifierQuery := removeQuote(split[1])
-
- switch qualifierName {
- case "status", "state":
- f, err := StatusFilter(qualifierQuery)
- if err != nil {
- return nil, err
- }
- result.Status = append(result.Status, f)
-
- case "author":
- f := AuthorFilter(qualifierQuery)
- result.Author = append(result.Author, f)
-
- case "actor":
- f := ActorFilter(qualifierQuery)
- result.Actor = append(result.Actor, f)
-
- case "participant":
- f := ParticipantFilter(qualifierQuery)
- result.Participant = append(result.Participant, f)
-
- case "label":
- f := LabelFilter(qualifierQuery)
- result.Label = append(result.Label, f)
-
- case "title":
- f := TitleFilter(qualifierQuery)
- result.Title = append(result.Title, f)
-
- case "no":
- err := result.parseNoFilter(qualifierQuery)
- if err != nil {
- return nil, err
- }
-
- case "sort":
- if sortingDone {
- return nil, fmt.Errorf("multiple sorting")
- }
-
- err := result.parseSorting(qualifierQuery)
- if err != nil {
- return nil, err
- }
-
- sortingDone = true
-
- default:
- return nil, fmt.Errorf("unknown qualifier name %s", qualifierName)
- }
- }
-
- return result, nil
-}
-
-func splitQuery(query string) []string {
- lastQuote := rune(0)
- f := func(c rune) bool {
- switch {
- case c == lastQuote:
- lastQuote = rune(0)
- return false
- case lastQuote != rune(0):
- return false
- case unicode.In(c, unicode.Quotation_Mark):
- lastQuote = c
- return false
- default:
- return unicode.IsSpace(c)
- }
- }
-
- return strings.FieldsFunc(query, f)
-}
-
-func removeQuote(field string) string {
- if len(field) >= 2 {
- if field[0] == '"' && field[len(field)-1] == '"' {
- return field[1 : len(field)-1]
- }
- }
- return field
-}
-
-func (q *Query) parseNoFilter(query string) error {
- switch query {
- case "label":
- q.NoFilters = append(q.NoFilters, NoLabelFilter())
- default:
- return fmt.Errorf("unknown \"no\" filter %s", query)
- }
-
- return nil
-}
-
-func (q *Query) parseSorting(query string) error {
- switch query {
- // default ASC
- case "id-desc":
- q.OrderBy = OrderById
- q.OrderDirection = OrderDescending
- case "id", "id-asc":
- q.OrderBy = OrderById
- q.OrderDirection = OrderAscending
-
- // default DESC
- case "creation", "creation-desc":
- q.OrderBy = OrderByCreation
- q.OrderDirection = OrderDescending
- case "creation-asc":
- q.OrderBy = OrderByCreation
- q.OrderDirection = OrderAscending
-
- // default DESC
- case "edit", "edit-desc":
- q.OrderBy = OrderByEdit
- q.OrderDirection = OrderDescending
- case "edit-asc":
- q.OrderBy = OrderByEdit
- q.OrderDirection = OrderAscending
-
- default:
- return fmt.Errorf("unknown sorting %s", query)
- }
-
- return nil
-}
diff --git a/cache/query_test.go b/cache/query_test.go
deleted file mode 100644
index 9ae62ac4..00000000
--- a/cache/query_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package cache
-
-import "testing"
-
-func TestQueryParse(t *testing.T) {
-
- var tests = []struct {
- input string
- ok bool
- }{
- {"gibberish", false},
-
- {"status:", false},
-
- {"status:open", true},
- {"status:closed", true},
- {"status:unknown", false},
-
- {"author:rene", true},
- {`author:"René Descartes"`, true},
-
- {"actor:bernhard", true},
- {"participant:leonhard", true},
-
- {"label:hello", true},
- {`label:"Good first issue"`, true},
-
- {"title:titleOne", true},
- {`title:"Bug titleTwo"`, true},
-
- {"sort:edit", true},
- {"sort:unknown", false},
- }
-
- for _, test := range tests {
- _, err := ParseQuery(test.input)
- if (err == nil) != test.ok {
- t.Fatalf("Unexpected parse result, expected: %v, err: %v", test.ok, err)
- }
- }
-}
diff --git a/cache/repo_cache.go b/cache/repo_cache.go
index d859a966..6546b15e 100644
--- a/cache/repo_cache.go
+++ b/cache/repo_cache.go
@@ -18,6 +18,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/identity"
+ "github.com/MichaelMure/git-bug/query/ast"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/git"
"github.com/MichaelMure/git-bug/util/process"
@@ -525,7 +526,7 @@ func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, erro
}
// QueryBugs return the id of all Bug matching the given Query
-func (c *RepoCache) QueryBugs(query *Query) []entity.Id {
+func (c *RepoCache) QueryBugs(query *ast.Query) []entity.Id {
c.muBug.RLock()
defer c.muBug.RUnlock()
@@ -533,10 +534,12 @@ func (c *RepoCache) QueryBugs(query *Query) []entity.Id {
return c.AllBugsIds()
}
+ matcher := compileMatcher(query.Filters)
+
var filtered []*BugExcerpt
for _, excerpt := range c.bugExcerpts {
- if query.Match(excerpt, c) {
+ if matcher.Match(excerpt, c) {
filtered = append(filtered, excerpt)
}
}
@@ -544,18 +547,23 @@ func (c *RepoCache) QueryBugs(query *Query) []entity.Id {
var sorter sort.Interface
switch query.OrderBy {
- case OrderById:
+ case ast.OrderById:
sorter = BugsById(filtered)
- case OrderByCreation:
+ case ast.OrderByCreation:
sorter = BugsByCreationTime(filtered)
- case OrderByEdit:
+ case ast.OrderByEdit:
sorter = BugsByEditTime(filtered)
default:
panic("missing sort type")
}
- if query.OrderDirection == OrderDescending {
+ switch query.OrderDirection {
+ case ast.OrderAscending:
+ // Nothing to do
+ case ast.OrderDescending:
sorter = sort.Reverse(sorter)
+ default:
+ panic("missing sort direction")
}
sort.Sort(sorter)
diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go
index c3bd3cc4..51393dfd 100644
--- a/cache/repo_cache_test.go
+++ b/cache/repo_cache_test.go
@@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/require"
+ "github.com/MichaelMure/git-bug/query"
"github.com/MichaelMure/git-bug/repository"
)
@@ -68,9 +69,9 @@ func TestCache(t *testing.T) {
require.NoError(t, err)
// Querying
- query, err := ParseQuery("status:open author:descartes sort:edit-asc")
+ q, err := query.Parse("status:open author:descartes sort:edit-asc")
require.NoError(t, err)
- require.Len(t, cache.QueryBugs(query), 2)
+ require.Len(t, cache.QueryBugs(q), 2)
// Close
require.NoError(t, cache.Close())
diff --git a/cache/sorting.go b/cache/sorting.go
deleted file mode 100644
index 19034a9d..00000000
--- a/cache/sorting.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package cache
-
-type OrderBy int
-
-const (
- _ OrderBy = iota
- OrderById
- OrderByCreation
- OrderByEdit
-)
-
-type OrderDirection int
-
-const (
- _ OrderDirection = iota
- OrderAscending
- OrderDescending
-)
diff --git a/commands/ls.go b/commands/ls.go
index 70a948e6..aff56f61 100644
--- a/commands/ls.go
+++ b/commands/ls.go
@@ -7,7 +7,10 @@ import (
text "github.com/MichaelMure/go-term-text"
"github.com/spf13/cobra"
+ "github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
+ "github.com/MichaelMure/git-bug/query"
+ "github.com/MichaelMure/git-bug/query/ast"
"github.com/MichaelMure/git-bug/util/colors"
"github.com/MichaelMure/git-bug/util/interrupt"
)
@@ -32,21 +35,21 @@ func runLsBug(cmd *cobra.Command, args []string) error {
defer backend.Close()
interrupt.RegisterCleaner(backend.Close)
- var query *cache.Query
+ var q *ast.Query
if len(args) >= 1 {
- query, err = cache.ParseQuery(strings.Join(args, " "))
+ q, err = query.Parse(strings.Join(args, " "))
if err != nil {
return err
}
} else {
- query, err = lsQueryFromFlags()
+ q, err = lsQueryFromFlags()
if err != nil {
return err
}
}
- allIds := backend.QueryBugs(query)
+ allIds := backend.QueryBugs(q)
for _, id := range allIds {
b, err := backend.ResolveBugExcerpt(id)
@@ -96,47 +99,37 @@ func runLsBug(cmd *cobra.Command, args []string) error {
return nil
}
-// Transform the command flags into a query
-func lsQueryFromFlags() (*cache.Query, error) {
- query := cache.NewQuery()
+// Transform the command flags into an ast.Query
+func lsQueryFromFlags() (*ast.Query, error) {
+ q := ast.NewQuery()
- for _, status := range lsStatusQuery {
- f, err := cache.StatusFilter(status)
+ for _, str := range lsStatusQuery {
+ status, err := bug.StatusFromString(str)
if err != nil {
return nil, err
}
- query.Status = append(query.Status, f)
+ q.Status = append(q.Status, status)
}
-
for _, title := range lsTitleQuery {
- f := cache.TitleFilter(title)
- query.Title = append(query.Title, f)
+ q.Title = append(q.Title, title)
}
-
for _, author := range lsAuthorQuery {
- f := cache.AuthorFilter(author)
- query.Author = append(query.Author, f)
+ q.Author = append(q.Author, author)
}
-
for _, actor := range lsActorQuery {
- f := cache.ActorFilter(actor)
- query.Actor = append(query.Actor, f)
+ q.Actor = append(q.Actor, actor)
}
-
for _, participant := range lsParticipantQuery {
- f := cache.ParticipantFilter(participant)
- query.Participant = append(query.Participant, f)
+ q.Participant = append(q.Participant, participant)
}
-
for _, label := range lsLabelQuery {
- f := cache.LabelFilter(label)
- query.Label = append(query.Label, f)
+ q.Label = append(q.Label, label)
}
for _, no := range lsNoQuery {
switch no {
case "label":
- query.NoFilters = append(query.NoFilters, cache.NoLabelFilter())
+ q.NoLabel = true
default:
return nil, fmt.Errorf("unknown \"no\" filter %s", no)
}
@@ -144,25 +137,25 @@ func lsQueryFromFlags() (*cache.Query, error) {
switch lsSortBy {
case "id":
- query.OrderBy = cache.OrderById
+ q.OrderBy = ast.OrderById
case "creation":
- query.OrderBy = cache.OrderByCreation
+ q.OrderBy = ast.OrderByCreation
case "edit":
- query.OrderBy = cache.OrderByEdit
+ q.OrderBy = ast.OrderByEdit
default:
return nil, fmt.Errorf("unknown sort flag %s", lsSortBy)
}
switch lsSortDirection {
case "asc":
- query.OrderDirection = cache.OrderAscending
+ q.OrderDirection = ast.OrderAscending
case "desc":
- query.OrderDirection = cache.OrderDescending
+ q.OrderDirection = ast.OrderDescending
default:
return nil, fmt.Errorf("unknown sort direction %s", lsSortDirection)
}
- return query, nil
+ return q, nil
}
var lsCmd = &cobra.Command{
diff --git a/graphql/resolvers/repo.go b/graphql/resolvers/repo.go
index d090544d..dde1dcf6 100644
--- a/graphql/resolvers/repo.go
+++ b/graphql/resolvers/repo.go
@@ -4,11 +4,12 @@ import (
"context"
"github.com/MichaelMure/git-bug/bug"
- "github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/graphql/connections"
"github.com/MichaelMure/git-bug/graphql/graph"
"github.com/MichaelMure/git-bug/graphql/models"
+ "github.com/MichaelMure/git-bug/query"
+ "github.com/MichaelMure/git-bug/query/ast"
)
var _ graph.RepositoryResolver = &repoResolver{}
@@ -28,19 +29,19 @@ func (repoResolver) AllBugs(_ context.Context, obj *models.Repository, after *st
Last: last,
}
- var query *cache.Query
+ var q *ast.Query
if queryStr != nil {
- query2, err := cache.ParseQuery(*queryStr)
+ query2, err := query.Parse(*queryStr)
if err != nil {
return nil, err
}
- query = query2
+ q = query2
} else {
- query = cache.NewQuery()
+ q = ast.NewQuery()
}
// Simply pass a []string with the ids to the pagination algorithm
- source := obj.Repo.QueryBugs(query)
+ source := obj.Repo.QueryBugs(q)
// The edger create a custom edge holding just the id
edger := func(id entity.Id, offset int) connections.Edge {
diff --git a/query/ast/ast.go b/query/ast/ast.go
new file mode 100644
index 00000000..fe77abf9
--- /dev/null
+++ b/query/ast/ast.go
@@ -0,0 +1,45 @@
+package ast
+
+import "github.com/MichaelMure/git-bug/bug"
+
+type Query struct {
+ Filters
+ OrderBy
+ OrderDirection
+}
+
+// NewQuery return an identity query with the default sorting (creation-desc).
+func NewQuery() *Query {
+ return &Query{
+ OrderBy: OrderByCreation,
+ OrderDirection: OrderDescending,
+ }
+}
+
+// Filters is a collection of Filter that implement a complex filter
+type Filters struct {
+ Status []bug.Status
+ Author []string
+ Actor []string
+ Participant []string
+ Label []string
+ Title []string
+ NoLabel bool
+}
+
+type OrderBy int
+
+const (
+ _ OrderBy = iota
+ OrderById
+ OrderByCreation
+ OrderByEdit
+)
+
+type OrderDirection int
+
+const (
+ _ OrderDirection = iota
+ OrderAscending
+ OrderDescending
+)
diff --git a/query/lexer.go b/query/lexer.go
new file mode 100644
index 00000000..ec05f44e
--- /dev/null
+++ b/query/lexer.go
@@ -0,0 +1,71 @@
+package query
+
+import (
+ "fmt"
+ "strings"
+ "unicode"
+)
+
+type token struct {
+ qualifier string
+ value string
+}
+
+// TODO: this lexer implementation behave badly with unmatched quotes.
+// A hand written one would be better instead of relying on strings.FieldsFunc()
+
+// tokenize parse and break a input into tokens ready to be
+// interpreted later by a parser to get the semantic.
+func tokenize(query string) ([]token, error) {
+ fields := splitQuery(query)
+
+ var tokens []token
+ for _, field := range fields {
+ split := strings.Split(field, ":")
+ if len(split) != 2 {
+ return nil, fmt.Errorf("can't tokenize \"%s\"", field)
+ }
+
+ if len(split[0]) == 0 {
+ return nil, fmt.Errorf("can't tokenize \"%s\": empty qualifier", field)
+ }
+ if len(split[1]) == 0 {
+ return nil, fmt.Errorf("empty value for qualifier \"%s\"", split[0])
+ }
+
+ tokens = append(tokens, token{
+ qualifier: split[0],
+ value: removeQuote(split[1]),
+ })
+ }
+ return tokens, nil
+}
+
+func splitQuery(query string) []string {
+ lastQuote := rune(0)
+ f := func(c rune) bool {
+ switch {
+ case c == lastQuote:
+ lastQuote = rune(0)
+ return false
+ case lastQuote != rune(0):
+ return false
+ case unicode.In(c, unicode.Quotation_Mark):
+ lastQuote = c
+ return false
+ default:
+ return unicode.IsSpace(c)
+ }
+ }
+
+ return strings.FieldsFunc(query, f)
+}
+
+func removeQuote(field string) string {
+ if len(field) >= 2 {
+ if field[0] == '"' && field[len(field)-1] == '"' {
+ return field[1 : len(field)-1]
+ }
+ }
+ return field
+}
diff --git a/query/lexer_test.go b/query/lexer_test.go
new file mode 100644
index 00000000..922e3fc9
--- /dev/null
+++ b/query/lexer_test.go
@@ -0,0 +1,45 @@
+package query
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTokenize(t *testing.T) {
+ var tests = []struct {
+ input string
+ tokens []token
+ }{
+ {"gibberish", nil},
+ {"status:", nil},
+ {":value", nil},
+
+ {"status:open", []token{{"status", "open"}}},
+ {"status:closed", []token{{"status", "closed"}}},
+
+ {"author:rene", []token{{"author", "rene"}}},
+ {`author:"René Descartes"`, []token{{"author", "René Descartes"}}},
+
+ {
+ `status:open status:closed author:rene author:"René Descartes"`,
+ []token{
+ {"status", "open"},
+ {"status", "closed"},
+ {"author", "rene"},
+ {"author", "René Descartes"},
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ tokens, err := tokenize(tc.input)
+ if tc.tokens == nil {
+ assert.Error(t, err)
+ assert.Nil(t, tokens)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tc.tokens, tokens)
+ }
+ }
+}
diff --git a/query/parser.go b/query/parser.go
new file mode 100644
index 00000000..89893b60
--- /dev/null
+++ b/query/parser.go
@@ -0,0 +1,100 @@
+package query
+
+import (
+ "fmt"
+
+ "github.com/MichaelMure/git-bug/bug"
+ "github.com/MichaelMure/git-bug/query/ast"
+)
+
+// Parse parse a query DSL
+//
+// Ex: "status:open author:descartes sort:edit-asc"
+//
+// Supported filter qualifiers and syntax are described in docs/queries.md
+func Parse(query string) (*ast.Query, error) {
+ tokens, err := tokenize(query)
+ if err != nil {
+ return nil, err
+ }
+
+ q := &ast.Query{
+ OrderBy: ast.OrderByCreation,
+ OrderDirection: ast.OrderDescending,
+ }
+ sortingDone := false
+
+ for _, t := range tokens {
+ switch t.qualifier {
+ case "status", "state":
+ status, err := bug.StatusFromString(t.value)
+ if err != nil {
+ return nil, err
+ }
+ q.Status = append(q.Status, status)
+ case "author":
+ q.Author = append(q.Author, t.value)
+ case "actor":
+ q.Actor = append(q.Actor, t.value)
+ case "participant":
+ q.Participant = append(q.Participant, t.value)
+ case "label":
+ q.Label = append(q.Label, t.value)
+ case "title":
+ q.Title = append(q.Title, t.value)
+ case "no":
+ switch t.value {
+ case "label":
+ q.NoLabel = true
+ default:
+ return nil, fmt.Errorf("unknown \"no\" filter \"%s\"", t.value)
+ }
+ case "sort":
+ if sortingDone {
+ return nil, fmt.Errorf("multiple sorting")
+ }
+ err = parseSorting(q, t.value)
+ if err != nil {
+ return nil, err
+ }
+ sortingDone = true
+
+ default:
+ return nil, fmt.Errorf("unknown qualifier \"%s\"", t.qualifier)
+ }
+ }
+ return q, nil
+}
+
+func parseSorting(q *ast.Query, value string) error {
+ switch value {
+ // default ASC
+ case "id-desc":
+ q.OrderBy = ast.OrderById
+ q.OrderDirection = ast.OrderDescending
+ case "id", "id-asc":
+ q.OrderBy = ast.OrderById
+ q.OrderDirection = ast.OrderAscending
+
+ // default DESC
+ case "creation", "creation-desc":
+ q.OrderBy = ast.OrderByCreation
+ q.OrderDirection = ast.OrderDescending
+ case "creation-asc":
+ q.OrderBy = ast.OrderByCreation
+ q.OrderDirection = ast.OrderAscending
+
+ // default DESC
+ case "edit", "edit-desc":
+ q.OrderBy = ast.OrderByEdit
+ q.OrderDirection = ast.OrderDescending
+ case "edit-asc":
+ q.OrderBy = ast.OrderByEdit
+ q.OrderDirection = ast.OrderAscending
+
+ default:
+ return fmt.Errorf("unknown sorting %s", value)
+ }
+
+ return nil
+}
diff --git a/query/parser_test.go b/query/parser_test.go
new file mode 100644
index 00000000..065e647a
--- /dev/null
+++ b/query/parser_test.go
@@ -0,0 +1,98 @@
+package query
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/MichaelMure/git-bug/bug"
+ "github.com/MichaelMure/git-bug/query/ast"
+)
+
+func TestParse(t *testing.T) {
+ var tests = []struct {
+ input string
+ output *ast.Query
+ }{
+ {"gibberish", nil},
+ {"status:", nil},
+ {":value", nil},
+
+ {"status:open", &ast.Query{
+ Filters: ast.Filters{Status: []bug.Status{bug.OpenStatus}},
+ }},
+ {"status:closed", &ast.Query{
+ Filters: ast.Filters{Status: []bug.Status{bug.ClosedStatus}},
+ }},
+ {"status:unknown", nil},
+
+ {"author:rene", &ast.Query{
+ Filters: ast.Filters{Author: []string{"rene"}},
+ }},
+ {`author:"René Descartes"`, &ast.Query{
+ Filters: ast.Filters{Author: []string{"René Descartes"}},
+ }},
+
+ {"actor:bernhard", &ast.Query{
+ Filters: ast.Filters{Actor: []string{"bernhard"}},
+ }},
+ {"participant:leonhard", &ast.Query{
+ Filters: ast.Filters{Participant: []string{"leonhard"}},
+ }},
+
+ {"label:hello", &ast.Query{
+ Filters: ast.Filters{Label: []string{"hello"}},
+ }},
+ {`label:"Good first issue"`, &ast.Query{
+ Filters: ast.Filters{Label: []string{"Good first issue"}},
+ }},
+
+ {"title:titleOne", &ast.Query{
+ Filters: ast.Filters{Title: []string{"titleOne"}},
+ }},
+ {`title:"Bug titleTwo"`, &ast.Query{
+ Filters: ast.Filters{Title: []string{"Bug titleTwo"}},
+ }},
+
+ {"no:label", &ast.Query{
+ Filters: ast.Filters{NoLabel: true},
+ }},
+
+ {"sort:edit", &ast.Query{
+ OrderBy: ast.OrderByEdit,
+ }},
+ {"sort:unknown", nil},
+
+ {`status:open author:"René Descartes" participant:leonhard label:hello label:"Good first issue" sort:edit-desc`,
+ &ast.Query{
+ Filters: ast.Filters{
+ Status: []bug.Status{bug.OpenStatus},
+ Author: []string{"René Descartes"},
+ Participant: []string{"leonhard"},
+ Label: []string{"hello", "Good first issue"},
+ },
+ OrderBy: ast.OrderByEdit,
+ OrderDirection: ast.OrderDescending,
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.input, func(t *testing.T) {
+ query, err := Parse(tc.input)
+ if tc.output == nil {
+ assert.Error(t, err)
+ assert.Nil(t, query)
+ } else {
+ assert.NoError(t, err)
+ if tc.output.OrderBy != 0 {
+ assert.Equal(t, tc.output.OrderBy, query.OrderBy)
+ }
+ if tc.output.OrderDirection != 0 {
+ assert.Equal(t, tc.output.OrderDirection, query.OrderDirection)
+ }
+ assert.Equal(t, tc.output.Filters, query.Filters)
+ }
+ })
+ }
+}
diff --git a/termui/bug_table.go b/termui/bug_table.go
index 41aa4e83..74f92826 100644
--- a/termui/bug_table.go
+++ b/termui/bug_table.go
@@ -12,6 +12,8 @@ import (
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/query"
+ "github.com/MichaelMure/git-bug/query/ast"
"github.com/MichaelMure/git-bug/util/colors"
)
@@ -26,7 +28,7 @@ const defaultQuery = "status:open"
type bugTable struct {
repo *cache.RepoCache
queryStr string
- query *cache.Query
+ query *ast.Query
allIds []entity.Id
excerpts []*cache.BugExcerpt
pageCursor int
@@ -34,14 +36,14 @@ type bugTable struct {
}
func newBugTable(c *cache.RepoCache) *bugTable {
- query, err := cache.ParseQuery(defaultQuery)
+ q, err := query.Parse(defaultQuery)
if err != nil {
panic(err)
}
return &bugTable{
repo: c,
- query: query,
+ query: q,
queryStr: defaultQuery,
pageCursor: 0,
selectCursor: 0,
diff --git a/termui/termui.go b/termui/termui.go
index 1ef960de..96b7583c 100644
--- a/termui/termui.go
+++ b/termui/termui.go
@@ -12,6 +12,7 @@ import (
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/input"
+ "github.com/MichaelMure/git-bug/query"
)
var errTerminateMainloop = errors.New("terminate gocui mainloop")
@@ -336,12 +337,12 @@ func editQueryWithEditor(bt *bugTable) error {
bt.queryStr = queryStr
- query, err := cache.ParseQuery(queryStr)
+ q, err := query.Parse(queryStr)
if err != nil {
ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
} else {
- bt.query = query
+ bt.query = q
}
initGui(nil)