1package cache
2
3import (
4 "errors"
5 "fmt"
6 "sort"
7 "strings"
8 "time"
9 "unicode/utf8"
10
11 "github.com/blevesearch/bleve"
12
13 "github.com/MichaelMure/git-bug/entities/bug"
14 "github.com/MichaelMure/git-bug/entities/identity"
15 "github.com/MichaelMure/git-bug/entity"
16 "github.com/MichaelMure/git-bug/query"
17 "github.com/MichaelMure/git-bug/repository"
18)
19
20type RepoCacheBug struct {
21 SubCache[*BugExcerpt, *BugCache, bug.Interface]
22}
23
24// ResolveBugCreateMetadata retrieve a bug that has the exact given metadata on
25// its Create operation, that is, the first operation. It fails if multiple bugs
26// match.
27func (c *RepoCacheBug) ResolveBugCreateMetadata(key string, value string) (*BugCache, error) {
28 return c.ResolveMatcher(func(excerpt *BugExcerpt) bool {
29 return excerpt.CreateMetadata[key] == value
30 })
31}
32
33// ResolveComment search for a Bug/Comment combination matching the merged
34// bug/comment Id prefix. Returns the Bug containing the Comment and the Comment's
35// Id.
36func (c *RepoCacheBug) ResolveComment(prefix string) (*BugCache, entity.CombinedId, error) {
37 bugPrefix, _ := entity.SeparateIds(prefix)
38 bugCandidate := make([]entity.Id, 0, 5)
39
40 // build a list of possible matching bugs
41 c.mu.RLock()
42 for _, excerpt := range c.excerpts {
43 if excerpt.Id().HasPrefix(bugPrefix) {
44 bugCandidate = append(bugCandidate, excerpt.Id())
45 }
46 }
47 c.mu.RUnlock()
48
49 matchingBugIds := make([]entity.Id, 0, 5)
50 matchingCommentId := entity.UnsetCombinedId
51 var matchingBug *BugCache
52
53 // search for matching comments
54 // searching every bug candidate allow for some collision with the bug prefix only,
55 // before being refined with the full comment prefix
56 for _, bugId := range bugCandidate {
57 b, err := c.Resolve(bugId)
58 if err != nil {
59 return nil, entity.UnsetCombinedId, err
60 }
61
62 for _, comment := range b.Snapshot().Comments {
63 if comment.CombinedId().HasPrefix(prefix) {
64 matchingBugIds = append(matchingBugIds, bugId)
65 matchingBug = b
66 matchingCommentId = comment.CombinedId()
67 }
68 }
69 }
70
71 if len(matchingBugIds) > 1 {
72 return nil, entity.UnsetCombinedId, entity.NewErrMultipleMatch("bug/comment", matchingBugIds)
73 } else if len(matchingBugIds) == 0 {
74 return nil, entity.UnsetCombinedId, errors.New("comment doesn't exist")
75 }
76
77 return matchingBug, matchingCommentId, nil
78}
79
80// QueryBugs return the id of all Bug matching the given Query
81func (c *RepoCacheBug) QueryBugs(q *query.Query) ([]entity.Id, error) {
82 c.mu.RLock()
83 defer c.mu.RUnlock()
84
85 if q == nil {
86 return c.AllIds(), nil
87 }
88
89 matcher := compileMatcher(q.Filters)
90
91 var filtered []*BugExcerpt
92 var foundBySearch map[entity.Id]*BugExcerpt
93
94 if q.Search != nil {
95 foundBySearch = map[entity.Id]*BugExcerpt{}
96
97 terms := make([]string, len(q.Search))
98 copy(terms, q.Search)
99 for i, search := range q.Search {
100 if strings.Contains(search, " ") {
101 terms[i] = fmt.Sprintf("\"%s\"", search)
102 }
103 }
104
105 bleveQuery := bleve.NewQueryStringQuery(strings.Join(terms, " "))
106 bleveSearch := bleve.NewSearchRequest(bleveQuery)
107
108 index, err := c.repo.GetBleveIndex("bug")
109 if err != nil {
110 return nil, err
111 }
112
113 searchResults, err := index.Search(bleveSearch)
114 if err != nil {
115 return nil, err
116 }
117
118 for _, hit := range searchResults.Hits {
119 foundBySearch[entity.Id(hit.ID)] = c.excerpts[entity.Id(hit.ID)]
120 }
121 } else {
122 foundBySearch = c.excerpts
123 }
124
125 for _, excerpt := range foundBySearch {
126 if matcher.Match(excerpt, c) {
127 filtered = append(filtered, excerpt)
128 }
129 }
130
131 var sorter sort.Interface
132
133 switch q.OrderBy {
134 case query.OrderById:
135 sorter = BugsById(filtered)
136 case query.OrderByCreation:
137 sorter = BugsByCreationTime(filtered)
138 case query.OrderByEdit:
139 sorter = BugsByEditTime(filtered)
140 default:
141 return nil, errors.New("missing sort type")
142 }
143
144 switch q.OrderDirection {
145 case query.OrderAscending:
146 // Nothing to do
147 case query.OrderDescending:
148 sorter = sort.Reverse(sorter)
149 default:
150 return nil, errors.New("missing sort direction")
151 }
152
153 sort.Sort(sorter)
154
155 result := make([]entity.Id, len(filtered))
156
157 for i, val := range filtered {
158 result[i] = val.Id()
159 }
160
161 return result, nil
162}
163
164// ValidLabels list valid labels
165//
166// Note: in the future, a proper label policy could be implemented where valid
167// labels are defined in a configuration file. Until that, the default behavior
168// is to return the list of labels already used.
169func (c *RepoCacheBug) ValidLabels() []bug.Label {
170 c.mu.RLock()
171 defer c.mu.RUnlock()
172
173 set := map[bug.Label]interface{}{}
174
175 for _, excerpt := range c.excerpts {
176 for _, l := range excerpt.Labels {
177 set[l] = nil
178 }
179 }
180
181 result := make([]bug.Label, len(set))
182
183 i := 0
184 for l := range set {
185 result[i] = l
186 i++
187 }
188
189 // Sort
190 sort.Slice(result, func(i, j int) bool {
191 return string(result[i]) < string(result[j])
192 })
193
194 return result
195}
196
197// New create a new bug
198// The new bug is written in the repository (commit)
199func (c *RepoCacheBug) New(title string, message string) (*BugCache, *bug.CreateOperation, error) {
200 return c.NewWithFiles(title, message, nil)
201}
202
203// NewWithFiles create a new bug with attached files for the message
204// The new bug is written in the repository (commit)
205func (c *RepoCacheBug) NewWithFiles(title string, message string, files []repository.Hash) (*BugCache, *bug.CreateOperation, error) {
206 author, err := c.getUserIdentity()
207 if err != nil {
208 return nil, nil, err
209 }
210
211 return c.NewRaw(author, time.Now().Unix(), title, message, files, nil)
212}
213
214// NewRaw create a new bug with attached files for the message, as
215// well as metadata for the Create operation.
216// The new bug is written in the repository (commit)
217func (c *RepoCacheBug) NewRaw(author identity.Interface, unixTime int64, title string, message string, files []repository.Hash, metadata map[string]string) (*BugCache, *bug.CreateOperation, error) {
218 b, op, err := bug.Create(author, unixTime, title, message, files, metadata)
219 if err != nil {
220 return nil, nil, err
221 }
222
223 err = b.Commit(c.repo)
224 if err != nil {
225 return nil, nil, err
226 }
227
228 cached, err := c.add(b)
229 if err != nil {
230 return nil, nil, err
231 }
232
233 return cached, op, nil
234}
235
236func (c *RepoCacheBug) addBugToSearchIndex(snap *bug.Snapshot) error {
237 searchableBug := struct {
238 Text []string
239 }{}
240
241 // See https://github.com/blevesearch/bleve/issues/1576
242 var sb strings.Builder
243 normalize := func(text string) string {
244 sb.Reset()
245 for _, field := range strings.Fields(text) {
246 if utf8.RuneCountInString(field) < 100 {
247 sb.WriteString(field)
248 sb.WriteRune(' ')
249 }
250 }
251 return sb.String()
252 }
253
254 for _, comment := range snap.Comments {
255 searchableBug.Text = append(searchableBug.Text, normalize(comment.Message))
256 }
257
258 searchableBug.Text = append(searchableBug.Text, normalize(snap.Title))
259
260 index, err := c.repo.GetBleveIndex("bug")
261 if err != nil {
262 return err
263 }
264
265 err = index.Index(snap.Id().String(), searchableBug)
266 if err != nil {
267 return err
268 }
269
270 return nil
271}