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
24func (op *LabelChangeOperation) base() *OpBase {
25 return &op.OpBase
26}
27
28func (op *LabelChangeOperation) Id() entity.Id {
29 return idOperation(op)
30}
31
32// Apply apply the operation
33func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
34 snapshot.addActor(op.Author)
35
36 // Add in the set
37AddLoop:
38 for _, added := range op.Added {
39 for _, label := range snapshot.Labels {
40 if label == added {
41 // Already exist
42 continue AddLoop
43 }
44 }
45
46 snapshot.Labels = append(snapshot.Labels, added)
47 }
48
49 // Remove in the set
50 for _, removed := range op.Removed {
51 for i, label := range snapshot.Labels {
52 if label == removed {
53 snapshot.Labels[i] = snapshot.Labels[len(snapshot.Labels)-1]
54 snapshot.Labels = snapshot.Labels[:len(snapshot.Labels)-1]
55 }
56 }
57 }
58
59 // Sort
60 sort.Slice(snapshot.Labels, func(i, j int) bool {
61 return string(snapshot.Labels[i]) < string(snapshot.Labels[j])
62 })
63
64 item := &LabelChangeTimelineItem{
65 id: op.Id(),
66 Author: op.Author,
67 UnixTime: timestamp.Timestamp(op.UnixTime),
68 Added: op.Added,
69 Removed: op.Removed,
70 }
71
72 snapshot.Timeline = append(snapshot.Timeline, item)
73}
74
75func (op *LabelChangeOperation) Validate() error {
76 if err := opBaseValidate(op, LabelChangeOp); err != nil {
77 return err
78 }
79
80 for _, l := range op.Added {
81 if err := l.Validate(); err != nil {
82 return errors.Wrap(err, "added label")
83 }
84 }
85
86 for _, l := range op.Removed {
87 if err := l.Validate(); err != nil {
88 return errors.Wrap(err, "removed label")
89 }
90 }
91
92 if len(op.Added)+len(op.Removed) <= 0 {
93 return fmt.Errorf("no label change")
94 }
95
96 return nil
97}
98
99// UnmarshalJSON is a two step JSON unmarshaling
100// This workaround is necessary to avoid the inner OpBase.MarshalJSON
101// overriding the outer op's MarshalJSON
102func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
103 // Unmarshal OpBase and the op separately
104
105 base := OpBase{}
106 err := json.Unmarshal(data, &base)
107 if err != nil {
108 return err
109 }
110
111 aux := struct {
112 Added []Label `json:"added"`
113 Removed []Label `json:"removed"`
114 }{}
115
116 err = json.Unmarshal(data, &aux)
117 if err != nil {
118 return err
119 }
120
121 op.OpBase = base
122 op.Added = aux.Added
123 op.Removed = aux.Removed
124
125 return nil
126}
127
128// Sign post method for gqlgen
129func (op *LabelChangeOperation) IsAuthored() {}
130
131func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
132 return &LabelChangeOperation{
133 OpBase: newOpBase(LabelChangeOp, author, unixTime),
134 Added: added,
135 Removed: removed,
136 }
137}
138
139type LabelChangeTimelineItem struct {
140 id entity.Id
141 Author identity.Interface
142 UnixTime timestamp.Timestamp
143 Added []Label
144 Removed []Label
145}
146
147func (l LabelChangeTimelineItem) Id() entity.Id {
148 return l.id
149}
150
151// Sign post method for gqlgen
152func (l *LabelChangeTimelineItem) IsAuthored() {}
153
154// ChangeLabels is a convenience function to apply the operation
155func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
156 var added, removed []Label
157 var results []LabelChangeResult
158
159 snap := b.Compile()
160
161 for _, str := range add {
162 label := Label(str)
163
164 // check for duplicate
165 if labelExist(added, label) {
166 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
167 continue
168 }
169
170 // check that the label doesn't already exist
171 if labelExist(snap.Labels, label) {
172 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAlreadySet})
173 continue
174 }
175
176 added = append(added, label)
177 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAdded})
178 }
179
180 for _, str := range remove {
181 label := Label(str)
182
183 // check for duplicate
184 if labelExist(removed, label) {
185 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
186 continue
187 }
188
189 // check that the label actually exist
190 if !labelExist(snap.Labels, label) {
191 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDoesntExist})
192 continue
193 }
194
195 removed = append(removed, label)
196 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeRemoved})
197 }
198
199 if len(added) == 0 && len(removed) == 0 {
200 return results, nil, fmt.Errorf("no label added or removed")
201 }
202
203 labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
204
205 if err := labelOp.Validate(); err != nil {
206 return nil, nil, err
207 }
208
209 b.Append(labelOp)
210
211 return results, labelOp, nil
212}
213
214// ForceChangeLabels is a convenience function to apply the operation
215// The difference with ChangeLabels is that no checks of deduplications are done. You are entirely
216// responsible of what you are doing. In the general case, you want to use ChangeLabels instead.
217// The intended use of this function is to allow importers to create legal but unexpected label changes,
218// like removing a label with no information of when it was added before.
219func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) (*LabelChangeOperation, error) {
220 added := make([]Label, len(add))
221 for i, str := range add {
222 added[i] = Label(str)
223 }
224
225 removed := make([]Label, len(remove))
226 for i, str := range remove {
227 removed[i] = Label(str)
228 }
229
230 labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
231
232 if err := labelOp.Validate(); err != nil {
233 return nil, err
234 }
235
236 b.Append(labelOp)
237
238 return labelOp, nil
239}
240
241func labelExist(labels []Label, label Label) bool {
242 for _, l := range labels {
243 if l == label {
244 return true
245 }
246 }
247
248 return false
249}
250
251type LabelChangeStatus int
252
253const (
254 _ LabelChangeStatus = iota
255 LabelChangeAdded
256 LabelChangeRemoved
257 LabelChangeDuplicateInOp
258 LabelChangeAlreadySet
259 LabelChangeDoesntExist
260)
261
262type LabelChangeResult struct {
263 Label Label
264 Status LabelChangeStatus
265}
266
267func (l LabelChangeResult) String() string {
268 switch l.Status {
269 case LabelChangeAdded:
270 return fmt.Sprintf("label %s added", l.Label)
271 case LabelChangeRemoved:
272 return fmt.Sprintf("label %s removed", l.Label)
273 case LabelChangeDuplicateInOp:
274 return fmt.Sprintf("label %s is a duplicate", l.Label)
275 case LabelChangeAlreadySet:
276 return fmt.Sprintf("label %s was already set", l.Label)
277 case LabelChangeDoesntExist:
278 return fmt.Sprintf("label %s doesn't exist on this bug", l.Label)
279 default:
280 panic(fmt.Sprintf("unknown label change status %v", l.Status))
281 }
282}