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