1package bug
2
3import (
4 "encoding/json"
5 "fmt"
6 "sort"
7
8 "github.com/pkg/errors"
9
10 "github.com/MichaelMure/git-bug/identity"
11 "github.com/MichaelMure/git-bug/util/timestamp"
12)
13
14var _ Operation = &LabelChangeOperation{}
15
16// LabelChangeOperation define a Bug operation to add or remove labels
17type LabelChangeOperation struct {
18 OpBase
19 Added []Label
20 Removed []Label
21}
22
23func (op *LabelChangeOperation) base() *OpBase {
24 return &op.OpBase
25}
26
27func (op *LabelChangeOperation) ID() string {
28 return idOperation(op)
29}
30
31// Apply apply the operation
32func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
33 snapshot.addActor(op.Author)
34
35 // Add in the set
36AddLoop:
37 for _, added := range op.Added {
38 for _, label := range snapshot.Labels {
39 if label == added {
40 // Already exist
41 continue AddLoop
42 }
43 }
44
45 snapshot.Labels = append(snapshot.Labels, added)
46 }
47
48 // Remove in the set
49 for _, removed := range op.Removed {
50 for i, label := range snapshot.Labels {
51 if label == removed {
52 snapshot.Labels[i] = snapshot.Labels[len(snapshot.Labels)-1]
53 snapshot.Labels = snapshot.Labels[:len(snapshot.Labels)-1]
54 }
55 }
56 }
57
58 // Sort
59 sort.Slice(snapshot.Labels, func(i, j int) bool {
60 return string(snapshot.Labels[i]) < string(snapshot.Labels[j])
61 })
62
63 item := &LabelChangeTimelineItem{
64 id: op.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 := opBaseValidate(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
98// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
99// MarshalJSON
100func (op *LabelChangeOperation) MarshalJSON() ([]byte, error) {
101 base, err := json.Marshal(op.OpBase)
102 if err != nil {
103 return nil, err
104 }
105
106 // revert back to a flat map to be able to add our own fields
107 var data map[string]interface{}
108 if err := json.Unmarshal(base, &data); err != nil {
109 return nil, err
110 }
111
112 data["added"] = op.Added
113 data["removed"] = op.Removed
114
115 return json.Marshal(data)
116}
117
118// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
119// MarshalJSON
120func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
121 // Unmarshal OpBase and the op separately
122
123 base := OpBase{}
124 err := json.Unmarshal(data, &base)
125 if err != nil {
126 return err
127 }
128
129 aux := struct {
130 Added []Label `json:"added"`
131 Removed []Label `json:"removed"`
132 }{}
133
134 err = json.Unmarshal(data, &aux)
135 if err != nil {
136 return err
137 }
138
139 op.OpBase = base
140 op.Added = aux.Added
141 op.Removed = aux.Removed
142
143 return nil
144}
145
146// Sign post method for gqlgen
147func (op *LabelChangeOperation) IsAuthored() {}
148
149func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
150 return &LabelChangeOperation{
151 OpBase: newOpBase(LabelChangeOp, author, unixTime),
152 Added: added,
153 Removed: removed,
154 }
155}
156
157type LabelChangeTimelineItem struct {
158 id string
159 Author identity.Interface
160 UnixTime timestamp.Timestamp
161 Added []Label
162 Removed []Label
163}
164
165func (l LabelChangeTimelineItem) ID() string {
166 return l.id
167}
168
169// Sign post method for gqlgen
170func (l *LabelChangeTimelineItem) IsAuthored() {}
171
172// ChangeLabels is a convenience function to apply the operation
173func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
174 var added, removed []Label
175 var results []LabelChangeResult
176
177 snap := b.Compile()
178
179 for _, str := range add {
180 label := Label(str)
181
182 // check for duplicate
183 if labelExist(added, label) {
184 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
185 continue
186 }
187
188 // check that the label doesn't already exist
189 if labelExist(snap.Labels, label) {
190 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAlreadySet})
191 continue
192 }
193
194 added = append(added, label)
195 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAdded})
196 }
197
198 for _, str := range remove {
199 label := Label(str)
200
201 // check for duplicate
202 if labelExist(removed, label) {
203 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
204 continue
205 }
206
207 // check that the label actually exist
208 if !labelExist(snap.Labels, label) {
209 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDoesntExist})
210 continue
211 }
212
213 removed = append(removed, label)
214 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeRemoved})
215 }
216
217 if len(added) == 0 && len(removed) == 0 {
218 return results, nil, fmt.Errorf("no label added or removed")
219 }
220
221 labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
222
223 if err := labelOp.Validate(); err != nil {
224 return nil, nil, err
225 }
226
227 b.Append(labelOp)
228
229 return results, labelOp, nil
230}
231
232// ForceChangeLabels is a convenience function to apply the operation
233// The difference with ChangeLabels is that no checks of deduplications are done. You are entirely
234// responsible of what you are doing. In the general case, you want to use ChangeLabels instead.
235// The intended use of this function is to allow importers to create legal but unexpected label changes,
236// like removing a label with no information of when it was added before.
237func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) (*LabelChangeOperation, error) {
238 added := make([]Label, len(add))
239 for i, str := range add {
240 added[i] = Label(str)
241 }
242
243 removed := make([]Label, len(remove))
244 for i, str := range remove {
245 removed[i] = Label(str)
246 }
247
248 labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
249
250 if err := labelOp.Validate(); err != nil {
251 return nil, err
252 }
253
254 b.Append(labelOp)
255
256 return labelOp, nil
257}
258
259func labelExist(labels []Label, label Label) bool {
260 for _, l := range labels {
261 if l == label {
262 return true
263 }
264 }
265
266 return false
267}
268
269type LabelChangeStatus int
270
271const (
272 _ LabelChangeStatus = iota
273 LabelChangeAdded
274 LabelChangeRemoved
275 LabelChangeDuplicateInOp
276 LabelChangeAlreadySet
277 LabelChangeDoesntExist
278)
279
280type LabelChangeResult struct {
281 Label Label
282 Status LabelChangeStatus
283}
284
285func (l LabelChangeResult) String() string {
286 switch l.Status {
287 case LabelChangeAdded:
288 return fmt.Sprintf("label %s added", l.Label)
289 case LabelChangeRemoved:
290 return fmt.Sprintf("label %s removed", l.Label)
291 case LabelChangeDuplicateInOp:
292 return fmt.Sprintf("label %s is a duplicate", l.Label)
293 case LabelChangeAlreadySet:
294 return fmt.Sprintf("label %s was already set", l.Label)
295 case LabelChangeDoesntExist:
296 return fmt.Sprintf("label %s doesn't exist on this bug", l.Label)
297 default:
298 panic(fmt.Sprintf("unknown label change status %v", l.Status))
299 }
300}