op_label_change.go

  1package bug
  2
  3import (
  4	"fmt"
  5	"io"
  6	"sort"
  7	"strconv"
  8
  9	"github.com/pkg/errors"
 10
 11	"github.com/git-bug/git-bug/entities/common"
 12	"github.com/git-bug/git-bug/entities/identity"
 13	"github.com/git-bug/git-bug/entity"
 14	"github.com/git-bug/git-bug/entity/dag"
 15	"github.com/git-bug/git-bug/util/timestamp"
 16)
 17
 18var _ Operation = &LabelChangeOperation{}
 19
 20// LabelChangeOperation define a Bug operation to add or remove labels
 21type LabelChangeOperation struct {
 22	dag.OpBase
 23	Added   []common.Label `json:"added"`
 24	Removed []common.Label `json:"removed"`
 25}
 26
 27func (op *LabelChangeOperation) Id() entity.Id {
 28	return dag.IdOperation(op, &op.OpBase)
 29}
 30
 31// Apply applies the operation
 32func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
 33	snapshot.addActor(op.Author())
 34
 35	// Add in the set
 36AddLoop:
 37	for _, added := range op.Added {
 38		for _, label := range snapshot.Labels {
 39			if label == added {
 40				// Already exist
 41				continue AddLoop
 42			}
 43		}
 44
 45		snapshot.Labels = append(snapshot.Labels, added)
 46	}
 47
 48	// Remove in the set
 49	for _, removed := range op.Removed {
 50		for i, label := range snapshot.Labels {
 51			if label == removed {
 52				snapshot.Labels[i] = snapshot.Labels[len(snapshot.Labels)-1]
 53				snapshot.Labels = snapshot.Labels[:len(snapshot.Labels)-1]
 54			}
 55		}
 56	}
 57
 58	// Sort
 59	sort.Slice(snapshot.Labels, func(i, j int) bool {
 60		return string(snapshot.Labels[i]) < string(snapshot.Labels[j])
 61	})
 62
 63	id := op.Id()
 64	item := &LabelChangeTimelineItem{
 65		// id:         id,
 66		combinedId: entity.CombineIds(snapshot.Id(), id),
 67		Author:     op.Author(),
 68		UnixTime:   timestamp.Timestamp(op.UnixTime),
 69		Added:      op.Added,
 70		Removed:    op.Removed,
 71	}
 72
 73	snapshot.Timeline = append(snapshot.Timeline, item)
 74}
 75
 76func (op *LabelChangeOperation) Validate() error {
 77	if err := op.OpBase.Validate(op, LabelChangeOp); err != nil {
 78		return err
 79	}
 80
 81	for _, l := range op.Added {
 82		if err := l.Validate(); err != nil {
 83			return errors.Wrap(err, "added label")
 84		}
 85	}
 86
 87	for _, l := range op.Removed {
 88		if err := l.Validate(); err != nil {
 89			return errors.Wrap(err, "removed label")
 90		}
 91	}
 92
 93	if len(op.Added)+len(op.Removed) <= 0 {
 94		return fmt.Errorf("no label change")
 95	}
 96
 97	return nil
 98}
 99
100func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []common.Label) *LabelChangeOperation {
101	return &LabelChangeOperation{
102		OpBase:  dag.NewOpBase(LabelChangeOp, author, unixTime),
103		Added:   added,
104		Removed: removed,
105	}
106}
107
108type LabelChangeTimelineItem struct {
109	combinedId entity.CombinedId
110	Author     identity.Interface
111	UnixTime   timestamp.Timestamp
112	Added      []common.Label
113	Removed    []common.Label
114}
115
116func (l LabelChangeTimelineItem) CombinedId() entity.CombinedId {
117	return l.combinedId
118}
119
120// IsAuthored is a sign post-method for gqlgen, to mark compliance to an interface.
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 []common.Label
126	var results []LabelChangeResult
127
128	snap := b.Compile()
129
130	for _, str := range add {
131		label := common.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 := common.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([]common.Label, len(add))
192	for i, str := range add {
193		added[i] = common.Label(str)
194	}
195
196	removed := make([]common.Label, len(remove))
197	for i, str := range remove {
198		removed[i] = common.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 []common.Label, label common.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		_, _ = w.Write([]byte(strconv.Quote("ADDED")))
240	case LabelChangeRemoved:
241		_, _ = w.Write([]byte(strconv.Quote("REMOVED")))
242	case LabelChangeDuplicateInOp:
243		_, _ = w.Write([]byte(strconv.Quote("DUPLICATE_IN_OP")))
244	case LabelChangeAlreadySet:
245		_, _ = w.Write([]byte(strconv.Quote("ALREADY_EXIST")))
246	case LabelChangeDoesntExist:
247		_, _ = w.Write([]byte(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  common.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}