mirror of
https://github.com/MichaelMure/git-bug.git
synced 2024-12-14 08:45:30 +03:00
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>.
This commit is contained in:
parent
956f98b676
commit
cb61245078
18
cache/filter.go
vendored
18
cache/filter.go
vendored
@ -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
|
||||
}
|
||||
|
@ -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...)
|
||||
|
@ -28,6 +28,10 @@ You can pass an additional query to filter and order the list. This query can be
|
||||
\fB\-a\fP, \fB\-\-author\fP=[]
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user