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