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