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