op_label_change.go

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