2018-07-13 17:13:40 +03:00
|
|
|
package bug
|
2018-07-12 22:31:41 +03:00
|
|
|
|
2018-08-03 00:37:49 +03:00
|
|
|
import (
|
2018-09-29 00:51:47 +03:00
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/json"
|
2018-09-15 14:15:00 +03:00
|
|
|
"fmt"
|
2018-08-03 00:37:49 +03:00
|
|
|
"time"
|
2018-09-29 00:51:47 +03:00
|
|
|
|
2019-08-11 15:08:03 +03:00
|
|
|
"github.com/pkg/errors"
|
2018-11-21 20:56:12 +03:00
|
|
|
|
2019-08-11 15:08:03 +03:00
|
|
|
"github.com/MichaelMure/git-bug/entity"
|
|
|
|
"github.com/MichaelMure/git-bug/identity"
|
2020-07-01 20:39:02 +03:00
|
|
|
"github.com/MichaelMure/git-bug/repository"
|
2018-08-03 00:37:49 +03:00
|
|
|
)
|
2018-07-18 01:16:06 +03:00
|
|
|
|
2018-09-13 12:13:51 +03:00
|
|
|
// OperationType is an operation type identifier
|
2018-07-12 22:31:41 +03:00
|
|
|
type OperationType int
|
|
|
|
|
|
|
|
const (
|
2018-07-17 20:28:37 +03:00
|
|
|
_ OperationType = iota
|
|
|
|
CreateOp
|
|
|
|
SetTitleOp
|
|
|
|
AddCommentOp
|
|
|
|
SetStatusOp
|
2018-07-18 17:41:09 +03:00
|
|
|
LabelChangeOp
|
2018-09-29 21:41:19 +03:00
|
|
|
EditCommentOp
|
2018-10-02 00:34:45 +03:00
|
|
|
NoOpOp
|
2018-10-21 01:55:58 +03:00
|
|
|
SetMetadataOp
|
2018-07-12 22:31:41 +03:00
|
|
|
)
|
|
|
|
|
2018-08-13 16:28:16 +03:00
|
|
|
// Operation define the interface to fulfill for an edit operation of a Bug
|
2018-07-12 22:31:41 +03:00
|
|
|
type Operation interface {
|
2018-09-28 21:39:39 +03:00
|
|
|
// base return the OpBase of the Operation, for package internal use
|
|
|
|
base() *OpBase
|
2019-08-11 15:08:03 +03:00
|
|
|
// Id return the identifier of the operation, to be used for back references
|
|
|
|
Id() entity.Id
|
2018-08-13 16:28:16 +03:00
|
|
|
// Time return the time when the operation was added
|
2018-07-18 01:16:06 +03:00
|
|
|
Time() time.Time
|
2018-09-12 17:57:04 +03:00
|
|
|
// GetFiles return the files needed by this operation
|
2020-07-01 20:39:02 +03:00
|
|
|
GetFiles() []repository.Hash
|
2018-09-15 14:15:00 +03:00
|
|
|
// Apply the operation to a Snapshot to create the final state
|
2018-09-29 12:28:18 +03:00
|
|
|
Apply(snapshot *Snapshot)
|
2018-09-15 14:15:00 +03:00
|
|
|
// Validate check if the operation is valid (ex: a title is a single line)
|
|
|
|
Validate() error
|
2018-09-24 21:19:16 +03:00
|
|
|
// SetMetadata store arbitrary metadata about the operation
|
|
|
|
SetMetadata(key string, value string)
|
|
|
|
// GetMetadata retrieve arbitrary metadata about the operation
|
|
|
|
GetMetadata(key string) (string, bool)
|
2018-10-02 00:34:45 +03:00
|
|
|
// AllMetadata return all metadata for this operation
|
|
|
|
AllMetadata() map[string]string
|
2019-06-23 18:54:38 +03:00
|
|
|
// GetAuthor return the author identity
|
2019-06-21 00:56:49 +03:00
|
|
|
GetAuthor() identity.Interface
|
2020-02-03 23:03:48 +03:00
|
|
|
|
|
|
|
// sign-post method for gqlgen
|
|
|
|
IsOperation()
|
2018-07-12 22:31:41 +03:00
|
|
|
}
|
2018-07-13 22:21:24 +03:00
|
|
|
|
2019-08-11 15:08:03 +03:00
|
|
|
func deriveId(data []byte) entity.Id {
|
|
|
|
sum := sha256.Sum256(data)
|
|
|
|
return entity.Id(fmt.Sprintf("%x", sum))
|
2018-09-29 00:51:47 +03:00
|
|
|
}
|
|
|
|
|
2019-08-11 15:08:03 +03:00
|
|
|
func idOperation(op Operation) entity.Id {
|
2018-09-29 00:51:47 +03:00
|
|
|
base := op.base()
|
|
|
|
|
2019-08-07 16:31:38 +03:00
|
|
|
if base.id == "" {
|
|
|
|
// something went really wrong
|
|
|
|
panic("op's id not set")
|
2018-09-29 00:51:47 +03:00
|
|
|
}
|
2019-08-11 15:08:03 +03:00
|
|
|
if base.id == entity.UnsetId {
|
|
|
|
// This means we are trying to get the op's Id *before* it has been stored, for instance when
|
2019-08-07 16:31:38 +03:00
|
|
|
// adding multiple ops in one go in an OperationPack.
|
2019-08-11 15:08:03 +03:00
|
|
|
// 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.
|
2019-08-07 16:31:38 +03:00
|
|
|
|
|
|
|
data, err := json.Marshal(op)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2018-09-29 00:51:47 +03:00
|
|
|
|
2019-08-11 15:08:03 +03:00
|
|
|
base.id = deriveId(data)
|
2018-09-29 00:51:47 +03:00
|
|
|
}
|
2019-08-07 16:31:38 +03:00
|
|
|
return base.id
|
|
|
|
}
|
2018-09-29 00:51:47 +03:00
|
|
|
|
2018-08-13 16:28:16 +03:00
|
|
|
// OpBase implement the common code for all operations
|
2018-07-13 22:21:24 +03:00
|
|
|
type OpBase struct {
|
2019-08-11 15:08:03 +03:00
|
|
|
OperationType OperationType `json:"type"`
|
|
|
|
Author identity.Interface `json:"author"`
|
2020-06-26 00:18:17 +03:00
|
|
|
// 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"`
|
2019-08-07 16:31:38 +03:00
|
|
|
// Not serialized. Store the op's id in memory.
|
2019-08-11 15:08:03 +03:00
|
|
|
id entity.Id
|
2018-11-21 20:56:12 +03:00
|
|
|
// Not serialized. Store the extra metadata in memory,
|
|
|
|
// compiled from SetMetadataOperation.
|
2018-10-21 01:55:58 +03:00
|
|
|
extraMetadata map[string]string
|
2018-07-18 01:16:06 +03:00
|
|
|
}
|
|
|
|
|
2018-09-28 21:39:39 +03:00
|
|
|
// newOpBase is the constructor for an OpBase
|
2018-11-21 20:56:12 +03:00
|
|
|
func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
|
2018-10-01 12:37:17 +03:00
|
|
|
return OpBase{
|
2018-07-18 01:16:06 +03:00
|
|
|
OperationType: opType,
|
|
|
|
Author: author,
|
2018-09-25 18:56:58 +03:00
|
|
|
UnixTime: unixTime,
|
2019-08-11 15:08:03 +03:00
|
|
|
id: entity.UnsetId,
|
2018-07-18 01:16:06 +03:00
|
|
|
}
|
2018-07-13 22:21:24 +03:00
|
|
|
}
|
|
|
|
|
2018-11-21 20:56:12 +03:00
|
|
|
func (op *OpBase) UnmarshalJSON(data []byte) error {
|
2019-08-11 15:08:03 +03:00
|
|
|
// Compute the Id when loading the op from disk.
|
|
|
|
op.id = deriveId(data)
|
2019-08-07 16:31:38 +03:00
|
|
|
|
2019-01-19 21:23:31 +03:00
|
|
|
aux := struct {
|
|
|
|
OperationType OperationType `json:"type"`
|
|
|
|
Author json.RawMessage `json:"author"`
|
|
|
|
UnixTime int64 `json:"timestamp"`
|
|
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
|
|
}{}
|
2018-11-21 20:56:12 +03:00
|
|
|
|
|
|
|
if err := json.Unmarshal(data, &aux); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-01-19 21:23:31 +03:00
|
|
|
// delegate the decoding of the identity
|
|
|
|
author, err := identity.UnmarshalJSON(aux.Author)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-11-21 20:56:12 +03:00
|
|
|
op.OperationType = aux.OperationType
|
2019-01-19 21:23:31 +03:00
|
|
|
op.Author = author
|
2018-11-21 20:56:12 +03:00
|
|
|
op.UnixTime = aux.UnixTime
|
|
|
|
op.Metadata = aux.Metadata
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-08-13 16:28:16 +03:00
|
|
|
// Time return the time when the operation was added
|
2018-09-24 21:19:16 +03:00
|
|
|
func (op *OpBase) Time() time.Time {
|
2018-09-10 20:03:17 +03:00
|
|
|
return time.Unix(op.UnixTime, 0)
|
2018-08-23 20:19:16 +03:00
|
|
|
}
|
|
|
|
|
2018-09-12 17:57:04 +03:00
|
|
|
// GetFiles return the files needed by this operation
|
2020-07-01 20:39:02 +03:00
|
|
|
func (op *OpBase) GetFiles() []repository.Hash {
|
2018-08-06 21:31:20 +03:00
|
|
|
return nil
|
|
|
|
}
|
2018-09-15 14:15:00 +03:00
|
|
|
|
|
|
|
// Validate check the OpBase for errors
|
2018-09-28 21:39:39 +03:00
|
|
|
func opBaseValidate(op Operation, opType OperationType) error {
|
|
|
|
if op.base().OperationType != opType {
|
|
|
|
return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType)
|
2018-09-15 14:15:00 +03:00
|
|
|
}
|
|
|
|
|
2020-06-26 00:18:17 +03:00
|
|
|
if op.Time().Unix() == 0 {
|
2018-09-15 14:15:00 +03:00
|
|
|
return fmt.Errorf("time not set")
|
|
|
|
}
|
|
|
|
|
2018-11-21 20:56:12 +03:00
|
|
|
if op.base().Author == nil {
|
|
|
|
return fmt.Errorf("author not set")
|
|
|
|
}
|
|
|
|
|
2018-09-28 21:39:39 +03:00
|
|
|
if err := op.base().Author.Validate(); err != nil {
|
2018-09-15 14:15:00 +03:00
|
|
|
return errors.Wrap(err, "author")
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, hash := range op.GetFiles() {
|
|
|
|
if !hash.IsValid() {
|
|
|
|
return fmt.Errorf("file with invalid hash %v", hash)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2018-09-24 21:19:16 +03:00
|
|
|
|
|
|
|
// SetMetadata store arbitrary metadata about the operation
|
|
|
|
func (op *OpBase) SetMetadata(key string, value string) {
|
|
|
|
if op.Metadata == nil {
|
|
|
|
op.Metadata = make(map[string]string)
|
|
|
|
}
|
|
|
|
|
|
|
|
op.Metadata[key] = value
|
2019-08-11 15:08:03 +03:00
|
|
|
op.id = entity.UnsetId
|
2018-09-24 21:19:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetMetadata retrieve arbitrary metadata about the operation
|
|
|
|
func (op *OpBase) GetMetadata(key string) (string, bool) {
|
|
|
|
val, ok := op.Metadata[key]
|
2018-10-21 01:55:58 +03:00
|
|
|
|
|
|
|
if ok {
|
|
|
|
return val, true
|
|
|
|
}
|
|
|
|
|
|
|
|
// extraMetadata can't replace the original operations value if any
|
|
|
|
val, ok = op.extraMetadata[key]
|
|
|
|
|
2018-09-24 21:19:16 +03:00
|
|
|
return val, ok
|
|
|
|
}
|
2018-10-02 00:34:45 +03:00
|
|
|
|
|
|
|
// AllMetadata return all metadata for this operation
|
|
|
|
func (op *OpBase) AllMetadata() map[string]string {
|
2018-10-21 01:55:58 +03:00
|
|
|
result := make(map[string]string)
|
|
|
|
|
|
|
|
for key, val := range op.extraMetadata {
|
|
|
|
result[key] = val
|
|
|
|
}
|
|
|
|
|
|
|
|
// Original metadata take precedence
|
|
|
|
for key, val := range op.Metadata {
|
|
|
|
result[key] = val
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
2018-10-02 00:34:45 +03:00
|
|
|
}
|
2019-06-21 00:56:49 +03:00
|
|
|
|
2019-06-23 18:54:38 +03:00
|
|
|
// GetAuthor return author identity
|
2019-06-21 00:56:49 +03:00
|
|
|
func (op *OpBase) GetAuthor() identity.Interface {
|
|
|
|
return op.Author
|
|
|
|
}
|