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