bug_subcache.go

  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}