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, resolver identity.Resolver) (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:"-"` // not serialized
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 UnixTime int64 `json:"timestamp"`
182 Metadata map[string]string `json:"metadata,omitempty"`
183 Nonce []byte `json:"nonce"`
184 }{}
185
186 if err := json.Unmarshal(data, &aux); err != nil {
187 return err
188 }
189
190 base.OperationType = aux.OperationType
191 base.UnixTime = aux.UnixTime
192 base.Metadata = aux.Metadata
193 base.Nonce = aux.Nonce
194
195 return nil
196}
197
198func (base *OpBase) Type() OperationType {
199 return base.OperationType
200}
201
202// Time return the time when the operation was added
203func (base *OpBase) Time() time.Time {
204 return time.Unix(base.UnixTime, 0)
205}
206
207// Validate check the OpBase for errors
208func (base *OpBase) Validate(op Operation, opType OperationType) error {
209 if base.OperationType != opType {
210 return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
211 }
212
213 if op.Time().Unix() == 0 {
214 return fmt.Errorf("time not set")
215 }
216
217 if base.Author_ == nil {
218 return fmt.Errorf("author not set")
219 }
220
221 if err := op.Author().Validate(); err != nil {
222 return errors.Wrap(err, "author")
223 }
224
225 if op, ok := op.(dag.OperationWithFiles); ok {
226 for _, hash := range op.GetFiles() {
227 if !hash.IsValid() {
228 return fmt.Errorf("file with invalid hash %v", hash)
229 }
230 }
231 }
232
233 if len(base.Nonce) > 64 {
234 return fmt.Errorf("nonce is too big")
235 }
236 if len(base.Nonce) < 20 {
237 return fmt.Errorf("nonce is too small")
238 }
239
240 return nil
241}
242
243// SetMetadata store arbitrary metadata about the operation
244func (base *OpBase) SetMetadata(key string, value string) {
245 if base.Metadata == nil {
246 base.Metadata = make(map[string]string)
247 }
248
249 base.Metadata[key] = value
250 base.id = entity.UnsetId
251}
252
253// GetMetadata retrieve arbitrary metadata about the operation
254func (base *OpBase) GetMetadata(key string) (string, bool) {
255 val, ok := base.Metadata[key]
256
257 if ok {
258 return val, true
259 }
260
261 // extraMetadata can't replace the original operations value if any
262 val, ok = base.extraMetadata[key]
263
264 return val, ok
265}
266
267// AllMetadata return all metadata for this operation
268func (base *OpBase) AllMetadata() map[string]string {
269 result := make(map[string]string)
270
271 for key, val := range base.extraMetadata {
272 result[key] = val
273 }
274
275 // Original metadata take precedence
276 for key, val := range base.Metadata {
277 result[key] = val
278 }
279
280 return result
281}
282
283func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
284 if base.extraMetadata == nil {
285 base.extraMetadata = make(map[string]string)
286 }
287 if _, exist := base.extraMetadata[key]; !exist {
288 base.extraMetadata[key] = value
289 }
290}
291
292// Author return author identity
293func (base *OpBase) Author() identity.Interface {
294 return base.Author_
295}