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