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