op_label_change.go

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