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