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