aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMiklos Vajna <vmiklos@collabora.com>2021-02-14 16:03:51 +0100
committerMiklos Vajna <vmiklos@collabora.com>2021-02-21 14:15:50 +0100
commitcb61245078a0e8f14e359ed20e0582a695645a08 (patch)
treeaa0e8f4ea3d8a5eb2f7ac3746875b0854c1714bb
parent956f98b676ab44d19ed522061c9520a32aab1a3c (diff)
downloadgit-bug-cb61245078a0e8f14e359ed20e0582a695645a08.tar.gz
Add ability to search by arbitrary metadata
Example: ~/git/git-bug/git-bug ls --metadata github-url=https://github.com/author/myproject/issues/42 or ~/git/git-bug/git-bug ls metadata:github-url:\"https://github.com/author/myproject/issues/42\" Fixes the cmdline part of <https://github.com/MichaelMure/git-bug/issues/567>.
-rw-r--r--cache/filter.go18
-rw-r--r--commands/ls.go13
-rw-r--r--doc/man/git-bug-ls.14
-rw-r--r--doc/md/git-bug_ls.md1
-rw-r--r--misc/bash_completion/git-bug6
-rw-r--r--misc/powershell_completion/git-bug2
-rw-r--r--query/lexer.go58
-rw-r--r--query/lexer_test.go8
-rw-r--r--query/parser.go15
-rw-r--r--query/parser_test.go5
-rw-r--r--query/query.go7
11 files changed, 129 insertions, 8 deletions
diff --git a/cache/filter.go b/cache/filter.go
index 2ac56ab5..c167fe71 100644
--- a/cache/filter.go
+++ b/cache/filter.go
@@ -38,6 +38,16 @@ func AuthorFilter(query string) Filter {
}
}
+// MetadataFilter return a Filter that match a bug metadata at creation time
+func MetadataFilter(pair query.StringPair) Filter {
+ return func(excerpt *BugExcerpt, resolver resolver) bool {
+ if value, ok := excerpt.CreateMetadata[pair.Key]; ok {
+ return value == pair.Value
+ }
+ return false
+ }
+}
+
// LabelFilter return a Filter that match a label
func LabelFilter(label string) Filter {
return func(excerpt *BugExcerpt, resolver resolver) bool {
@@ -109,6 +119,7 @@ func NoLabelFilter() Filter {
type Matcher struct {
Status []Filter
Author []Filter
+ Metadata []Filter
Actor []Filter
Participant []Filter
Label []Filter
@@ -127,6 +138,9 @@ func compileMatcher(filters query.Filters) *Matcher {
for _, value := range filters.Author {
result.Author = append(result.Author, AuthorFilter(value))
}
+ for _, value := range filters.Metadata {
+ result.Metadata = append(result.Metadata, MetadataFilter(value))
+ }
for _, value := range filters.Actor {
result.Actor = append(result.Actor, ActorFilter(value))
}
@@ -153,6 +167,10 @@ func (f *Matcher) Match(excerpt *BugExcerpt, resolver resolver) bool {
return false
}
+ if match := f.orMatch(f.Metadata, excerpt, resolver); !match {
+ return false
+ }
+
if match := f.orMatch(f.Participant, excerpt, resolver); !match {
return false
}
diff --git a/commands/ls.go b/commands/ls.go
index 327fd37f..71c420c6 100644
--- a/commands/ls.go
+++ b/commands/ls.go
@@ -19,6 +19,7 @@ import (
type lsOptions struct {
statusQuery []string
authorQuery []string
+ metadataQuery []string
participantQuery []string
actorQuery []string
labelQuery []string
@@ -65,6 +66,8 @@ git bug ls status:open --by creation "foo bar" baz
"Filter by status. Valid values are [open,closed]")
flags.StringSliceVarP(&options.authorQuery, "author", "a", nil,
"Filter by author")
+ flags.StringSliceVarP(&options.metadataQuery, "metadata", "m", nil,
+ "Filter by metadata. Example: github-url=URL")
flags.StringSliceVarP(&options.participantQuery, "participant", "p", nil,
"Filter by participant")
flags.StringSliceVarP(&options.actorQuery, "actor", "A", nil,
@@ -337,6 +340,16 @@ func completeQuery(q *query.Query, opts lsOptions) error {
}
q.Author = append(q.Author, opts.authorQuery...)
+ for _, str := range opts.metadataQuery {
+ tokens := strings.Split(str, "=")
+ if len(tokens) < 2 {
+ return fmt.Errorf("no \"=\" in key=value metadata markup")
+ }
+ var pair query.StringPair
+ pair.Key = tokens[0]
+ pair.Value = tokens[1]
+ q.Metadata = append(q.Metadata, pair)
+ }
q.Participant = append(q.Participant, opts.participantQuery...)
q.Actor = append(q.Actor, opts.actorQuery...)
q.Label = append(q.Label, opts.labelQuery...)
diff --git a/doc/man/git-bug-ls.1 b/doc/man/git-bug-ls.1
index a0e60db7..0ab51709 100644
--- a/doc/man/git-bug-ls.1
+++ b/doc/man/git-bug-ls.1
@@ -29,6 +29,10 @@ You can pass an additional query to filter and order the list. This query can be
Filter by author
.PP
+\fB\-m\fP, \fB\-\-metadata\fP=[]
+ Filter by metadata. Example: github\-url=URL
+
+.PP
\fB\-p\fP, \fB\-\-participant\fP=[]
Filter by participant
diff --git a/doc/md/git-bug_ls.md b/doc/md/git-bug_ls.md
index df54224f..7d1e490d 100644
--- a/doc/md/git-bug_ls.md
+++ b/doc/md/git-bug_ls.md
@@ -34,6 +34,7 @@ git bug ls status:open --by creation "foo bar" baz
```
-s, --status strings Filter by status. Valid values are [open,closed]
-a, --author strings Filter by author
+ -m, --metadata strings Filter by metadata. Example: github-url=URL
-p, --participant strings Filter by participant
-A, --actor strings Filter by actor
-l, --label strings Filter by label
diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug
index 912e87b4..3cedd86a 100644
--- a/misc/bash_completion/git-bug
+++ b/misc/bash_completion/git-bug
@@ -851,6 +851,12 @@ _git-bug_ls()
local_nonpersistent_flags+=("--author")
local_nonpersistent_flags+=("--author=")
local_nonpersistent_flags+=("-a")
+ flags+=("--metadata=")
+ two_word_flags+=("--metadata")
+ two_word_flags+=("-m")
+ local_nonpersistent_flags+=("--metadata")
+ local_nonpersistent_flags+=("--metadata=")
+ local_nonpersistent_flags+=("-m")
flags+=("--participant=")
two_word_flags+=("--participant")
two_word_flags+=("-p")
diff --git a/misc/powershell_completion/git-bug b/misc/powershell_completion/git-bug
index c2aa0adf..29cb327a 100644
--- a/misc/powershell_completion/git-bug
+++ b/misc/powershell_completion/git-bug
@@ -146,6 +146,8 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock {
[CompletionResult]::new('--status', 'status', [CompletionResultType]::ParameterName, 'Filter by status. Valid values are [open,closed]')
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Filter by author')
[CompletionResult]::new('--author', 'author', [CompletionResultType]::ParameterName, 'Filter by author')
+ [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Filter by metadata. Example: github-url=URL')
+ [CompletionResult]::new('--metadata', 'metadata', [CompletionResultType]::ParameterName, 'Filter by metadata. Example: github-url=URL')
[CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Filter by participant')
[CompletionResult]::new('--participant', 'participant', [CompletionResultType]::ParameterName, 'Filter by participant')
[CompletionResult]::new('-A', 'A', [CompletionResultType]::ParameterName, 'Filter by actor')
diff --git a/query/lexer.go b/query/lexer.go
index 5ca700c7..45f657df 100644
--- a/query/lexer.go
+++ b/query/lexer.go
@@ -11,16 +11,20 @@ type tokenKind int
const (
_ tokenKind = iota
tokenKindKV
+ tokenKindKVV
tokenKindSearch
)
type token struct {
kind tokenKind
- // KV
+ // KV and KVV
qualifier string
value string
+ // KVV only
+ subQualifier string
+
// Search
term string
}
@@ -33,6 +37,15 @@ func newTokenKV(qualifier, value string) token {
}
}
+func newTokenKVV(qualifier, subQualifier, value string) token {
+ return token{
+ kind: tokenKindKVV,
+ qualifier: qualifier,
+ subQualifier: subQualifier,
+ value: value,
+ }
+}
+
func newTokenSearch(term string) token {
return token{
kind: tokenKindSearch,
@@ -50,7 +63,23 @@ func tokenize(query string) ([]token, error) {
var tokens []token
for _, field := range fields {
- split := strings.Split(field, ":")
+ // Split using ':' as separator, but separators inside '"' don't count.
+ quoted := false
+ split := strings.FieldsFunc(field, func(r rune) bool {
+ if r == '"' {
+ quoted = !quoted
+ }
+ return !quoted && r == ':'
+ })
+ if (strings.HasPrefix(field, ":")) {
+ split = append([]string{""}, split...)
+ }
+ if (strings.HasSuffix(field, ":")) {
+ split = append(split, "")
+ }
+ if (quoted) {
+ return nil, fmt.Errorf("can't tokenize \"%s\": unmatched quote", field)
+ }
// full text search
if len(split) == 1 {
@@ -58,18 +87,31 @@ func tokenize(query string) ([]token, error) {
continue
}
- if len(split) != 2 {
- return nil, fmt.Errorf("can't tokenize \"%s\"", field)
+ if len(split) > 3 {
+ return nil, fmt.Errorf("can't tokenize \"%s\": too many separators", 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, newTokenKV(split[0], removeQuote(split[1])))
+ if len(split) == 2 {
+ if len(split[1]) == 0 {
+ return nil, fmt.Errorf("empty value for qualifier \"%s\"", split[0])
+ }
+
+ tokens = append(tokens, newTokenKV(split[0], removeQuote(split[1])))
+ } else {
+ if len(split[1]) == 0 {
+ return nil, fmt.Errorf("empty sub-qualifier for qualifier \"%s\"", split[0])
+ }
+
+ if len(split[2]) == 0 {
+ return nil, fmt.Errorf("empty value for qualifier \"%s:%s\"", split[0], split[1])
+ }
+
+ tokens = append(tokens, newTokenKVV(split[0], removeQuote(split[1]), removeQuote(split[2])))
+ }
}
return tokens, nil
}
diff --git a/query/lexer_test.go b/query/lexer_test.go
index 59f17dec..4ffb35a0 100644
--- a/query/lexer_test.go
+++ b/query/lexer_test.go
@@ -37,6 +37,14 @@ func TestTokenize(t *testing.T) {
{`key:'value value`, nil},
{`key:value value'`, nil},
+ // sub-qualifier posive testing
+ {`key:subkey:"value:value"`, []token{newTokenKVV("key", "subkey", "value:value")}},
+
+ // sub-qualifier negative testing
+ {`key:subkey:value:value`, nil},
+ {`key:subkey:`, nil},
+ {`key:subkey:"value`, nil},
+
// full text search
{"search", []token{newTokenSearch("search")}},
{"search more terms", []token{
diff --git a/query/parser.go b/query/parser.go
index 762a47e5..e820c629 100644
--- a/query/parser.go
+++ b/query/parser.go
@@ -67,6 +67,21 @@ func Parse(query string) (*Query, error) {
default:
return nil, fmt.Errorf("unknown qualifier \"%s\"", t.qualifier)
}
+
+ case tokenKindKVV:
+ switch t.qualifier {
+ case "metadata":
+ if len(t.subQualifier) == 0 {
+ return nil, fmt.Errorf("empty value for sub-qualifier \"metadata:%s\"", t.subQualifier)
+ }
+ var pair StringPair
+ pair.Key = t.subQualifier
+ pair.Value = t.value
+ q.Metadata = append(q.Metadata, pair)
+
+ default:
+ return nil, fmt.Errorf("unknown qualifier \"%s:%s\"", t.qualifier, t.subQualifier)
+ }
}
}
return q, nil
diff --git a/query/parser_test.go b/query/parser_test.go
index 87dd870a..6d91d6cc 100644
--- a/query/parser_test.go
+++ b/query/parser_test.go
@@ -84,6 +84,11 @@ func TestParse(t *testing.T) {
OrderDirection: OrderDescending,
},
},
+
+ // Metadata
+ {`metadata:key:"https://www.example.com/"`, &Query{
+ Filters: Filters{Metadata: []StringPair{{"key", "https://www.example.com/"}}},
+ }},
}
for _, tc := range tests {
diff --git a/query/query.go b/query/query.go
index 816d6414..3a2321cf 100644
--- a/query/query.go
+++ b/query/query.go
@@ -23,10 +23,17 @@ func NewQuery() *Query {
type Search []string
+// Used for key-value pairs when filtering based on metadata
+type StringPair struct {
+ Key string
+ Value string
+}
+
// Filters is a collection of Filter that implement a complex filter
type Filters struct {
Status []bug.Status
Author []string
+ Metadata []StringPair
Actor []string
Participant []string
Label []string