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