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