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