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 "github.com/MichaelMure/git-bug/repository"
15)
16
17// OperationType is an operation type identifier
18type OperationType int
19
20const (
21 _ OperationType = iota
22 CreateOp
23 SetTitleOp
24 AddCommentOp
25 SetStatusOp
26 LabelChangeOp
27 EditCommentOp
28 NoOpOp
29 SetMetadataOp
30)
31
32// Operation define the interface to fulfill for an edit operation of a Bug
33type Operation interface {
34 dag.Operation
35
36 // Type return the type of the operation
37 Type() OperationType
38
39 // Time return the time when the operation was added
40 Time() time.Time
41 // GetFiles return the files needed by this operation
42 GetFiles() []repository.Hash
43 // Apply the operation to a Snapshot to create the final state
44 Apply(snapshot *Snapshot)
45 // SetMetadata store arbitrary metadata about the operation
46 SetMetadata(key string, value string)
47 // GetMetadata retrieve arbitrary metadata about the operation
48 GetMetadata(key string) (string, bool)
49 // AllMetadata return all metadata for this operation
50 AllMetadata() map[string]string
51
52 setExtraMetadataImmutable(key string, value string)
53}
54
55func idOperation(op Operation, base *OpBase) entity.Id {
56 if base.id == "" {
57 // something went really wrong
58 panic("op's id not set")
59 }
60 if base.id == entity.UnsetId {
61 // This means we are trying to get the op's Id *before* it has been stored, for instance when
62 // adding multiple ops in one go in an OperationPack.
63 // As the Id is computed based on the actual bytes written on the disk, we are going to predict
64 // those and then get the Id. This is safe as it will be the exact same code writing on disk later.
65
66 data, err := json.Marshal(op)
67 if err != nil {
68 panic(err)
69 }
70
71 base.id = entity.DeriveId(data)
72 }
73 return base.id
74}
75
76func operationUnmarshaller(author identity.Interface, raw json.RawMessage) (dag.Operation, error) {
77 var t struct {
78 OperationType OperationType `json:"type"`
79 }
80
81 if err := json.Unmarshal(raw, &t); err != nil {
82 return nil, err
83 }
84
85 var op Operation
86
87 switch t.OperationType {
88 case AddCommentOp:
89 op = &AddCommentOperation{}
90 case CreateOp:
91 op = &CreateOperation{}
92 case EditCommentOp:
93 op = &EditCommentOperation{}
94 case LabelChangeOp:
95 op = &LabelChangeOperation{}
96 case NoOpOp:
97 op = &NoOpOperation{}
98 case SetMetadataOp:
99 op = &SetMetadataOperation{}
100 case SetStatusOp:
101 op = &SetStatusOperation{}
102 case SetTitleOp:
103 op = &SetTitleOperation{}
104 default:
105 panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
106 }
107
108 err := json.Unmarshal(raw, &op)
109 if err != nil {
110 return nil, err
111 }
112
113 switch op := op.(type) {
114 case *AddCommentOperation:
115 op.Author_ = author
116 case *CreateOperation:
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// GetFiles return the files needed by this operation
216func (base *OpBase) GetFiles() []repository.Hash {
217 return nil
218}
219
220// Validate check the OpBase for errors
221func (base *OpBase) Validate(op Operation, opType OperationType) error {
222 if base.OperationType != opType {
223 return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
224 }
225
226 if op.Time().Unix() == 0 {
227 return fmt.Errorf("time not set")
228 }
229
230 if base.Author_ == nil {
231 return fmt.Errorf("author not set")
232 }
233
234 if err := op.Author().Validate(); err != nil {
235 return errors.Wrap(err, "author")
236 }
237
238 for _, hash := range op.GetFiles() {
239 if !hash.IsValid() {
240 return fmt.Errorf("file with invalid hash %v", hash)
241 }
242 }
243
244 if len(base.Nonce) > 64 {
245 return fmt.Errorf("nonce is too big")
246 }
247 if len(base.Nonce) < 20 {
248 return fmt.Errorf("nonce is too small")
249 }
250
251 return nil
252}
253
254// SetMetadata store arbitrary metadata about the operation
255func (base *OpBase) SetMetadata(key string, value string) {
256 if base.Metadata == nil {
257 base.Metadata = make(map[string]string)
258 }
259
260 base.Metadata[key] = value
261 base.id = entity.UnsetId
262}
263
264// GetMetadata retrieve arbitrary metadata about the operation
265func (base *OpBase) GetMetadata(key string) (string, bool) {
266 val, ok := base.Metadata[key]
267
268 if ok {
269 return val, true
270 }
271
272 // extraMetadata can't replace the original operations value if any
273 val, ok = base.extraMetadata[key]
274
275 return val, ok
276}
277
278// AllMetadata return all metadata for this operation
279func (base *OpBase) AllMetadata() map[string]string {
280 result := make(map[string]string)
281
282 for key, val := range base.extraMetadata {
283 result[key] = val
284 }
285
286 // Original metadata take precedence
287 for key, val := range base.Metadata {
288 result[key] = val
289 }
290
291 return result
292}
293
294func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
295 if base.extraMetadata == nil {
296 base.extraMetadata = make(map[string]string)
297 }
298 if _, exist := base.extraMetadata[key]; !exist {
299 base.extraMetadata[key] = value
300 }
301}
302
303// Author return author identity
304func (base *OpBase) Author() identity.Interface {
305 return base.Author_
306}