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/entity"
 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	OpBase
 20	Added   []Label `json:"added"`
 21	Removed []Label `json:"removed"`
 22}
 23
 24func (op *LabelChangeOperation) base() *OpBase {
 25	return &op.OpBase
 26}
 27
 28func (op *LabelChangeOperation) Id() entity.Id {
 29	return idOperation(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	item := &LabelChangeTimelineItem{
 65		id:       op.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 := opBaseValidate(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
 99// UnmarshalJSON is a two step JSON unmarshaling
100// This workaround is necessary to avoid the inner OpBase.MarshalJSON
101// overriding the outer op's MarshalJSON
102func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
103	// Unmarshal OpBase and the op separately
104
105	base := OpBase{}
106	err := json.Unmarshal(data, &base)
107	if err != nil {
108		return err
109	}
110
111	aux := struct {
112		Added   []Label `json:"added"`
113		Removed []Label `json:"removed"`
114	}{}
115
116	err = json.Unmarshal(data, &aux)
117	if err != nil {
118		return err
119	}
120
121	op.OpBase = base
122	op.Added = aux.Added
123	op.Removed = aux.Removed
124
125	return nil
126}
127
128// Sign post method for gqlgen
129func (op *LabelChangeOperation) IsAuthored() {}
130
131func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
132	return &LabelChangeOperation{
133		OpBase:  newOpBase(LabelChangeOp, author, unixTime),
134		Added:   added,
135		Removed: removed,
136	}
137}
138
139type LabelChangeTimelineItem struct {
140	id       entity.Id
141	Author   identity.Interface
142	UnixTime timestamp.Timestamp
143	Added    []Label
144	Removed  []Label
145}
146
147func (l LabelChangeTimelineItem) Id() entity.Id {
148	return l.id
149}
150
151// Sign post method for gqlgen
152func (l *LabelChangeTimelineItem) IsAuthored() {}
153
154// ChangeLabels is a convenience function to apply the operation
155func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
156	var added, removed []Label
157	var results []LabelChangeResult
158
159	snap := b.Compile()
160
161	for _, str := range add {
162		label := Label(str)
163
164		// check for duplicate
165		if labelExist(added, label) {
166			results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
167			continue
168		}
169
170		// check that the label doesn't already exist
171		if labelExist(snap.Labels, label) {
172			results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAlreadySet})
173			continue
174		}
175
176		added = append(added, label)
177		results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAdded})
178	}
179
180	for _, str := range remove {
181		label := Label(str)
182
183		// check for duplicate
184		if labelExist(removed, label) {
185			results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
186			continue
187		}
188
189		// check that the label actually exist
190		if !labelExist(snap.Labels, label) {
191			results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDoesntExist})
192			continue
193		}
194
195		removed = append(removed, label)
196		results = append(results, LabelChangeResult{Label: label, Status: LabelChangeRemoved})
197	}
198
199	if len(added) == 0 && len(removed) == 0 {
200		return results, nil, fmt.Errorf("no label added or removed")
201	}
202
203	labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
204
205	if err := labelOp.Validate(); err != nil {
206		return nil, nil, err
207	}
208
209	b.Append(labelOp)
210
211	return results, labelOp, nil
212}
213
214// ForceChangeLabels is a convenience function to apply the operation
215// The difference with ChangeLabels is that no checks of deduplications are done. You are entirely
216// responsible of what you are doing. In the general case, you want to use ChangeLabels instead.
217// The intended use of this function is to allow importers to create legal but unexpected label changes,
218// like removing a label with no information of when it was added before.
219func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) (*LabelChangeOperation, error) {
220	added := make([]Label, len(add))
221	for i, str := range add {
222		added[i] = Label(str)
223	}
224
225	removed := make([]Label, len(remove))
226	for i, str := range remove {
227		removed[i] = Label(str)
228	}
229
230	labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
231
232	if err := labelOp.Validate(); err != nil {
233		return nil, err
234	}
235
236	b.Append(labelOp)
237
238	return labelOp, nil
239}
240
241func labelExist(labels []Label, label Label) bool {
242	for _, l := range labels {
243		if l == label {
244			return true
245		}
246	}
247
248	return false
249}
250
251type LabelChangeStatus int
252
253const (
254	_ LabelChangeStatus = iota
255	LabelChangeAdded
256	LabelChangeRemoved
257	LabelChangeDuplicateInOp
258	LabelChangeAlreadySet
259	LabelChangeDoesntExist
260)
261
262type LabelChangeResult struct {
263	Label  Label
264	Status LabelChangeStatus
265}
266
267func (l LabelChangeResult) String() string {
268	switch l.Status {
269	case LabelChangeAdded:
270		return fmt.Sprintf("label %s added", l.Label)
271	case LabelChangeRemoved:
272		return fmt.Sprintf("label %s removed", l.Label)
273	case LabelChangeDuplicateInOp:
274		return fmt.Sprintf("label %s is a duplicate", l.Label)
275	case LabelChangeAlreadySet:
276		return fmt.Sprintf("label %s was already set", l.Label)
277	case LabelChangeDoesntExist:
278		return fmt.Sprintf("label %s doesn't exist on this bug", l.Label)
279	default:
280		panic(fmt.Sprintf("unknown label change status %v", l.Status))
281	}
282}