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