1package bug
2
3import (
4 "crypto/sha256"
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/identity"
13 "github.com/MichaelMure/git-bug/util/git"
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 // base return the OpBase of the Operation, for package internal use
34 base() *OpBase
35 // Id return the identifier of the operation, to be used for back references
36 Id() entity.Id
37 // Time return the time when the operation was added
38 Time() time.Time
39 // GetUnixTime return the unix timestamp when the operation was added
40 GetUnixTime() int64
41 // GetFiles return the files needed by this operation
42 GetFiles() []git.Hash
43 // Apply the operation to a Snapshot to create the final state
44 Apply(snapshot *Snapshot)
45 // Validate check if the operation is valid (ex: a title is a single line)
46 Validate() error
47 // SetMetadata store arbitrary metadata about the operation
48 SetMetadata(key string, value string)
49 // GetMetadata retrieve arbitrary metadata about the operation
50 GetMetadata(key string) (string, bool)
51 // AllMetadata return all metadata for this operation
52 AllMetadata() map[string]string
53 // GetAuthor return the author identity
54 GetAuthor() identity.Interface
55
56 // sign-post method for gqlgen
57 IsOperation()
58}
59
60func deriveId(data []byte) entity.Id {
61 sum := sha256.Sum256(data)
62 return entity.Id(fmt.Sprintf("%x", sum))
63}
64
65func idOperation(op Operation) entity.Id {
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 == entity.UnsetId {
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 = deriveId(data)
84 }
85 return base.id
86}
87
88// OpBase implement the common code for all operations
89type OpBase struct {
90 OperationType OperationType `json:"type"`
91 Author identity.Interface `json:"author"`
92 UnixTime int64 `json:"timestamp"`
93 Metadata map[string]string `json:"metadata,omitempty"`
94 // Not serialized. Store the op's id in memory.
95 id entity.Id
96 // Not serialized. Store the extra metadata in memory,
97 // compiled from SetMetadataOperation.
98 extraMetadata map[string]string
99}
100
101// newOpBase is the constructor for an OpBase
102func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
103 return OpBase{
104 OperationType: opType,
105 Author: author,
106 UnixTime: unixTime,
107 id: entity.UnsetId,
108 }
109}
110
111func (op *OpBase) UnmarshalJSON(data []byte) error {
112 // Compute the Id when loading the op from disk.
113 op.id = deriveId(data)
114
115 aux := struct {
116 OperationType OperationType `json:"type"`
117 Author json.RawMessage `json:"author"`
118 UnixTime int64 `json:"timestamp"`
119 Metadata map[string]string `json:"metadata,omitempty"`
120 }{}
121
122 if err := json.Unmarshal(data, &aux); err != nil {
123 return err
124 }
125
126 // delegate the decoding of the identity
127 author, err := identity.UnmarshalJSON(aux.Author)
128 if err != nil {
129 return err
130 }
131
132 op.OperationType = aux.OperationType
133 op.Author = author
134 op.UnixTime = aux.UnixTime
135 op.Metadata = aux.Metadata
136
137 return nil
138}
139
140// Time return the time when the operation was added
141func (op *OpBase) Time() time.Time {
142 return time.Unix(op.UnixTime, 0)
143}
144
145// GetUnixTime return the unix timestamp when the operation was added
146func (op *OpBase) GetUnixTime() int64 {
147 return op.UnixTime
148}
149
150// GetFiles return the files needed by this operation
151func (op *OpBase) GetFiles() []git.Hash {
152 return nil
153}
154
155// Validate check the OpBase for errors
156func opBaseValidate(op Operation, opType OperationType) error {
157 if op.base().OperationType != opType {
158 return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType)
159 }
160
161 if op.GetUnixTime() == 0 {
162 return fmt.Errorf("time not set")
163 }
164
165 if op.base().Author == nil {
166 return fmt.Errorf("author not set")
167 }
168
169 if err := op.base().Author.Validate(); err != nil {
170 return errors.Wrap(err, "author")
171 }
172
173 for _, hash := range op.GetFiles() {
174 if !hash.IsValid() {
175 return fmt.Errorf("file with invalid hash %v", hash)
176 }
177 }
178
179 return nil
180}
181
182// SetMetadata store arbitrary metadata about the operation
183func (op *OpBase) SetMetadata(key string, value string) {
184 if op.Metadata == nil {
185 op.Metadata = make(map[string]string)
186 }
187
188 op.Metadata[key] = value
189 op.id = entity.UnsetId
190}
191
192// GetMetadata retrieve arbitrary metadata about the operation
193func (op *OpBase) GetMetadata(key string) (string, bool) {
194 val, ok := op.Metadata[key]
195
196 if ok {
197 return val, true
198 }
199
200 // extraMetadata can't replace the original operations value if any
201 val, ok = op.extraMetadata[key]
202
203 return val, ok
204}
205
206// AllMetadata return all metadata for this operation
207func (op *OpBase) AllMetadata() map[string]string {
208 result := make(map[string]string)
209
210 for key, val := range op.extraMetadata {
211 result[key] = val
212 }
213
214 // Original metadata take precedence
215 for key, val := range op.Metadata {
216 result[key] = val
217 }
218
219 return result
220}
221
222// GetAuthor return author identity
223func (op *OpBase) GetAuthor() identity.Interface {
224 return op.Author
225}