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 *EditCommentOperation:
117 op.Author_ = author
118 case *LabelChangeOperation:
119 op.Author_ = author
120 case *NoOpOperation:
121 op.Author_ = author
122 case *SetMetadataOperation:
123 op.Author_ = author
124 case *SetStatusOperation:
125 op.Author_ = author
126 case *SetTitleOperation:
127 op.Author_ = author
128 default:
129 panic(fmt.Sprintf("unknown operation type %T", op))
130 }
131
132 return op, nil
133}
134
135// OpBase implement the common code for all operations
136type OpBase struct {
137 OperationType OperationType `json:"type"`
138 Author_ identity.Interface `json:"author"`
139 // TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
140 UnixTime int64 `json:"timestamp"`
141 Metadata map[string]string `json:"metadata,omitempty"`
142
143 // mandatory random bytes to ensure a better randomness of the data used to later generate the ID
144 // len(Nonce) should be > 20 and < 64 bytes
145 // It has no functional purpose and should be ignored.
146 Nonce []byte `json:"nonce"`
147
148 // Not serialized. Store the op's id in memory.
149 id entity.Id
150 // Not serialized. Store the extra metadata in memory,
151 // compiled from SetMetadataOperation.
152 extraMetadata map[string]string
153}
154
155// newOpBase is the constructor for an OpBase
156func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
157 return OpBase{
158 OperationType: opType,
159 Author_: author,
160 UnixTime: unixTime,
161 Nonce: makeNonce(20),
162 id: entity.UnsetId,
163 }
164}
165
166func makeNonce(len int) []byte {
167 result := make([]byte, len)
168 _, err := rand.Read(result)
169 if err != nil {
170 panic(err)
171 }
172 return result
173}
174
175func (base *OpBase) UnmarshalJSON(data []byte) error {
176 // Compute the Id when loading the op from disk.
177 base.id = entity.DeriveId(data)
178
179 aux := struct {
180 OperationType OperationType `json:"type"`
181 Author json.RawMessage `json:"author"`
182 UnixTime int64 `json:"timestamp"`
183 Metadata map[string]string `json:"metadata,omitempty"`
184 Nonce []byte `json:"nonce"`
185 }{}
186
187 if err := json.Unmarshal(data, &aux); err != nil {
188 return err
189 }
190
191 // delegate the decoding of the identity
192 author, err := identity.UnmarshalJSON(aux.Author)
193 if err != nil {
194 return err
195 }
196
197 base.OperationType = aux.OperationType
198 base.Author_ = author
199 base.UnixTime = aux.UnixTime
200 base.Metadata = aux.Metadata
201 base.Nonce = aux.Nonce
202
203 return nil
204}
205
206func (base *OpBase) Type() OperationType {
207 return base.OperationType
208}
209
210// Time return the time when the operation was added
211func (base *OpBase) Time() time.Time {
212 return time.Unix(base.UnixTime, 0)
213}
214
215// Validate check the OpBase for errors
216func (base *OpBase) Validate(op Operation, opType OperationType) error {
217 if base.OperationType != opType {
218 return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
219 }
220
221 if op.Time().Unix() == 0 {
222 return fmt.Errorf("time not set")
223 }
224
225 if base.Author_ == nil {
226 return fmt.Errorf("author not set")
227 }
228
229 if err := op.Author().Validate(); err != nil {
230 return errors.Wrap(err, "author")
231 }
232
233 if op, ok := op.(dag.OperationWithFiles); ok {
234 for _, hash := range op.GetFiles() {
235 if !hash.IsValid() {
236 return fmt.Errorf("file with invalid hash %v", hash)
237 }
238 }
239 }
240
241 if len(base.Nonce) > 64 {
242 return fmt.Errorf("nonce is too big")
243 }
244 if len(base.Nonce) < 20 {
245 return fmt.Errorf("nonce is too small")
246 }
247
248 return nil
249}
250
251// SetMetadata store arbitrary metadata about the operation
252func (base *OpBase) SetMetadata(key string, value string) {
253 if base.Metadata == nil {
254 base.Metadata = make(map[string]string)
255 }
256
257 base.Metadata[key] = value
258 base.id = entity.UnsetId
259}
260
261// GetMetadata retrieve arbitrary metadata about the operation
262func (base *OpBase) GetMetadata(key string) (string, bool) {
263 val, ok := base.Metadata[key]
264
265 if ok {
266 return val, true
267 }
268
269 // extraMetadata can't replace the original operations value if any
270 val, ok = base.extraMetadata[key]
271
272 return val, ok
273}
274
275// AllMetadata return all metadata for this operation
276func (base *OpBase) AllMetadata() map[string]string {
277 result := make(map[string]string)
278
279 for key, val := range base.extraMetadata {
280 result[key] = val
281 }
282
283 // Original metadata take precedence
284 for key, val := range base.Metadata {
285 result[key] = val
286 }
287
288 return result
289}
290
291func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
292 if base.extraMetadata == nil {
293 base.extraMetadata = make(map[string]string)
294 }
295 if _, exist := base.extraMetadata[key]; !exist {
296 base.extraMetadata[key] = value
297 }
298}
299
300// Author return author identity
301func (base *OpBase) Author() identity.Interface {
302 return base.Author_
303}