1package cache
2
3import (
4 "strings"
5
6 "github.com/git-bug/git-bug/entities/common"
7 "github.com/git-bug/git-bug/entity"
8 "github.com/git-bug/git-bug/query"
9)
10
11// Filter is a predicate that matches a subset of bugs
12type Filter func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool
13
14// StatusFilter return a Filter that matches a bug status
15func StatusFilter(status common.Status) Filter {
16 return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
17 return excerpt.Status == status
18 }
19}
20
21// AuthorFilter return a Filter that matches a bug author
22func AuthorFilter(query string) Filter {
23 return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
24 query = strings.ToLower(query)
25
26 author, err := entity.Resolve[*IdentityExcerpt](resolvers, excerpt.AuthorId)
27 if err != nil {
28 panic(err)
29 }
30
31 return author.Match(query)
32 }
33}
34
35// MetadataFilter return a Filter that matches a bug metadata at creation time
36func MetadataFilter(pair query.StringPair) Filter {
37 return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
38 if value, ok := excerpt.CreateMetadata[pair.Key]; ok {
39 return value == pair.Value
40 }
41 return false
42 }
43}
44
45// LabelFilter return a Filter that matches a label
46func LabelFilter(label string) Filter {
47 return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
48 for _, l := range excerpt.Labels {
49 if string(l) == label {
50 return true
51 }
52 }
53 return false
54 }
55}
56
57// ActorFilter return a Filter that matches a bug actor
58func ActorFilter(query string) Filter {
59 return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
60 query = strings.ToLower(query)
61
62 for _, id := range excerpt.Actors {
63 identityExcerpt, err := entity.Resolve[*IdentityExcerpt](resolvers, id)
64 if err != nil {
65 panic(err)
66 }
67
68 if identityExcerpt.Match(query) {
69 return true
70 }
71 }
72 return false
73 }
74}
75
76// ParticipantFilter return a Filter that matches a bug participant
77func ParticipantFilter(query string) Filter {
78 return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
79 query = strings.ToLower(query)
80
81 for _, id := range excerpt.Participants {
82 identityExcerpt, err := entity.Resolve[*IdentityExcerpt](resolvers, id)
83 if err != nil {
84 panic(err)
85 }
86
87 if identityExcerpt.Match(query) {
88 return true
89 }
90 }
91 return false
92 }
93}
94
95// TitleFilter return a Filter that matches if the title contains the given query
96func TitleFilter(query string) Filter {
97 return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
98 return strings.Contains(
99 strings.ToLower(excerpt.Title),
100 strings.ToLower(query),
101 )
102 }
103}
104
105// NoLabelFilter return a Filter that matches the absence of labels
106func NoLabelFilter() Filter {
107 return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
108 return len(excerpt.Labels) == 0
109 }
110}
111
112// Matcher is a collection of Filter that implement a complex filter
113type Matcher struct {
114 Status []Filter
115 Author []Filter
116 Metadata []Filter
117 Actor []Filter
118 Participant []Filter
119 Label []Filter
120 Title []Filter
121 NoFilters []Filter
122}
123
124// compileMatcher transform a query.Filters into a specialized matcher
125// for the cache.
126func compileMatcher(filters query.Filters) *Matcher {
127 result := &Matcher{}
128
129 for _, value := range filters.Status {
130 result.Status = append(result.Status, StatusFilter(value))
131 }
132 for _, value := range filters.Author {
133 result.Author = append(result.Author, AuthorFilter(value))
134 }
135 for _, value := range filters.Metadata {
136 result.Metadata = append(result.Metadata, MetadataFilter(value))
137 }
138 for _, value := range filters.Actor {
139 result.Actor = append(result.Actor, ActorFilter(value))
140 }
141 for _, value := range filters.Participant {
142 result.Participant = append(result.Participant, ParticipantFilter(value))
143 }
144 for _, value := range filters.Label {
145 result.Label = append(result.Label, LabelFilter(value))
146 }
147 for _, value := range filters.Title {
148 result.Title = append(result.Title, TitleFilter(value))
149 }
150 if filters.NoLabel {
151 result.NoFilters = append(result.NoFilters, NoLabelFilter())
152 }
153
154 return result
155}
156
157// Match check if a bug matches the set of filters
158func (f *Matcher) Match(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
159 if match := f.orMatch(f.Status, excerpt, resolvers); !match {
160 return false
161 }
162
163 if match := f.orMatch(f.Author, excerpt, resolvers); !match {
164 return false
165 }
166
167 if match := f.orMatch(f.Metadata, excerpt, resolvers); !match {
168 return false
169 }
170
171 if match := f.orMatch(f.Participant, excerpt, resolvers); !match {
172 return false
173 }
174
175 if match := f.orMatch(f.Actor, excerpt, resolvers); !match {
176 return false
177 }
178
179 if match := f.andMatch(f.Label, excerpt, resolvers); !match {
180 return false
181 }
182
183 if match := f.andMatch(f.NoFilters, excerpt, resolvers); !match {
184 return false
185 }
186
187 if match := f.andMatch(f.Title, excerpt, resolvers); !match {
188 return false
189 }
190
191 return true
192}
193
194// Check if any of the filters provided match the bug
195func (*Matcher) orMatch(filters []Filter, excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
196 if len(filters) == 0 {
197 return true
198 }
199
200 match := false
201 for _, f := range filters {
202 match = match || f(excerpt, resolvers)
203 }
204
205 return match
206}
207
208// Check if all the filters provided match the bug
209func (*Matcher) andMatch(filters []Filter, excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
210 if len(filters) == 0 {
211 return true
212 }
213
214 match := true
215 for _, f := range filters {
216 match = match && f(excerpt, resolvers)
217 }
218
219 return match
220}