operation.go

  1package bug
  2
  3import (
  4	"crypto/sha256"
  5	"encoding/json"
  6	"fmt"
  7	"time"
  8
  9	"github.com/MichaelMure/git-bug/identity"
 10
 11	"github.com/MichaelMure/git-bug/util/git"
 12	"github.com/pkg/errors"
 13)
 14
 15// OperationType is an operation type identifier
 16type OperationType int
 17
 18const (
 19	_ OperationType = iota
 20	CreateOp
 21	SetTitleOp
 22	AddCommentOp
 23	SetStatusOp
 24	LabelChangeOp
 25	EditCommentOp
 26	NoOpOp
 27	SetMetadataOp
 28)
 29
 30const unsetIDMarker = "unset"
 31
 32// Operation define the interface to fulfill for an edit operation of a Bug
 33type Operation interface {
 34	// base return the OpBase of the Operation, for package internal use
 35	base() *OpBase
 36	// ID return the identifier of the operation, to be used for back references
 37	ID() string
 38	// Time return the time when the operation was added
 39	Time() time.Time
 40	// GetUnixTime return the unix timestamp when the operation was added
 41	GetUnixTime() int64
 42	// GetFiles return the files needed by this operation
 43	GetFiles() []git.Hash
 44	// Apply the operation to a Snapshot to create the final state
 45	Apply(snapshot *Snapshot)
 46	// Validate check if the operation is valid (ex: a title is a single line)
 47	Validate() error
 48	// SetMetadata store arbitrary metadata about the operation
 49	SetMetadata(key string, value string)
 50	// GetMetadata retrieve arbitrary metadata about the operation
 51	GetMetadata(key string) (string, bool)
 52	// AllMetadata return all metadata for this operation
 53	AllMetadata() map[string]string
 54	// GetAuthor return the author identity
 55	GetAuthor() identity.Interface
 56}
 57
 58func hashRaw(data []byte) string {
 59	hasher := sha256.New()
 60	// Write can't fail
 61	_, _ = hasher.Write(data)
 62	return fmt.Sprintf("%x", hasher.Sum(nil))
 63}
 64
 65func idOperation(op Operation) string {
 66	base := op.base()
 67
 68	if base.id == "" {
 69		// something went really wrong
 70		panic("op's id not set")
 71	}
 72	if base.id == "unset" {
 73		// This means we are trying to get the op's ID *before* it has been stored, for instance when
 74		// adding multiple ops in one go in an OperationPack.
 75		// As the ID is computed based on the actual bytes written on the disk, we are going to predict
 76		// those and then get the ID. This is safe as it will be the exact same code writing on disk later.
 77
 78		data, err := json.Marshal(op)
 79		if err != nil {
 80			panic(err)
 81		}
 82
 83		base.id = hashRaw(data)
 84	}
 85	return base.id
 86}
 87
 88func IDIsValid(id string) bool {
 89	// IDs have the same format as a git hash
 90	if len(id) != 40 && len(id) != 64 {
 91		return false
 92	}
 93	for _, r := range id {
 94		if (r < 'a' || r > 'z') && (r < '0' || r > '9') {
 95			return false
 96		}
 97	}
 98	return true
 99}
100
101// OpBase implement the common code for all operations
102type OpBase struct {
103	OperationType OperationType
104	Author        identity.Interface
105	UnixTime      int64
106	Metadata      map[string]string
107	// Not serialized. Store the op's id in memory.
108	id string
109	// Not serialized. Store the extra metadata in memory,
110	// compiled from SetMetadataOperation.
111	extraMetadata map[string]string
112}
113
114// newOpBase is the constructor for an OpBase
115func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
116	return OpBase{
117		OperationType: opType,
118		Author:        author,
119		UnixTime:      unixTime,
120		id:            unsetIDMarker,
121	}
122}
123
124func (op OpBase) MarshalJSON() ([]byte, error) {
125	return json.Marshal(struct {
126		OperationType OperationType      `json:"type"`
127		Author        identity.Interface `json:"author"`
128		UnixTime      int64              `json:"timestamp"`
129		Metadata      map[string]string  `json:"metadata,omitempty"`
130	}{
131		OperationType: op.OperationType,
132		Author:        op.Author,
133		UnixTime:      op.UnixTime,
134		Metadata:      op.Metadata,
135	})
136}
137
138func (op *OpBase) UnmarshalJSON(data []byte) error {
139	// Compute the ID when loading the op from disk.
140	op.id = hashRaw(data)
141
142	aux := struct {
143		OperationType OperationType     `json:"type"`
144		Author        json.RawMessage   `json:"author"`
145		UnixTime      int64             `json:"timestamp"`
146		Metadata      map[string]string `json:"metadata,omitempty"`
147	}{}
148
149	if err := json.Unmarshal(data, &aux); err != nil {
150		return err
151	}
152
153	// delegate the decoding of the identity
154	author, err := identity.UnmarshalJSON(aux.Author)
155	if err != nil {
156		return err
157	}
158
159	op.OperationType = aux.OperationType
160	op.Author = author
161	op.UnixTime = aux.UnixTime
162	op.Metadata = aux.Metadata
163
164	return nil
165}
166
167// Time return the time when the operation was added
168func (op *OpBase) Time() time.Time {
169	return time.Unix(op.UnixTime, 0)
170}
171
172// GetUnixTime return the unix timestamp when the operation was added
173func (op *OpBase) GetUnixTime() int64 {
174	return op.UnixTime
175}
176
177// GetFiles return the files needed by this operation
178func (op *OpBase) GetFiles() []git.Hash {
179	return nil
180}
181
182// Validate check the OpBase for errors
183func opBaseValidate(op Operation, opType OperationType) error {
184	if op.base().OperationType != opType {
185		return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType)
186	}
187
188	if op.GetUnixTime() == 0 {
189		return fmt.Errorf("time not set")
190	}
191
192	if op.base().Author == nil {
193		return fmt.Errorf("author not set")
194	}
195
196	if err := op.base().Author.Validate(); err != nil {
197		return errors.Wrap(err, "author")
198	}
199
200	for _, hash := range op.GetFiles() {
201		if !hash.IsValid() {
202			return fmt.Errorf("file with invalid hash %v", hash)
203		}
204	}
205
206	return nil
207}
208
209// SetMetadata store arbitrary metadata about the operation
210func (op *OpBase) SetMetadata(key string, value string) {
211	if op.Metadata == nil {
212		op.Metadata = make(map[string]string)
213	}
214
215	op.Metadata[key] = value
216	op.id = unsetIDMarker
217}
218
219// GetMetadata retrieve arbitrary metadata about the operation
220func (op *OpBase) GetMetadata(key string) (string, bool) {
221	val, ok := op.Metadata[key]
222
223	if ok {
224		return val, true
225	}
226
227	// extraMetadata can't replace the original operations value if any
228	val, ok = op.extraMetadata[key]
229
230	return val, ok
231}
232
233// AllMetadata return all metadata for this operation
234func (op *OpBase) AllMetadata() map[string]string {
235	result := make(map[string]string)
236
237	for key, val := range op.extraMetadata {
238		result[key] = val
239	}
240
241	// Original metadata take precedence
242	for key, val := range op.Metadata {
243		result[key] = val
244	}
245
246	return result
247}
248
249// GetAuthor return author identity
250func (op *OpBase) GetAuthor() identity.Interface {
251	return op.Author
252}