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