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, resolver identity.Resolver) (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 *EditCommentOperation:
117		op.Author_ = author
118	case *LabelChangeOperation:
119		op.Author_ = author
120	case *NoOpOperation:
121		op.Author_ = author
122	case *SetMetadataOperation:
123		op.Author_ = author
124	case *SetStatusOperation:
125		op.Author_ = author
126	case *SetTitleOperation:
127		op.Author_ = author
128	default:
129		panic(fmt.Sprintf("unknown operation type %T", op))
130	}
131
132	return op, nil
133}
134
135// OpBase implement the common code for all operations
136type OpBase struct {
137	OperationType OperationType      `json:"type"`
138	Author_       identity.Interface `json:"author"`
139	// TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
140	UnixTime int64             `json:"timestamp"`
141	Metadata map[string]string `json:"metadata,omitempty"`
142
143	// mandatory random bytes to ensure a better randomness of the data used to later generate the ID
144	// len(Nonce) should be > 20 and < 64 bytes
145	// It has no functional purpose and should be ignored.
146	Nonce []byte `json:"nonce"`
147
148	// Not serialized. Store the op's id in memory.
149	id entity.Id
150	// Not serialized. Store the extra metadata in memory,
151	// compiled from SetMetadataOperation.
152	extraMetadata map[string]string
153}
154
155// newOpBase is the constructor for an OpBase
156func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
157	return OpBase{
158		OperationType: opType,
159		Author_:       author,
160		UnixTime:      unixTime,
161		Nonce:         makeNonce(20),
162		id:            entity.UnsetId,
163	}
164}
165
166func makeNonce(len int) []byte {
167	result := make([]byte, len)
168	_, err := rand.Read(result)
169	if err != nil {
170		panic(err)
171	}
172	return result
173}
174
175func (base *OpBase) UnmarshalJSON(data []byte) error {
176	// Compute the Id when loading the op from disk.
177	base.id = entity.DeriveId(data)
178
179	aux := struct {
180		OperationType OperationType     `json:"type"`
181		Author        json.RawMessage   `json:"author"`
182		UnixTime      int64             `json:"timestamp"`
183		Metadata      map[string]string `json:"metadata,omitempty"`
184		Nonce         []byte            `json:"nonce"`
185	}{}
186
187	if err := json.Unmarshal(data, &aux); err != nil {
188		return err
189	}
190
191	// delegate the decoding of the identity
192	author, err := identity.UnmarshalJSON(aux.Author)
193	if err != nil {
194		return err
195	}
196
197	base.OperationType = aux.OperationType
198	base.Author_ = author
199	base.UnixTime = aux.UnixTime
200	base.Metadata = aux.Metadata
201	base.Nonce = aux.Nonce
202
203	return nil
204}
205
206func (base *OpBase) Type() OperationType {
207	return base.OperationType
208}
209
210// Time return the time when the operation was added
211func (base *OpBase) Time() time.Time {
212	return time.Unix(base.UnixTime, 0)
213}
214
215// Validate check the OpBase for errors
216func (base *OpBase) Validate(op Operation, opType OperationType) error {
217	if base.OperationType != opType {
218		return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
219	}
220
221	if op.Time().Unix() == 0 {
222		return fmt.Errorf("time not set")
223	}
224
225	if base.Author_ == nil {
226		return fmt.Errorf("author not set")
227	}
228
229	if err := op.Author().Validate(); err != nil {
230		return errors.Wrap(err, "author")
231	}
232
233	if op, ok := op.(dag.OperationWithFiles); ok {
234		for _, hash := range op.GetFiles() {
235			if !hash.IsValid() {
236				return fmt.Errorf("file with invalid hash %v", hash)
237			}
238		}
239	}
240
241	if len(base.Nonce) > 64 {
242		return fmt.Errorf("nonce is too big")
243	}
244	if len(base.Nonce) < 20 {
245		return fmt.Errorf("nonce is too small")
246	}
247
248	return nil
249}
250
251// SetMetadata store arbitrary metadata about the operation
252func (base *OpBase) SetMetadata(key string, value string) {
253	if base.Metadata == nil {
254		base.Metadata = make(map[string]string)
255	}
256
257	base.Metadata[key] = value
258	base.id = entity.UnsetId
259}
260
261// GetMetadata retrieve arbitrary metadata about the operation
262func (base *OpBase) GetMetadata(key string) (string, bool) {
263	val, ok := base.Metadata[key]
264
265	if ok {
266		return val, true
267	}
268
269	// extraMetadata can't replace the original operations value if any
270	val, ok = base.extraMetadata[key]
271
272	return val, ok
273}
274
275// AllMetadata return all metadata for this operation
276func (base *OpBase) AllMetadata() map[string]string {
277	result := make(map[string]string)
278
279	for key, val := range base.extraMetadata {
280		result[key] = val
281	}
282
283	// Original metadata take precedence
284	for key, val := range base.Metadata {
285		result[key] = val
286	}
287
288	return result
289}
290
291func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
292	if base.extraMetadata == nil {
293		base.extraMetadata = make(map[string]string)
294	}
295	if _, exist := base.extraMetadata[key]; !exist {
296		base.extraMetadata[key] = value
297	}
298}
299
300// Author return author identity
301func (base *OpBase) Author() identity.Interface {
302	return base.Author_
303}