operation.go

  1package bug
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"time"
  7
  8	"github.com/pkg/errors"
  9
 10	"github.com/MichaelMure/git-bug/entity"
 11	"github.com/MichaelMure/git-bug/entity/dag"
 12	"github.com/MichaelMure/git-bug/identity"
 13	"github.com/MichaelMure/git-bug/repository"
 14)
 15
 16// OperationType is an operation type identifier
 17type OperationType int
 18
 19const (
 20	_ OperationType = iota
 21	CreateOp
 22	SetTitleOp
 23	AddCommentOp
 24	SetStatusOp
 25	LabelChangeOp
 26	EditCommentOp
 27	NoOpOp
 28	SetMetadataOp
 29)
 30
 31// Operation define the interface to fulfill for an edit operation of a Bug
 32type Operation interface {
 33	dag.Operation
 34
 35	// Type return the type of the operation
 36	Type() OperationType
 37
 38	// Time return the time when the operation was added
 39	Time() time.Time
 40	// GetFiles return the files needed by this operation
 41	GetFiles() []repository.Hash
 42	// Apply the operation to a Snapshot to create the final state
 43	Apply(snapshot *Snapshot)
 44	// SetMetadata store arbitrary metadata about the operation
 45	SetMetadata(key string, value string)
 46	// GetMetadata retrieve arbitrary metadata about the operation
 47	GetMetadata(key string) (string, bool)
 48	// AllMetadata return all metadata for this operation
 49	AllMetadata() map[string]string
 50
 51	setExtraMetadataImmutable(key string, value string)
 52
 53	// sign-post method for gqlgen
 54	IsOperation()
 55}
 56
 57func idOperation(op Operation, base *OpBase) entity.Id {
 58	if base.id == "" {
 59		// something went really wrong
 60		panic("op's id not set")
 61	}
 62	if base.id == entity.UnsetId {
 63		// This means we are trying to get the op's Id *before* it has been stored, for instance when
 64		// adding multiple ops in one go in an OperationPack.
 65		// As the Id is computed based on the actual bytes written on the disk, we are going to predict
 66		// those and then get the Id. This is safe as it will be the exact same code writing on disk later.
 67
 68		data, err := json.Marshal(op)
 69		if err != nil {
 70			panic(err)
 71		}
 72
 73		base.id = entity.DeriveId(data)
 74	}
 75	return base.id
 76}
 77
 78func operationUnmarshaller(author identity.Interface, raw json.RawMessage) (dag.Operation, error) {
 79	var t struct {
 80		OperationType OperationType `json:"type"`
 81	}
 82
 83	if err := json.Unmarshal(raw, &t); err != nil {
 84		return nil, err
 85	}
 86
 87	var op Operation
 88
 89	switch t.OperationType {
 90	case AddCommentOp:
 91		op = &AddCommentOperation{}
 92	case CreateOp:
 93		op = &CreateOperation{}
 94	case EditCommentOp:
 95		op = &EditCommentOperation{}
 96	case LabelChangeOp:
 97		op = &LabelChangeOperation{}
 98	case NoOpOp:
 99		op = &NoOpOperation{}
100	case SetMetadataOp:
101		op = &SetMetadataOperation{}
102	case SetStatusOp:
103		op = &SetStatusOperation{}
104	case SetTitleOp:
105		op = &SetTitleOperation{}
106	default:
107		panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
108	}
109
110	err := json.Unmarshal(raw, &op)
111	if err != nil {
112		return nil, err
113	}
114
115	switch op := op.(type) {
116	case *AddCommentOperation:
117		op.Author_ = author
118	case *CreateOperation:
119		op.Author_ = author
120	case *LabelChangeOperation:
121		op.Author_ = author
122	case *NoOpOperation:
123		op.Author_ = author
124	case *SetMetadataOperation:
125		op.Author_ = author
126	case *SetStatusOperation:
127		op.Author_ = author
128	case *SetTitleOperation:
129		op.Author_ = author
130	default:
131		panic(fmt.Sprintf("unknown operation type %T", op))
132	}
133
134	return op, nil
135}
136
137// OpBase implement the common code for all operations
138type OpBase struct {
139	OperationType OperationType      `json:"type"`
140	Author_       identity.Interface `json:"author"`
141	// TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
142	UnixTime int64             `json:"timestamp"`
143	Metadata map[string]string `json:"metadata,omitempty"`
144	// Not serialized. Store the op's id in memory.
145	id entity.Id
146	// Not serialized. Store the extra metadata in memory,
147	// compiled from SetMetadataOperation.
148	extraMetadata map[string]string
149}
150
151// newOpBase is the constructor for an OpBase
152func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
153	return OpBase{
154		OperationType: opType,
155		Author_:       author,
156		UnixTime:      unixTime,
157		id:            entity.UnsetId,
158	}
159}
160
161func (base *OpBase) UnmarshalJSON(data []byte) error {
162	// Compute the Id when loading the op from disk.
163	base.id = entity.DeriveId(data)
164
165	aux := struct {
166		OperationType OperationType     `json:"type"`
167		Author        json.RawMessage   `json:"author"`
168		UnixTime      int64             `json:"timestamp"`
169		Metadata      map[string]string `json:"metadata,omitempty"`
170	}{}
171
172	if err := json.Unmarshal(data, &aux); err != nil {
173		return err
174	}
175
176	// delegate the decoding of the identity
177	author, err := identity.UnmarshalJSON(aux.Author)
178	if err != nil {
179		return err
180	}
181
182	base.OperationType = aux.OperationType
183	base.Author_ = author
184	base.UnixTime = aux.UnixTime
185	base.Metadata = aux.Metadata
186
187	return nil
188}
189
190func (base *OpBase) Type() OperationType {
191	return base.OperationType
192}
193
194// Time return the time when the operation was added
195func (base *OpBase) Time() time.Time {
196	return time.Unix(base.UnixTime, 0)
197}
198
199// GetFiles return the files needed by this operation
200func (base *OpBase) GetFiles() []repository.Hash {
201	return nil
202}
203
204// Validate check the OpBase for errors
205func (base *OpBase) Validate(op Operation, opType OperationType) error {
206	if base.OperationType != opType {
207		return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
208	}
209
210	if op.Time().Unix() == 0 {
211		return fmt.Errorf("time not set")
212	}
213
214	if base.Author_ == nil {
215		return fmt.Errorf("author not set")
216	}
217
218	if err := op.Author().Validate(); err != nil {
219		return errors.Wrap(err, "author")
220	}
221
222	for _, hash := range op.GetFiles() {
223		if !hash.IsValid() {
224			return fmt.Errorf("file with invalid hash %v", hash)
225		}
226	}
227
228	return nil
229}
230
231// SetMetadata store arbitrary metadata about the operation
232func (base *OpBase) SetMetadata(key string, value string) {
233	if base.Metadata == nil {
234		base.Metadata = make(map[string]string)
235	}
236
237	base.Metadata[key] = value
238	base.id = entity.UnsetId
239}
240
241// GetMetadata retrieve arbitrary metadata about the operation
242func (base *OpBase) GetMetadata(key string) (string, bool) {
243	val, ok := base.Metadata[key]
244
245	if ok {
246		return val, true
247	}
248
249	// extraMetadata can't replace the original operations value if any
250	val, ok = base.extraMetadata[key]
251
252	return val, ok
253}
254
255// AllMetadata return all metadata for this operation
256func (base *OpBase) AllMetadata() map[string]string {
257	result := make(map[string]string)
258
259	for key, val := range base.extraMetadata {
260		result[key] = val
261	}
262
263	// Original metadata take precedence
264	for key, val := range base.Metadata {
265		result[key] = val
266	}
267
268	return result
269}
270
271func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
272	if base.extraMetadata == nil {
273		base.extraMetadata = make(map[string]string)
274	}
275	if _, exist := base.extraMetadata[key]; !exist {
276		base.extraMetadata[key] = value
277	}
278}
279
280// Author return author identity
281func (base *OpBase) Author() identity.Interface {
282	return base.Author_
283}