op_label_change.go

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