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}