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)
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 dag.Operation
34
35 // Type return the type of the operation
36 Type() OperationType
37
38 // Time return the time when the operation was added
39 Time() time.Time
40 // Apply the operation to a Snapshot to create the final state
41 Apply(snapshot *Snapshot)
42
43 // SetMetadata store arbitrary metadata about the operation
44 SetMetadata(key string, value string)
45 // GetMetadata retrieve arbitrary metadata about the operation
46 GetMetadata(key string) (string, bool)
47 // AllMetadata return all metadata for this operation
48 AllMetadata() map[string]string
49
50 setExtraMetadataImmutable(key string, value string)
51}
52
53func idOperation(op Operation, base *OpBase) entity.Id {
54 if base.id == "" {
55 // something went really wrong
56 panic("op's id not set")
57 }
58 if base.id == entity.UnsetId {
59 // This means we are trying to get the op's Id *before* it has been stored, for instance when
60 // adding multiple ops in one go in an OperationPack.
61 // As the Id is computed based on the actual bytes written on the disk, we are going to predict
62 // those and then get the Id. This is safe as it will be the exact same code writing on disk later.
63
64 data, err := json.Marshal(op)
65 if err != nil {
66 panic(err)
67 }
68
69 base.id = entity.DeriveId(data)
70 }
71 return base.id
72}
73
74func operationUnmarshaller(author identity.Interface, raw json.RawMessage) (dag.Operation, error) {
75 var t struct {
76 OperationType OperationType `json:"type"`
77 }
78
79 if err := json.Unmarshal(raw, &t); err != nil {
80 return nil, err
81 }
82
83 var op Operation
84
85 switch t.OperationType {
86 case AddCommentOp:
87 op = &AddCommentOperation{}
88 case CreateOp:
89 op = &CreateOperation{}
90 case EditCommentOp:
91 op = &EditCommentOperation{}
92 case LabelChangeOp:
93 op = &LabelChangeOperation{}
94 case NoOpOp:
95 op = &NoOpOperation{}
96 case SetMetadataOp:
97 op = &SetMetadataOperation{}
98 case SetStatusOp:
99 op = &SetStatusOperation{}
100 case SetTitleOp:
101 op = &SetTitleOperation{}
102 default:
103 panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
104 }
105
106 err := json.Unmarshal(raw, &op)
107 if err != nil {
108 return nil, err
109 }
110
111 switch op := op.(type) {
112 case *AddCommentOperation:
113 op.Author_ = author
114 case *CreateOperation:
115 op.Author_ = author
116 case *LabelChangeOperation:
117 op.Author_ = author
118 case *NoOpOperation:
119 op.Author_ = author
120 case *SetMetadataOperation:
121 op.Author_ = author
122 case *SetStatusOperation:
123 op.Author_ = author
124 case *SetTitleOperation:
125 op.Author_ = author
126 default:
127 panic(fmt.Sprintf("unknown operation type %T", op))
128 }
129
130 return op, nil
131}
132
133// OpBase implement the common code for all operations
134type OpBase struct {
135 OperationType OperationType `json:"type"`
136 Author_ identity.Interface `json:"author"`
137 // TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
138 UnixTime int64 `json:"timestamp"`
139 Metadata map[string]string `json:"metadata,omitempty"`
140
141 // mandatory random bytes to ensure a better randomness of the data used to later generate the ID
142 // len(Nonce) should be > 20 and < 64 bytes
143 // It has no functional purpose and should be ignored.
144 Nonce []byte `json:"nonce"`
145
146 // Not serialized. Store the op's id in memory.
147 id entity.Id
148 // Not serialized. Store the extra metadata in memory,
149 // compiled from SetMetadataOperation.
150 extraMetadata map[string]string
151}
152
153// newOpBase is the constructor for an OpBase
154func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
155 return OpBase{
156 OperationType: opType,
157 Author_: author,
158 UnixTime: unixTime,
159 Nonce: makeNonce(20),
160 id: entity.UnsetId,
161 }
162}
163
164func makeNonce(len int) []byte {
165 result := make([]byte, len)
166 _, err := rand.Read(result)
167 if err != nil {
168 panic(err)
169 }
170 return result
171}
172
173func (base *OpBase) UnmarshalJSON(data []byte) error {
174 // Compute the Id when loading the op from disk.
175 base.id = entity.DeriveId(data)
176
177 aux := struct {
178 OperationType OperationType `json:"type"`
179 Author json.RawMessage `json:"author"`
180 UnixTime int64 `json:"timestamp"`
181 Metadata map[string]string `json:"metadata,omitempty"`
182 Nonce []byte `json:"nonce"`
183 }{}
184
185 if err := json.Unmarshal(data, &aux); err != nil {
186 return err
187 }
188
189 // delegate the decoding of the identity
190 author, err := identity.UnmarshalJSON(aux.Author)
191 if err != nil {
192 return err
193 }
194
195 base.OperationType = aux.OperationType
196 base.Author_ = author
197 base.UnixTime = aux.UnixTime
198 base.Metadata = aux.Metadata
199 base.Nonce = aux.Nonce
200
201 return nil
202}
203
204func (base *OpBase) Type() OperationType {
205 return base.OperationType
206}
207
208// Time return the time when the operation was added
209func (base *OpBase) Time() time.Time {
210 return time.Unix(base.UnixTime, 0)
211}
212
213// Validate check the OpBase for errors
214func (base *OpBase) Validate(op Operation, opType OperationType) error {
215 if base.OperationType != opType {
216 return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
217 }
218
219 if op.Time().Unix() == 0 {
220 return fmt.Errorf("time not set")
221 }
222
223 if base.Author_ == nil {
224 return fmt.Errorf("author not set")
225 }
226
227 if err := op.Author().Validate(); err != nil {
228 return errors.Wrap(err, "author")
229 }
230
231 if op, ok := op.(dag.OperationWithFiles); ok {
232 for _, hash := range op.GetFiles() {
233 if !hash.IsValid() {
234 return fmt.Errorf("file with invalid hash %v", hash)
235 }
236 }
237 }
238
239 if len(base.Nonce) > 64 {
240 return fmt.Errorf("nonce is too big")
241 }
242 if len(base.Nonce) < 20 {
243 return fmt.Errorf("nonce is too small")
244 }
245
246 return nil
247}
248
249// SetMetadata store arbitrary metadata about the operation
250func (base *OpBase) SetMetadata(key string, value string) {
251 if base.Metadata == nil {
252 base.Metadata = make(map[string]string)
253 }
254
255 base.Metadata[key] = value
256 base.id = entity.UnsetId
257}
258
259// GetMetadata retrieve arbitrary metadata about the operation
260func (base *OpBase) GetMetadata(key string) (string, bool) {
261 val, ok := base.Metadata[key]
262
263 if ok {
264 return val, true
265 }
266
267 // extraMetadata can't replace the original operations value if any
268 val, ok = base.extraMetadata[key]
269
270 return val, ok
271}
272
273// AllMetadata return all metadata for this operation
274func (base *OpBase) AllMetadata() map[string]string {
275 result := make(map[string]string)
276
277 for key, val := range base.extraMetadata {
278 result[key] = val
279 }
280
281 // Original metadata take precedence
282 for key, val := range base.Metadata {
283 result[key] = val
284 }
285
286 return result
287}
288
289func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
290 if base.extraMetadata == nil {
291 base.extraMetadata = make(map[string]string)
292 }
293 if _, exist := base.extraMetadata[key]; !exist {
294 base.extraMetadata[key] = value
295 }
296}
297
298// Author return author identity
299func (base *OpBase) Author() identity.Interface {
300 return base.Author_
301}