1package bug
2
3import (
4 "fmt"
5 "io"
6 "sort"
7 "strconv"
8
9 "github.com/pkg/errors"
10
11 "github.com/MichaelMure/git-bug/entity"
12 "github.com/MichaelMure/git-bug/entity/dag"
13 "github.com/MichaelMure/git-bug/util/timestamp"
14)
15
16var _ Operation = &LabelChangeOperation{}
17
18// LabelChangeOperation define a Bug operation to add or remove labels
19type LabelChangeOperation struct {
20 dag.OpBase
21 Added []Label `json:"added"`
22 Removed []Label `json:"removed"`
23}
24
25func (op *LabelChangeOperation) Id() entity.Id {
26 return dag.IdOperation(op, &op.OpBase)
27}
28
29// Apply applies the operation
30func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
31 snapshot.addActor(op.Author())
32
33 // Add in the set
34AddLoop:
35 for _, added := range op.Added {
36 for _, label := range snapshot.Labels {
37 if label == added {
38 // Already exist
39 continue AddLoop
40 }
41 }
42
43 snapshot.Labels = append(snapshot.Labels, added)
44 }
45
46 // Remove in the set
47 for _, removed := range op.Removed {
48 for i, label := range snapshot.Labels {
49 if label == removed {
50 snapshot.Labels[i] = snapshot.Labels[len(snapshot.Labels)-1]
51 snapshot.Labels = snapshot.Labels[:len(snapshot.Labels)-1]
52 }
53 }
54 }
55
56 // Sort
57 sort.Slice(snapshot.Labels, func(i, j int) bool {
58 return string(snapshot.Labels[i]) < string(snapshot.Labels[j])
59 })
60
61 id := op.Id()
62 item := &LabelChangeTimelineItem{
63 // id: id,
64 combinedId: entity.CombineIds(snapshot.Id(), id),
65 Author: op.Author(),
66 UnixTime: timestamp.Timestamp(op.UnixTime),
67 Added: op.Added,
68 Removed: op.Removed,
69 }
70
71 snapshot.Timeline = append(snapshot.Timeline, item)
72}
73
74func (op *LabelChangeOperation) Validate() error {
75 if err := op.OpBase.Validate(op, LabelChangeOp); err != nil {
76 return err
77 }
78
79 for _, l := range op.Added {
80 if err := l.Validate(); err != nil {
81 return errors.Wrap(err, "added label")
82 }
83 }
84
85 for _, l := range op.Removed {
86 if err := l.Validate(); err != nil {
87 return errors.Wrap(err, "removed label")
88 }
89 }
90
91 if len(op.Added)+len(op.Removed) <= 0 {
92 return fmt.Errorf("no label change")
93 }
94
95 return nil
96}
97
98func NewLabelChangeOperation(author entity.Identity, unixTime int64, added, removed []Label) *LabelChangeOperation {
99 return &LabelChangeOperation{
100 OpBase: dag.NewOpBase(LabelChangeOp, author, unixTime),
101 Added: added,
102 Removed: removed,
103 }
104}
105
106type LabelChangeTimelineItem struct {
107 combinedId entity.CombinedId
108 Author entity.Identity
109 UnixTime timestamp.Timestamp
110 Added []Label
111 Removed []Label
112}
113
114func (l LabelChangeTimelineItem) CombinedId() entity.CombinedId {
115 return l.combinedId
116}
117
118// IsAuthored is a sign post method for gqlgen
119func (l *LabelChangeTimelineItem) IsAuthored() {}
120
121// ChangeLabels is a convenience function to change labels on a bug
122func ChangeLabels(b Interface, author entity.Identity, unixTime int64, add, remove []string, metadata map[string]string) ([]LabelChangeResult, *LabelChangeOperation, error) {
123 var added, removed []Label
124 var results []LabelChangeResult
125
126 snap := b.Compile()
127
128 for _, str := range add {
129 label := Label(str)
130
131 // check for duplicate
132 if labelExist(added, label) {
133 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
134 continue
135 }
136
137 // check that the label doesn't already exist
138 if labelExist(snap.Labels, label) {
139 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAlreadySet})
140 continue
141 }
142
143 added = append(added, label)
144 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAdded})
145 }
146
147 for _, str := range remove {
148 label := Label(str)
149
150 // check for duplicate
151 if labelExist(removed, label) {
152 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
153 continue
154 }
155
156 // check that the label actually exist
157 if !labelExist(snap.Labels, label) {
158 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDoesntExist})
159 continue
160 }
161
162 removed = append(removed, label)
163 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeRemoved})
164 }
165
166 if len(added) == 0 && len(removed) == 0 {
167 return results, nil, fmt.Errorf("no label added or removed")
168 }
169
170 op := NewLabelChangeOperation(author, unixTime, added, removed)
171 for key, val := range metadata {
172 op.SetMetadata(key, val)
173 }
174 if err := op.Validate(); err != nil {
175 return nil, nil, err
176 }
177
178 b.Append(op)
179
180 return results, op, nil
181}
182
183// ForceChangeLabels is a convenience function to apply the operation
184// The difference with ChangeLabels is that no checks for deduplication are done. You are entirely
185// responsible for what you are doing. In the general case, you want to use ChangeLabels instead.
186// The intended use of this function is to allow importers to create legal but unexpected label changes,
187// like removing a label with no information of when it was added before.
188func ForceChangeLabels(b Interface, author entity.Identity, unixTime int64, add, remove []string, metadata map[string]string) (*LabelChangeOperation, error) {
189 added := make([]Label, len(add))
190 for i, str := range add {
191 added[i] = Label(str)
192 }
193
194 removed := make([]Label, len(remove))
195 for i, str := range remove {
196 removed[i] = Label(str)
197 }
198
199 op := NewLabelChangeOperation(author, unixTime, added, removed)
200
201 for key, val := range metadata {
202 op.SetMetadata(key, val)
203 }
204 if err := op.Validate(); err != nil {
205 return nil, err
206 }
207
208 b.Append(op)
209
210 return op, nil
211}
212
213func labelExist(labels []Label, label Label) bool {
214 for _, l := range labels {
215 if l == label {
216 return true
217 }
218 }
219
220 return false
221}
222
223type LabelChangeStatus int
224
225const (
226 _ LabelChangeStatus = iota
227 LabelChangeAdded
228 LabelChangeRemoved
229 LabelChangeDuplicateInOp
230 LabelChangeAlreadySet
231 LabelChangeDoesntExist
232)
233
234func (l LabelChangeStatus) MarshalGQL(w io.Writer) {
235 switch l {
236 case LabelChangeAdded:
237 _, _ = fmt.Fprintf(w, strconv.Quote("ADDED"))
238 case LabelChangeRemoved:
239 _, _ = fmt.Fprintf(w, strconv.Quote("REMOVED"))
240 case LabelChangeDuplicateInOp:
241 _, _ = fmt.Fprintf(w, strconv.Quote("DUPLICATE_IN_OP"))
242 case LabelChangeAlreadySet:
243 _, _ = fmt.Fprintf(w, strconv.Quote("ALREADY_EXIST"))
244 case LabelChangeDoesntExist:
245 _, _ = fmt.Fprintf(w, strconv.Quote("DOESNT_EXIST"))
246 default:
247 panic("missing case")
248 }
249}
250
251func (l *LabelChangeStatus) UnmarshalGQL(v interface{}) error {
252 str, ok := v.(string)
253 if !ok {
254 return fmt.Errorf("enums must be strings")
255 }
256 switch str {
257 case "ADDED":
258 *l = LabelChangeAdded
259 case "REMOVED":
260 *l = LabelChangeRemoved
261 case "DUPLICATE_IN_OP":
262 *l = LabelChangeDuplicateInOp
263 case "ALREADY_EXIST":
264 *l = LabelChangeAlreadySet
265 case "DOESNT_EXIST":
266 *l = LabelChangeDoesntExist
267 default:
268 return fmt.Errorf("%s is not a valid LabelChangeStatus", str)
269 }
270 return nil
271}
272
273type LabelChangeResult struct {
274 Label Label
275 Status LabelChangeStatus
276}
277
278func (l LabelChangeResult) String() string {
279 switch l.Status {
280 case LabelChangeAdded:
281 return fmt.Sprintf("label %s added", l.Label)
282 case LabelChangeRemoved:
283 return fmt.Sprintf("label %s removed", l.Label)
284 case LabelChangeDuplicateInOp:
285 return fmt.Sprintf("label %s is a duplicate", l.Label)
286 case LabelChangeAlreadySet:
287 return fmt.Sprintf("label %s was already set", l.Label)
288 case LabelChangeDoesntExist:
289 return fmt.Sprintf("label %s doesn't exist on this bug", l.Label)
290 default:
291 panic(fmt.Sprintf("unknown label change status %v", l.Status))
292 }
293}