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