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