op_label_change.go

  1package bug
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"sort"
  7
  8	"github.com/MichaelMure/git-bug/identity"
  9	"github.com/MichaelMure/git-bug/util/timestamp"
 10
 11	"github.com/MichaelMure/git-bug/util/git"
 12	"github.com/pkg/errors"
 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
 21	Removed []Label
 22}
 23
 24func (op *LabelChangeOperation) base() *OpBase {
 25	return &op.OpBase
 26}
 27
 28func (op *LabelChangeOperation) Hash() (git.Hash, error) {
 29	return hashOperation(op)
 30}
 31
 32// Apply apply the operation
 33func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
 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	hash, err := op.Hash()
 63	if err != nil {
 64		// Should never error unless a programming error happened
 65		// (covered in OpBase.Validate())
 66		panic(err)
 67	}
 68
 69	item := &LabelChangeTimelineItem{
 70		hash:     hash,
 71		Author:   op.Author,
 72		UnixTime: timestamp.Timestamp(op.UnixTime),
 73		Added:    op.Added,
 74		Removed:  op.Removed,
 75	}
 76
 77	snapshot.Timeline = append(snapshot.Timeline, item)
 78}
 79
 80func (op *LabelChangeOperation) Validate() error {
 81	if err := opBaseValidate(op, LabelChangeOp); err != nil {
 82		return err
 83	}
 84
 85	for _, l := range op.Added {
 86		if err := l.Validate(); err != nil {
 87			return errors.Wrap(err, "added label")
 88		}
 89	}
 90
 91	for _, l := range op.Removed {
 92		if err := l.Validate(); err != nil {
 93			return errors.Wrap(err, "removed label")
 94		}
 95	}
 96
 97	if len(op.Added)+len(op.Removed) <= 0 {
 98		return fmt.Errorf("no label change")
 99	}
100
101	return nil
102}
103
104// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
105// MarshalJSON
106func (op *LabelChangeOperation) MarshalJSON() ([]byte, error) {
107	base, err := json.Marshal(op.OpBase)
108	if err != nil {
109		return nil, err
110	}
111
112	// revert back to a flat map to be able to add our own fields
113	var data map[string]interface{}
114	if err := json.Unmarshal(base, &data); err != nil {
115		return nil, err
116	}
117
118	data["added"] = op.Added
119	data["removed"] = op.Removed
120
121	return json.Marshal(data)
122}
123
124// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
125// MarshalJSON
126func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
127	// Unmarshal OpBase and the op separately
128
129	base := OpBase{}
130	err := json.Unmarshal(data, &base)
131	if err != nil {
132		return err
133	}
134
135	aux := struct {
136		Added   []Label `json:"added"`
137		Removed []Label `json:"removed"`
138	}{}
139
140	err = json.Unmarshal(data, &aux)
141	if err != nil {
142		return err
143	}
144
145	op.OpBase = base
146	op.Added = aux.Added
147	op.Removed = aux.Removed
148
149	return nil
150}
151
152// Sign post method for gqlgen
153func (op *LabelChangeOperation) IsAuthored() {}
154
155func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
156	return &LabelChangeOperation{
157		OpBase:  newOpBase(LabelChangeOp, author, unixTime),
158		Added:   added,
159		Removed: removed,
160	}
161}
162
163type LabelChangeTimelineItem struct {
164	hash     git.Hash
165	Author   identity.Interface
166	UnixTime timestamp.Timestamp
167	Added    []Label
168	Removed  []Label
169}
170
171func (l LabelChangeTimelineItem) Hash() git.Hash {
172	return l.hash
173}
174
175// ChangeLabels is a convenience function to apply the operation
176func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
177	var added, removed []Label
178	var results []LabelChangeResult
179
180	snap := b.Compile()
181
182	for _, str := range add {
183		label := Label(str)
184
185		// check for duplicate
186		if labelExist(added, label) {
187			results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
188			continue
189		}
190
191		// check that the label doesn't already exist
192		if labelExist(snap.Labels, label) {
193			results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAlreadySet})
194			continue
195		}
196
197		added = append(added, label)
198		results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAdded})
199	}
200
201	for _, str := range remove {
202		label := Label(str)
203
204		// check for duplicate
205		if labelExist(removed, label) {
206			results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
207			continue
208		}
209
210		// check that the label actually exist
211		if !labelExist(snap.Labels, label) {
212			results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDoesntExist})
213			continue
214		}
215
216		removed = append(removed, label)
217		results = append(results, LabelChangeResult{Label: label, Status: LabelChangeRemoved})
218	}
219
220	if len(added) == 0 && len(removed) == 0 {
221		return results, nil, fmt.Errorf("no label added or removed")
222	}
223
224	labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
225
226	if err := labelOp.Validate(); err != nil {
227		return nil, nil, err
228	}
229
230	b.Append(labelOp)
231
232	return results, labelOp, nil
233}
234
235func labelExist(labels []Label, label Label) bool {
236	for _, l := range labels {
237		if l == label {
238			return true
239		}
240	}
241
242	return false
243}
244
245type LabelChangeStatus int
246
247const (
248	_ LabelChangeStatus = iota
249	LabelChangeAdded
250	LabelChangeRemoved
251	LabelChangeDuplicateInOp
252	LabelChangeAlreadySet
253	LabelChangeDoesntExist
254)
255
256type LabelChangeResult struct {
257	Label  Label
258	Status LabelChangeStatus
259}
260
261func (l LabelChangeResult) String() string {
262	switch l.Status {
263	case LabelChangeAdded:
264		return fmt.Sprintf("label %s added", l.Label)
265	case LabelChangeRemoved:
266		return fmt.Sprintf("label %s removed", l.Label)
267	case LabelChangeDuplicateInOp:
268		return fmt.Sprintf("label %s is a duplicate", l.Label)
269	case LabelChangeAlreadySet:
270		return fmt.Sprintf("label %s was already set", l.Label)
271	case LabelChangeDoesntExist:
272		return fmt.Sprintf("label %s doesn't exist on this bug", l.Label)
273	default:
274		panic(fmt.Sprintf("unknown label change status %v", l.Status))
275	}
276}