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