operation.go

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