op_label_change.go

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