aboutsummaryrefslogblamecommitdiffstats
path: root/bug/operation.go
blob: b36103815233777373ccc7454a0d3c62a2bc188b (plain) (tree)
1
2
3
4
5
6
7
8
9
           
 
        
                     
                       
             
              
 
                               
 
                                               
                                                   
                                                 
 
 
                                                


                      




                              
                     
                     
              
                     

 
                                                                           
                          




                                                
                                                            
                        
                                                                      
                                 
 



                                                                      

                                                             

                                                           
 
 
                                                        


                                              
         

                                                                                                             
                                                                     

                                                                                                                   




                                             
 
                                               
         

                      
 









































                                                                                                   

                                   
















                                                                   
                                                      
                    
                                                      
                                                        


                                                                                                





                                                                                                         
                                                       
                    

                                                              
                                       

 
                                             
                                                                                        
                      
                                      
                                      
                                        
                                             
                                              
         

 








                                   
                                                      
                                                        
                                       
 




                                                                           
                                                              
           




                                                          





                                                         



                                              
                              



                  



                                          
                                                    

                                          

 
                                       


                                                                                                                    

         
                                  


                                                 
                                


                                                   
                                                      


                                                 




                                                                                    


                 






                                                       

                  

                                                           


                                                           

         

                                  


                                                              

                                                            





                                                                           
                                         
 

                      

                                                     
                                                     

                                         
                                                  



                                            
                                             



                                 
 
 











                                                                         
 
package bug

import (
	"crypto/rand"
	"encoding/json"
	"fmt"
	"time"

	"github.com/pkg/errors"

	"github.com/MichaelMure/git-bug/entity"
	"github.com/MichaelMure/git-bug/entity/dag"
	"github.com/MichaelMure/git-bug/identity"
)

// OperationType is an operation type identifier
type OperationType int

const (
	_ OperationType = iota
	CreateOp
	SetTitleOp
	AddCommentOp
	SetStatusOp
	LabelChangeOp
	EditCommentOp
	NoOpOp
	SetMetadataOp
)

// Operation define the interface to fulfill for an edit operation of a Bug
type Operation interface {
	dag.Operation

	// Type return the type of the operation
	Type() OperationType

	// Time return the time when the operation was added
	Time() time.Time
	// Apply the operation to a Snapshot to create the final state
	Apply(snapshot *Snapshot)

	// SetMetadata store arbitrary metadata about the operation
	SetMetadata(key string, value string)
	// GetMetadata retrieve arbitrary metadata about the operation
	GetMetadata(key string) (string, bool)
	// AllMetadata return all metadata for this operation
	AllMetadata() map[string]string

	setExtraMetadataImmutable(key string, value string)
}

func idOperation(op Operation, base *OpBase) entity.Id {
	if base.id == "" {
		// something went really wrong
		panic("op's id not set")
	}
	if base.id == entity.UnsetId {
		// This means we are trying to get the op's Id *before* it has been stored, for instance when
		// adding multiple ops in one go in an OperationPack.
		// As the Id is computed based on the actual bytes written on the disk, we are going to predict
		// those and then get the Id. This is safe as it will be the exact same code writing on disk later.

		data, err := json.Marshal(op)
		if err != nil {
			panic(err)
		}

		base.id = entity.DeriveId(data)
	}
	return base.id
}

func operationUnmarshaller(author identity.Interface, raw json.RawMessage) (dag.Operation, error) {
	var t struct {
		OperationType OperationType `json:"type"`
	}

	if err := json.Unmarshal(raw, &t); err != nil {
		return nil, err
	}

	var op Operation

	switch t.OperationType {
	case AddCommentOp:
		op = &AddCommentOperation{}
	case CreateOp:
		op = &CreateOperation{}
	case EditCommentOp:
		op = &EditCommentOperation{}
	case LabelChangeOp:
		op = &LabelChangeOperation{}
	case NoOpOp:
		op = &NoOpOperation{}
	case SetMetadataOp:
		op = &SetMetadataOperation{}
	case SetStatusOp:
		op = &SetStatusOperation{}
	case SetTitleOp:
		op = &SetTitleOperation{}
	default:
		panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
	}

	err := json.Unmarshal(raw, &op)
	if err != nil {
		return nil, err
	}

	switch op := op.(type) {
	case *AddCommentOperation:
		op.Author_ = author
	case *CreateOperation:
		op.Author_ = author
	case *EditCommentOperation:
		op.Author_ = author
	case *LabelChangeOperation:
		op.Author_ = author
	case *NoOpOperation:
		op.Author_ = author
	case *SetMetadataOperation:
		op.Author_ = author
	case *SetStatusOperation:
		op.Author_ = author
	case *SetTitleOperation:
		op.Author_ = author
	default:
		panic(fmt.Sprintf("unknown operation type %T", op))
	}

	return op, nil
}

// OpBase implement the common code for all operations
type OpBase struct {
	OperationType OperationType      `json:"type"`
	Author_       identity.Interface `json:"author"`
	// TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
	UnixTime int64             `json:"timestamp"`
	Metadata map[string]string `json:"metadata,omitempty"`

	// mandatory random bytes to ensure a better randomness of the data used to later generate the ID
	// len(Nonce) should be > 20 and < 64 bytes
	// It has no functional purpose and should be ignored.
	Nonce []byte `json:"nonce"`

	// Not serialized. Store the op's id in memory.
	id entity.Id
	// Not serialized. Store the extra metadata in memory,
	// compiled from SetMetadataOperation.
	extraMetadata map[string]string
}

// newOpBase is the constructor for an OpBase
func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
	return OpBase{
		OperationType: opType,
		Author_:       author,
		UnixTime:      unixTime,
		Nonce:         makeNonce(20),
		id:            entity.UnsetId,
	}
}

func makeNonce(len int) []byte {
	result := make([]byte, len)
	_, err := rand.Read(result)
	if err != nil {
		panic(err)
	}
	return result
}

func (base *OpBase) UnmarshalJSON(data []byte) error {
	// Compute the Id when loading the op from disk.
	base.id = entity.DeriveId(data)

	aux := struct {
		OperationType OperationType     `json:"type"`
		Author        json.RawMessage   `json:"author"`
		UnixTime      int64             `json:"timestamp"`
		Metadata      map[string]string `json:"metadata,omitempty"`
		Nonce         []byte            `json:"nonce"`
	}{}

	if err := json.Unmarshal(data, &aux); err != nil {
		return err
	}

	// delegate the decoding of the identity
	author, err := identity.UnmarshalJSON(aux.Author)
	if err != nil {
		return err
	}

	base.OperationType = aux.OperationType
	base.Author_ = author
	base.UnixTime = aux.UnixTime
	base.Metadata = aux.Metadata
	base.Nonce = aux.Nonce

	return nil
}

func (base *OpBase) Type() OperationType {
	return base.OperationType
}

// Time return the time when the operation was added
func (base *OpBase) Time() time.Time {
	return time.Unix(base.UnixTime, 0)
}

// Validate check the OpBase for errors
func (base *OpBase) Validate(op Operation, opType OperationType) error {
	if base.OperationType != opType {
		return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
	}

	if op.Time().Unix() == 0 {
		return fmt.Errorf("time not set")
	}

	if base.Author_ == nil {
		return fmt.Errorf("author not set")
	}

	if err := op.Author().Validate(); err != nil {
		return errors.Wrap(err, "author")
	}

	if op, ok := op.(dag.OperationWithFiles); ok {
		for _, hash := range op.GetFiles() {
			if !hash.IsValid() {
				return fmt.Errorf("file with invalid hash %v", hash)
			}
		}
	}

	if len(base.Nonce) > 64 {
		return fmt.Errorf("nonce is too big")
	}
	if len(base.Nonce) < 20 {
		return fmt.Errorf("nonce is too small")
	}

	return nil
}

// SetMetadata store arbitrary metadata about the operation
func (base *OpBase) SetMetadata(key string, value string) {
	if base.Metadata == nil {
		base.Metadata = make(map[string]string)
	}

	base.Metadata[key] = value
	base.id = entity.UnsetId
}

// GetMetadata retrieve arbitrary metadata about the operation
func (base *OpBase) GetMetadata(key string) (string, bool) {
	val, ok := base.Metadata[key]

	if ok {
		return val, true
	}

	// extraMetadata can't replace the original operations value if any
	val, ok = base.extraMetadata[key]

	return val, ok
}

// AllMetadata return all metadata for this operation
func (base *OpBase) AllMetadata() map[string]string {
	result := make(map[string]string)

	for key, val := range base.extraMetadata {
		result[key] = val
	}

	// Original metadata take precedence
	for key, val := range base.Metadata {
		result[key] = val
	}

	return result
}

func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
	if base.extraMetadata == nil {
		base.extraMetadata = make(map[string]string)
	}
	if _, exist := base.extraMetadata[key]; !exist {
		base.extraMetadata[key] = value
	}
}

// Author return author identity
func (base *OpBase) Author() identity.Interface {
	return base.Author_
}