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	"github.com/MichaelMure/git-bug/repository"
 15)
 16
 17// OperationType is an operation type identifier
 18type OperationType int
 19
 20const (
 21	_ OperationType = iota
 22	CreateOp
 23	SetTitleOp
 24	AddCommentOp
 25	SetStatusOp
 26	LabelChangeOp
 27	EditCommentOp
 28	NoOpOp
 29	SetMetadataOp
 30)
 31
 32// Operation define the interface to fulfill for an edit operation of a Bug
 33type Operation interface {
 34	dag.Operation
 35
 36	// Type return the type of the operation
 37	Type() OperationType
 38
 39	// Time return the time when the operation was added
 40	Time() time.Time
 41	// GetFiles return the files needed by this operation
 42	GetFiles() []repository.Hash
 43	// Apply the operation to a Snapshot to create the final state
 44	Apply(snapshot *Snapshot)
 45	// SetMetadata store arbitrary metadata about the operation
 46	SetMetadata(key string, value string)
 47	// GetMetadata retrieve arbitrary metadata about the operation
 48	GetMetadata(key string) (string, bool)
 49	// AllMetadata return all metadata for this operation
 50	AllMetadata() map[string]string
 51
 52	setExtraMetadataImmutable(key string, value string)
 53}
 54
 55func idOperation(op Operation, base *OpBase) entity.Id {
 56	if base.id == "" {
 57		// something went really wrong
 58		panic("op's id not set")
 59	}
 60	if base.id == entity.UnsetId {
 61		// This means we are trying to get the op's Id *before* it has been stored, for instance when
 62		// adding multiple ops in one go in an OperationPack.
 63		// As the Id is computed based on the actual bytes written on the disk, we are going to predict
 64		// those and then get the Id. This is safe as it will be the exact same code writing on disk later.
 65
 66		data, err := json.Marshal(op)
 67		if err != nil {
 68			panic(err)
 69		}
 70
 71		base.id = entity.DeriveId(data)
 72	}
 73	return base.id
 74}
 75
 76func operationUnmarshaller(author identity.Interface, raw json.RawMessage) (dag.Operation, error) {
 77	var t struct {
 78		OperationType OperationType `json:"type"`
 79	}
 80
 81	if err := json.Unmarshal(raw, &t); err != nil {
 82		return nil, err
 83	}
 84
 85	var op Operation
 86
 87	switch t.OperationType {
 88	case AddCommentOp:
 89		op = &AddCommentOperation{}
 90	case CreateOp:
 91		op = &CreateOperation{}
 92	case EditCommentOp:
 93		op = &EditCommentOperation{}
 94	case LabelChangeOp:
 95		op = &LabelChangeOperation{}
 96	case NoOpOp:
 97		op = &NoOpOperation{}
 98	case SetMetadataOp:
 99		op = &SetMetadataOperation{}
100	case SetStatusOp:
101		op = &SetStatusOperation{}
102	case SetTitleOp:
103		op = &SetTitleOperation{}
104	default:
105		panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
106	}
107
108	err := json.Unmarshal(raw, &op)
109	if err != nil {
110		return nil, err
111	}
112
113	switch op := op.(type) {
114	case *AddCommentOperation:
115		op.Author_ = author
116	case *CreateOperation:
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// GetFiles return the files needed by this operation
216func (base *OpBase) GetFiles() []repository.Hash {
217	return nil
218}
219
220// Validate check the OpBase for errors
221func (base *OpBase) Validate(op Operation, opType OperationType) error {
222	if base.OperationType != opType {
223		return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
224	}
225
226	if op.Time().Unix() == 0 {
227		return fmt.Errorf("time not set")
228	}
229
230	if base.Author_ == nil {
231		return fmt.Errorf("author not set")
232	}
233
234	if err := op.Author().Validate(); err != nil {
235		return errors.Wrap(err, "author")
236	}
237
238	for _, hash := range op.GetFiles() {
239		if !hash.IsValid() {
240			return fmt.Errorf("file with invalid hash %v", hash)
241		}
242	}
243
244	if len(base.Nonce) > 64 {
245		return fmt.Errorf("nonce is too big")
246	}
247	if len(base.Nonce) < 20 {
248		return fmt.Errorf("nonce is too small")
249	}
250
251	return nil
252}
253
254// SetMetadata store arbitrary metadata about the operation
255func (base *OpBase) SetMetadata(key string, value string) {
256	if base.Metadata == nil {
257		base.Metadata = make(map[string]string)
258	}
259
260	base.Metadata[key] = value
261	base.id = entity.UnsetId
262}
263
264// GetMetadata retrieve arbitrary metadata about the operation
265func (base *OpBase) GetMetadata(key string) (string, bool) {
266	val, ok := base.Metadata[key]
267
268	if ok {
269		return val, true
270	}
271
272	// extraMetadata can't replace the original operations value if any
273	val, ok = base.extraMetadata[key]
274
275	return val, ok
276}
277
278// AllMetadata return all metadata for this operation
279func (base *OpBase) AllMetadata() map[string]string {
280	result := make(map[string]string)
281
282	for key, val := range base.extraMetadata {
283		result[key] = val
284	}
285
286	// Original metadata take precedence
287	for key, val := range base.Metadata {
288		result[key] = val
289	}
290
291	return result
292}
293
294func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
295	if base.extraMetadata == nil {
296		base.extraMetadata = make(map[string]string)
297	}
298	if _, exist := base.extraMetadata[key]; !exist {
299		base.extraMetadata[key] = value
300	}
301}
302
303// Author return author identity
304func (base *OpBase) Author() identity.Interface {
305	return base.Author_
306}