forked from MichaelMure/git-bug
-
Notifications
You must be signed in to change notification settings - Fork 3
/
snapshot.go
265 lines (230 loc) · 7.2 KB
/
snapshot.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
package bug
import (
"fmt"
"time"
"github.com/daedaleanai/git-ticket/bug/review"
"github.com/daedaleanai/git-ticket/entity"
"github.com/daedaleanai/git-ticket/identity"
"github.com/pkg/errors"
)
// Snapshot is a compiled form of the Bug data structure used for storage and merge
type Snapshot struct {
id entity.Id
Status Status
Title string
Comments []Comment
Labels []Label
Checklists map[Label]map[entity.Id]ChecklistSnapshot // label and reviewer id
Reviews map[string]review.PullRequest // Pull request ID
Author identity.Interface
Assignee identity.Interface
Actors []identity.Interface
Participants []identity.Interface
Ccb []CcbInfo
CreateTime time.Time
Timeline []TimelineItem
Operations []Operation
}
// Return the Bug identifier
func (snap *Snapshot) Id() entity.Id {
return snap.id
}
// Return the last time a bug was modified
func (snap *Snapshot) EditTime() time.Time {
if len(snap.Operations) == 0 {
return time.Unix(0, 0)
}
return snap.Operations[len(snap.Operations)-1].Time()
}
// GetCreateMetadata return the creation metadata
func (snap *Snapshot) GetCreateMetadata(key string) (string, bool) {
return snap.Operations[0].GetMetadata(key)
}
// SearchComment will search for a comment matching the given hash
func (snap *Snapshot) SearchComment(id entity.Id) (*Comment, error) {
for _, c := range snap.Comments {
if c.id == id {
return &c, nil
}
}
return nil, fmt.Errorf("comment item not found")
}
// GetComment will return the comment for a given index
func (snap *Snapshot) GetComment(index int) (*Comment, error) {
if index < len(snap.Comments) {
return &snap.Comments[index], nil
}
return nil, fmt.Errorf("comment item not found")
}
// append the operation author to the actors list
func (snap *Snapshot) addActor(actor identity.Interface) {
for _, a := range snap.Actors {
if actor.Id() == a.Id() {
return
}
}
snap.Actors = append(snap.Actors, actor)
}
// append the operation author to the participants list
func (snap *Snapshot) addParticipant(participant identity.Interface) {
for _, p := range snap.Participants {
if participant.Id() == p.Id() {
return
}
}
snap.Participants = append(snap.Participants, participant)
}
// HasParticipant return true if the id is a participant
func (snap *Snapshot) HasParticipant(id entity.Id) bool {
for _, p := range snap.Participants {
if p.Id() == id {
return true
}
}
return false
}
// HasAnyParticipant return true if one of the ids is a participant
func (snap *Snapshot) HasAnyParticipant(ids ...entity.Id) bool {
for _, id := range ids {
if snap.HasParticipant(id) {
return true
}
}
return false
}
// HasActor return true if the id is a actor
func (snap *Snapshot) HasActor(id entity.Id) bool {
for _, p := range snap.Actors {
if p.Id() == id {
return true
}
}
return false
}
// HasAnyActor return true if one of the ids is a actor
func (snap *Snapshot) HasAnyActor(ids ...entity.Id) bool {
for _, id := range ids {
if snap.HasActor(id) {
return true
}
}
return false
}
// GetCcbState returns the state assocated with the id in the ticket CCB group
func (snap *Snapshot) GetCcbState(id entity.Id, status Status) CcbState {
for _, c := range snap.Ccb {
if c.User.Id() == id && c.Status == status {
return c.State
}
}
return RemovedCcbState
}
// Sign post method for gqlgen
func (snap *Snapshot) IsAuthored() {}
// GetUserChecklists returns a map of checklists associated with this snapshot for the given reviewer id,
// if the blank flag is set then always return a clean set of checklists
func (snap *Snapshot) GetUserChecklists(reviewer entity.Id, blank bool) (map[Label]Checklist, error) {
checklists := make(map[Label]Checklist)
// Only checklists named in the labels list are currently valid
for _, l := range snap.Labels {
if l.IsChecklist() {
if snapshotChecklist, present := snap.Checklists[l][reviewer]; !blank && present {
checklists[l] = snapshotChecklist.Checklist
} else {
var err error
checklists[l], err = GetChecklist(l)
if err != nil {
return nil, err
}
}
}
}
return checklists, nil
}
// GetChecklistCompoundStates returns a map of checklist states mapped to label, associated with this snapshot
func (snap *Snapshot) GetChecklistCompoundStates() map[Label]ChecklistState {
states := make(map[Label]ChecklistState)
// Only checklists named in the labels list are currently valid
for _, l := range snap.Labels {
if l.IsChecklist() {
// default state is TBD
states[l] = TBD
clMap, present := snap.Checklists[l]
if present {
// at least one user has edited this checklist
ReviewsLoop:
for _, cl := range clMap {
clState := cl.CompoundState()
switch clState {
case Failed:
// someone failed it, it's failed
states[l] = Failed
break ReviewsLoop
case Passed:
// someone passed it, and no-one failed it yet
states[l] = Passed
}
}
}
}
}
return states
}
// NextStatuses returns a slice of next possible statuses for the assigned workflow
func (snap *Snapshot) NextStatuses() ([]Status, error) {
w := FindWorkflow(snap.Labels)
if w == nil {
return nil, fmt.Errorf("ticket has no associated workflow")
}
return w.NextStatuses(snap.Status), nil
}
// ValidateTransitionAndApplyActions returns an error if the supplied state is an invalid
// destination from the current state for the assigned workflow. If the destination status
// is not invalid, then it applies the entry actions defined in the workflow
func (snap *Snapshot) ValidateTransitionAndApplyActions(b Interface, newStatus Status, author identity.Interface, unixTime int64) error {
w := FindWorkflow(snap.Labels)
if w == nil {
return fmt.Errorf("ticket has no associated workflow")
}
if err := w.ValidateTransition(snap, newStatus); err != nil {
return err
}
return w.ApplyTransitionActions(b, snap, newStatus, author, unixTime)
}
// ValidateAssigneeSet returns an error if the snapshot assignee is not set
func ValidateAssigneeSet(snap *Snapshot, next Status) error {
if snap.Assignee == nil {
return errors.New("assignee not set")
}
return nil
}
// ValidateCcb returns an error if the snapshot does not have CCB set and approved for the next status
func ValidateCcb(snap *Snapshot, next Status) error {
var ccbAssigned int
// Loop through the entire CCB list, each entry represents an approval: a ticket status plus
// a CCB member who should approve it
for _, approval := range snap.Ccb {
if approval.Status == next {
// This approval is needed for the requested 'next' status
ccbAssigned++
if approval.State != ApprovedCcbState {
return fmt.Errorf("not all CCB have approved ticket status %s", next)
}
}
}
// Check at least one approval is associated with the requested status
if ccbAssigned == 0 {
return fmt.Errorf("no CCB assigned to ticket status %s", next)
}
return nil
}
// ValidateChecklistsCompleted returns an error if at least one of the checklists attached to the snapshot
// has not been completed
func ValidateChecklistsCompleted(snap *Snapshot, next Status) error {
for _, st := range snap.GetChecklistCompoundStates() {
if st == TBD {
return errors.New("at least one checklist still TBD")
}
}
return nil
}