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