1package bug
2
3import (
4 "encoding/json"
5 "fmt"
6 "sort"
7
8 "github.com/MichaelMure/git-bug/identity"
9 "github.com/MichaelMure/git-bug/util/timestamp"
10
11 "github.com/MichaelMure/git-bug/util/git"
12 "github.com/pkg/errors"
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
21 Removed []Label
22}
23
24func (op *LabelChangeOperation) base() *OpBase {
25 return &op.OpBase
26}
27
28func (op *LabelChangeOperation) Hash() (git.Hash, error) {
29 return hashOperation(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 hash, err := op.Hash()
65 if err != nil {
66 // Should never error unless a programming error happened
67 // (covered in OpBase.Validate())
68 panic(err)
69 }
70
71 item := &LabelChangeTimelineItem{
72 hash: hash,
73 Author: op.Author,
74 UnixTime: timestamp.Timestamp(op.UnixTime),
75 Added: op.Added,
76 Removed: op.Removed,
77 }
78
79 snapshot.Timeline = append(snapshot.Timeline, item)
80}
81
82func (op *LabelChangeOperation) Validate() error {
83 if err := opBaseValidate(op, LabelChangeOp); err != nil {
84 return err
85 }
86
87 for _, l := range op.Added {
88 if err := l.Validate(); err != nil {
89 return errors.Wrap(err, "added label")
90 }
91 }
92
93 for _, l := range op.Removed {
94 if err := l.Validate(); err != nil {
95 return errors.Wrap(err, "removed label")
96 }
97 }
98
99 if len(op.Added)+len(op.Removed) <= 0 {
100 return fmt.Errorf("no label change")
101 }
102
103 return nil
104}
105
106// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
107// MarshalJSON
108func (op *LabelChangeOperation) MarshalJSON() ([]byte, error) {
109 base, err := json.Marshal(op.OpBase)
110 if err != nil {
111 return nil, err
112 }
113
114 // revert back to a flat map to be able to add our own fields
115 var data map[string]interface{}
116 if err := json.Unmarshal(base, &data); err != nil {
117 return nil, err
118 }
119
120 data["added"] = op.Added
121 data["removed"] = op.Removed
122
123 return json.Marshal(data)
124}
125
126// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
127// MarshalJSON
128func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
129 // Unmarshal OpBase and the op separately
130
131 base := OpBase{}
132 err := json.Unmarshal(data, &base)
133 if err != nil {
134 return err
135 }
136
137 aux := struct {
138 Added []Label `json:"added"`
139 Removed []Label `json:"removed"`
140 }{}
141
142 err = json.Unmarshal(data, &aux)
143 if err != nil {
144 return err
145 }
146
147 op.OpBase = base
148 op.Added = aux.Added
149 op.Removed = aux.Removed
150
151 return nil
152}
153
154// Sign post method for gqlgen
155func (op *LabelChangeOperation) IsAuthored() {}
156
157func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
158 return &LabelChangeOperation{
159 OpBase: newOpBase(LabelChangeOp, author, unixTime),
160 Added: added,
161 Removed: removed,
162 }
163}
164
165type LabelChangeTimelineItem struct {
166 hash git.Hash
167 Author identity.Interface
168 UnixTime timestamp.Timestamp
169 Added []Label
170 Removed []Label
171}
172
173func (l LabelChangeTimelineItem) Hash() git.Hash {
174 return l.hash
175}
176
177// Sign post method for gqlgen
178func (l *LabelChangeTimelineItem) IsAuthored() {}
179
180// ChangeLabels is a convenience function to apply the operation
181func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
182 var added, removed []Label
183 var results []LabelChangeResult
184
185 snap := b.Compile()
186
187 for _, str := range add {
188 label := Label(str)
189
190 // check for duplicate
191 if labelExist(added, label) {
192 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
193 continue
194 }
195
196 // check that the label doesn't already exist
197 if labelExist(snap.Labels, label) {
198 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAlreadySet})
199 continue
200 }
201
202 added = append(added, label)
203 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAdded})
204 }
205
206 for _, str := range remove {
207 label := Label(str)
208
209 // check for duplicate
210 if labelExist(removed, label) {
211 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
212 continue
213 }
214
215 // check that the label actually exist
216 if !labelExist(snap.Labels, label) {
217 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDoesntExist})
218 continue
219 }
220
221 removed = append(removed, label)
222 results = append(results, LabelChangeResult{Label: label, Status: LabelChangeRemoved})
223 }
224
225 if len(added) == 0 && len(removed) == 0 {
226 return results, nil, fmt.Errorf("no label added or removed")
227 }
228
229 labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
230
231 if err := labelOp.Validate(); err != nil {
232 return nil, nil, err
233 }
234
235 b.Append(labelOp)
236
237 return results, labelOp, nil
238}
239
240// ForceChangeLabels is a convenience function to apply the operation
241// The difference with ChangeLabels is that no checks of deduplications are done. You are entirely
242// responsible of what you are doing. In the general case, you want to use ChangeLabels instead.
243// The intended use of this function is to allow importers to create legal but unexpected label changes,
244// like removing a label with no information of when it was added before.
245func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) (*LabelChangeOperation, error) {
246 added := make([]Label, len(add))
247 for i, str := range add {
248 added[i] = Label(str)
249 }
250
251 removed := make([]Label, len(remove))
252 for i, str := range remove {
253 removed[i] = Label(str)
254 }
255
256 labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
257
258 if err := labelOp.Validate(); err != nil {
259 return nil, err
260 }
261
262 b.Append(labelOp)
263
264 return labelOp, nil
265}
266
267func labelExist(labels []Label, label Label) bool {
268 for _, l := range labels {
269 if l == label {
270 return true
271 }
272 }
273
274 return false
275}
276
277type LabelChangeStatus int
278
279const (
280 _ LabelChangeStatus = iota
281 LabelChangeAdded
282 LabelChangeRemoved
283 LabelChangeDuplicateInOp
284 LabelChangeAlreadySet
285 LabelChangeDoesntExist
286)
287
288type LabelChangeResult struct {
289 Label Label
290 Status LabelChangeStatus
291}
292
293func (l LabelChangeResult) String() string {
294 switch l.Status {
295 case LabelChangeAdded:
296 return fmt.Sprintf("label %s added", l.Label)
297 case LabelChangeRemoved:
298 return fmt.Sprintf("label %s removed", l.Label)
299 case LabelChangeDuplicateInOp:
300 return fmt.Sprintf("label %s is a duplicate", l.Label)
301 case LabelChangeAlreadySet:
302 return fmt.Sprintf("label %s was already set", l.Label)
303 case LabelChangeDoesntExist:
304 return fmt.Sprintf("label %s doesn't exist on this bug", l.Label)
305 default:
306 panic(fmt.Sprintf("unknown label change status %v", l.Status))
307 }
308}