2018-09-28 21:39:39 +03:00
|
|
|
package bug
|
2018-07-18 17:41:09 +03:00
|
|
|
|
|
|
|
import (
|
2019-01-19 21:23:31 +03:00
|
|
|
"encoding/json"
|
2018-07-25 19:01:32 +03:00
|
|
|
"fmt"
|
2018-07-18 17:41:09 +03:00
|
|
|
"sort"
|
2018-08-06 21:31:20 +03:00
|
|
|
|
2018-11-21 20:56:12 +03:00
|
|
|
"github.com/MichaelMure/git-bug/identity"
|
2019-02-25 01:05:03 +03:00
|
|
|
"github.com/MichaelMure/git-bug/util/timestamp"
|
2018-11-21 20:56:12 +03:00
|
|
|
|
2018-09-29 00:51:47 +03:00
|
|
|
"github.com/MichaelMure/git-bug/util/git"
|
2018-09-15 14:15:00 +03:00
|
|
|
"github.com/pkg/errors"
|
2018-07-18 17:41:09 +03:00
|
|
|
)
|
|
|
|
|
2018-09-29 21:41:19 +03:00
|
|
|
var _ Operation = &LabelChangeOperation{}
|
2018-07-18 17:41:09 +03:00
|
|
|
|
2018-08-13 16:28:16 +03:00
|
|
|
// LabelChangeOperation define a Bug operation to add or remove labels
|
2018-07-18 17:41:09 +03:00
|
|
|
type LabelChangeOperation struct {
|
2018-10-01 12:37:17 +03:00
|
|
|
OpBase
|
2019-01-19 21:23:31 +03:00
|
|
|
Added []Label
|
|
|
|
Removed []Label
|
2018-09-28 21:39:39 +03:00
|
|
|
}
|
|
|
|
|
2018-09-29 21:41:19 +03:00
|
|
|
func (op *LabelChangeOperation) base() *OpBase {
|
2018-10-01 12:37:17 +03:00
|
|
|
return &op.OpBase
|
2018-07-18 17:41:09 +03:00
|
|
|
}
|
|
|
|
|
2018-09-29 21:41:19 +03:00
|
|
|
func (op *LabelChangeOperation) Hash() (git.Hash, error) {
|
2018-09-29 00:51:47 +03:00
|
|
|
return hashOperation(op)
|
|
|
|
}
|
|
|
|
|
2018-08-13 16:28:16 +03:00
|
|
|
// Apply apply the operation
|
2018-09-29 21:41:19 +03:00
|
|
|
func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
|
2019-03-31 23:32:35 +03:00
|
|
|
snapshot.addActor(op.Author)
|
|
|
|
|
2018-07-18 17:41:09 +03:00
|
|
|
// Add in the set
|
|
|
|
AddLoop:
|
|
|
|
for _, added := range op.Added {
|
|
|
|
for _, label := range snapshot.Labels {
|
|
|
|
if label == added {
|
|
|
|
// Already exist
|
|
|
|
continue AddLoop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
snapshot.Labels = append(snapshot.Labels, added)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove in the set
|
|
|
|
for _, removed := range op.Removed {
|
|
|
|
for i, label := range snapshot.Labels {
|
|
|
|
if label == removed {
|
|
|
|
snapshot.Labels[i] = snapshot.Labels[len(snapshot.Labels)-1]
|
|
|
|
snapshot.Labels = snapshot.Labels[:len(snapshot.Labels)-1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sort
|
|
|
|
sort.Slice(snapshot.Labels, func(i, j int) bool {
|
|
|
|
return string(snapshot.Labels[i]) < string(snapshot.Labels[j])
|
|
|
|
})
|
2018-09-29 21:41:19 +03:00
|
|
|
|
2018-09-30 18:15:54 +03:00
|
|
|
hash, err := op.Hash()
|
|
|
|
if err != nil {
|
|
|
|
// Should never error unless a programming error happened
|
|
|
|
// (covered in OpBase.Validate())
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
item := &LabelChangeTimelineItem{
|
|
|
|
hash: hash,
|
|
|
|
Author: op.Author,
|
2019-02-25 01:05:03 +03:00
|
|
|
UnixTime: timestamp.Timestamp(op.UnixTime),
|
2018-09-30 18:15:54 +03:00
|
|
|
Added: op.Added,
|
|
|
|
Removed: op.Removed,
|
|
|
|
}
|
|
|
|
|
|
|
|
snapshot.Timeline = append(snapshot.Timeline, item)
|
2018-07-18 17:41:09 +03:00
|
|
|
}
|
2018-07-25 19:01:32 +03:00
|
|
|
|
2018-09-29 21:41:19 +03:00
|
|
|
func (op *LabelChangeOperation) Validate() error {
|
2018-09-28 21:39:39 +03:00
|
|
|
if err := opBaseValidate(op, LabelChangeOp); err != nil {
|
2018-09-15 14:15:00 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, l := range op.Added {
|
|
|
|
if err := l.Validate(); err != nil {
|
|
|
|
return errors.Wrap(err, "added label")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, l := range op.Removed {
|
|
|
|
if err := l.Validate(); err != nil {
|
|
|
|
return errors.Wrap(err, "removed label")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(op.Added)+len(op.Removed) <= 0 {
|
|
|
|
return fmt.Errorf("no label change")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-01-19 21:23:31 +03:00
|
|
|
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
|
|
|
// MarshalJSON
|
|
|
|
func (op *LabelChangeOperation) MarshalJSON() ([]byte, error) {
|
|
|
|
base, err := json.Marshal(op.OpBase)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// revert back to a flat map to be able to add our own fields
|
|
|
|
var data map[string]interface{}
|
|
|
|
if err := json.Unmarshal(base, &data); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
data["added"] = op.Added
|
|
|
|
data["removed"] = op.Removed
|
|
|
|
|
|
|
|
return json.Marshal(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
|
|
|
|
// MarshalJSON
|
|
|
|
func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
|
|
|
|
// Unmarshal OpBase and the op separately
|
|
|
|
|
|
|
|
base := OpBase{}
|
|
|
|
err := json.Unmarshal(data, &base)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
aux := struct {
|
|
|
|
Added []Label `json:"added"`
|
|
|
|
Removed []Label `json:"removed"`
|
|
|
|
}{}
|
|
|
|
|
|
|
|
err = json.Unmarshal(data, &aux)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
op.OpBase = base
|
|
|
|
op.Added = aux.Added
|
|
|
|
op.Removed = aux.Removed
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-12-23 19:11:37 +03:00
|
|
|
// Sign post method for gqlgen
|
|
|
|
func (op *LabelChangeOperation) IsAuthored() {}
|
|
|
|
|
2018-11-21 20:56:12 +03:00
|
|
|
func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
|
2018-09-29 21:41:19 +03:00
|
|
|
return &LabelChangeOperation{
|
2018-09-28 21:39:39 +03:00
|
|
|
OpBase: newOpBase(LabelChangeOp, author, unixTime),
|
2018-07-25 22:25:26 +03:00
|
|
|
Added: added,
|
|
|
|
Removed: removed,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-30 18:15:54 +03:00
|
|
|
type LabelChangeTimelineItem struct {
|
|
|
|
hash git.Hash
|
2018-11-21 20:56:12 +03:00
|
|
|
Author identity.Interface
|
2019-02-25 01:05:03 +03:00
|
|
|
UnixTime timestamp.Timestamp
|
2018-09-30 18:15:54 +03:00
|
|
|
Added []Label
|
|
|
|
Removed []Label
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l LabelChangeTimelineItem) Hash() git.Hash {
|
|
|
|
return l.hash
|
|
|
|
}
|
|
|
|
|
2018-08-13 16:28:16 +03:00
|
|
|
// ChangeLabels is a convenience function to apply the operation
|
2018-11-21 20:56:12 +03:00
|
|
|
func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
|
2018-09-28 21:39:39 +03:00
|
|
|
var added, removed []Label
|
2018-09-13 13:20:28 +03:00
|
|
|
var results []LabelChangeResult
|
2018-08-01 22:57:12 +03:00
|
|
|
|
2018-07-25 19:01:32 +03:00
|
|
|
snap := b.Compile()
|
|
|
|
|
|
|
|
for _, str := range add {
|
2018-09-28 21:39:39 +03:00
|
|
|
label := Label(str)
|
2018-07-25 19:01:32 +03:00
|
|
|
|
|
|
|
// check for duplicate
|
|
|
|
if labelExist(added, label) {
|
2018-09-13 13:20:28 +03:00
|
|
|
results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
|
2018-07-25 19:01:32 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// check that the label doesn't already exist
|
|
|
|
if labelExist(snap.Labels, label) {
|
2018-09-13 13:20:28 +03:00
|
|
|
results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAlreadySet})
|
2018-07-25 19:01:32 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
added = append(added, label)
|
2018-09-13 13:20:28 +03:00
|
|
|
results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAdded})
|
2018-07-25 19:01:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, str := range remove {
|
2018-09-28 21:39:39 +03:00
|
|
|
label := Label(str)
|
2018-07-25 19:01:32 +03:00
|
|
|
|
|
|
|
// check for duplicate
|
|
|
|
if labelExist(removed, label) {
|
2018-09-13 13:20:28 +03:00
|
|
|
results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
|
2018-07-25 19:01:32 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// check that the label actually exist
|
|
|
|
if !labelExist(snap.Labels, label) {
|
2018-09-13 13:20:28 +03:00
|
|
|
results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDoesntExist})
|
2018-07-25 19:01:32 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
removed = append(removed, label)
|
2018-09-13 13:20:28 +03:00
|
|
|
results = append(results, LabelChangeResult{Label: label, Status: LabelChangeRemoved})
|
2018-07-25 19:01:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(added) == 0 && len(removed) == 0 {
|
2018-10-02 00:31:16 +03:00
|
|
|
return results, nil, fmt.Errorf("no label added or removed")
|
2018-07-25 19:01:32 +03:00
|
|
|
}
|
|
|
|
|
2018-09-25 18:56:58 +03:00
|
|
|
labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
|
2018-07-25 19:01:32 +03:00
|
|
|
|
2018-09-15 14:15:00 +03:00
|
|
|
if err := labelOp.Validate(); err != nil {
|
2018-10-02 00:31:16 +03:00
|
|
|
return nil, nil, err
|
2018-09-15 14:15:00 +03:00
|
|
|
}
|
|
|
|
|
2018-07-25 19:01:32 +03:00
|
|
|
b.Append(labelOp)
|
|
|
|
|
2018-10-02 00:31:16 +03:00
|
|
|
return results, labelOp, nil
|
2018-07-25 19:01:32 +03:00
|
|
|
}
|
|
|
|
|
2019-05-04 14:19:56 +03:00
|
|
|
// ForceChangeLabels is a convenience function to apply the operation
|
|
|
|
// The difference with ChangeLabels is that no checks of deduplications are done. You are entirely
|
|
|
|
// responsible of what you are doing. In the general case, you want to use ChangeLabels instead.
|
|
|
|
// The intended use of this function is to allow importers to create legal but unexpected label changes,
|
|
|
|
// like removing a label with no information of when it was added before.
|
|
|
|
func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) (*LabelChangeOperation, error) {
|
|
|
|
added := make([]Label, len(add))
|
|
|
|
for i, str := range add {
|
|
|
|
added[i] = Label(str)
|
|
|
|
}
|
|
|
|
|
|
|
|
removed := make([]Label, len(remove))
|
|
|
|
for i, str := range remove {
|
|
|
|
removed[i] = Label(str)
|
|
|
|
}
|
|
|
|
|
|
|
|
labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
|
|
|
|
|
|
|
|
if err := labelOp.Validate(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
b.Append(labelOp)
|
|
|
|
|
|
|
|
return labelOp, nil
|
|
|
|
}
|
|
|
|
|
2018-09-28 21:39:39 +03:00
|
|
|
func labelExist(labels []Label, label Label) bool {
|
2018-07-25 19:01:32 +03:00
|
|
|
for _, l := range labels {
|
|
|
|
if l == label {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
2018-09-13 13:20:28 +03:00
|
|
|
|
|
|
|
type LabelChangeStatus int
|
|
|
|
|
|
|
|
const (
|
|
|
|
_ LabelChangeStatus = iota
|
|
|
|
LabelChangeAdded
|
|
|
|
LabelChangeRemoved
|
|
|
|
LabelChangeDuplicateInOp
|
|
|
|
LabelChangeAlreadySet
|
|
|
|
LabelChangeDoesntExist
|
|
|
|
)
|
|
|
|
|
|
|
|
type LabelChangeResult struct {
|
2018-09-28 21:39:39 +03:00
|
|
|
Label Label
|
2018-09-13 13:20:28 +03:00
|
|
|
Status LabelChangeStatus
|
|
|
|
}
|
2018-09-17 15:33:34 +03:00
|
|
|
|
|
|
|
func (l LabelChangeResult) String() string {
|
|
|
|
switch l.Status {
|
|
|
|
case LabelChangeAdded:
|
|
|
|
return fmt.Sprintf("label %s added", l.Label)
|
|
|
|
case LabelChangeRemoved:
|
|
|
|
return fmt.Sprintf("label %s removed", l.Label)
|
|
|
|
case LabelChangeDuplicateInOp:
|
|
|
|
return fmt.Sprintf("label %s is a duplicate", l.Label)
|
|
|
|
case LabelChangeAlreadySet:
|
|
|
|
return fmt.Sprintf("label %s was already set", l.Label)
|
|
|
|
case LabelChangeDoesntExist:
|
|
|
|
return fmt.Sprintf("label %s doesn't exist on this bug", l.Label)
|
|
|
|
default:
|
|
|
|
panic(fmt.Sprintf("unknown label change status %v", l.Status))
|
|
|
|
}
|
|
|
|
}
|
2019-05-22 22:46:43 +03:00
|
|
|
|
|
|
|
// Sign post method for gqlgen
|
|
|
|
func (item *LabelChangeTimelineItem) IsAuthored() {}
|