1package bug
2
3import (
4 "crypto/sha256"
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/identity"
13 "github.com/MichaelMure/git-bug/util/git"
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 // base return the OpBase of the Operation, for package internal use
34 base() *OpBase
35 // Id return the identifier of the operation, to be used for back references
36 Id() entity.Id
37 // Time return the time when the operation was added
38 Time() time.Time
39 // GetFiles return the files needed by this operation
40 GetFiles() []git.Hash
41 // Apply the operation to a Snapshot to create the final state
42 Apply(snapshot *Snapshot)
43 // Validate check if the operation is valid (ex: a title is a single line)
44 Validate() error
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 // GetAuthor return the author identity
52 GetAuthor() identity.Interface
53
54 // sign-post method for gqlgen
55 IsOperation()
56}
57
58func deriveId(data []byte) entity.Id {
59 sum := sha256.Sum256(data)
60 return entity.Id(fmt.Sprintf("%x", sum))
61}
62
63func idOperation(op Operation) entity.Id {
64 base := op.base()
65
66 if base.id == "" {
67 // something went really wrong
68 panic("op's id not set")
69 }
70 if base.id == entity.UnsetId {
71 // This means we are trying to get the op's Id *before* it has been stored, for instance when
72 // adding multiple ops in one go in an OperationPack.
73 // As the Id is computed based on the actual bytes written on the disk, we are going to predict
74 // those and then get the Id. This is safe as it will be the exact same code writing on disk later.
75
76 data, err := json.Marshal(op)
77 if err != nil {
78 panic(err)
79 }
80
81 base.id = deriveId(data)
82 }
83 return base.id
84}
85
86// OpBase implement the common code for all operations
87type OpBase struct {
88 OperationType OperationType `json:"type"`
89 Author identity.Interface `json:"author"`
90 // TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
91 UnixTime int64 `json:"timestamp"`
92 Metadata map[string]string `json:"metadata,omitempty"`
93 // Not serialized. Store the op's id in memory.
94 id entity.Id
95 // Not serialized. Store the extra metadata in memory,
96 // compiled from SetMetadataOperation.
97 extraMetadata map[string]string
98}
99
100// newOpBase is the constructor for an OpBase
101func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
102 return OpBase{
103 OperationType: opType,
104 Author: author,
105 UnixTime: unixTime,
106 id: entity.UnsetId,
107 }
108}
109
110func (op *OpBase) UnmarshalJSON(data []byte) error {
111 // Compute the Id when loading the op from disk.
112 op.id = deriveId(data)
113
114 aux := struct {
115 OperationType OperationType `json:"type"`
116 Author json.RawMessage `json:"author"`
117 UnixTime int64 `json:"timestamp"`
118 Metadata map[string]string `json:"metadata,omitempty"`
119 }{}
120
121 if err := json.Unmarshal(data, &aux); err != nil {
122 return err
123 }
124
125 // delegate the decoding of the identity
126 author, err := identity.UnmarshalJSON(aux.Author)
127 if err != nil {
128 return err
129 }
130
131 op.OperationType = aux.OperationType
132 op.Author = author
133 op.UnixTime = aux.UnixTime
134 op.Metadata = aux.Metadata
135
136 return nil
137}
138
139// Time return the time when the operation was added
140func (op *OpBase) Time() time.Time {
141 return time.Unix(op.UnixTime, 0)
142}
143
144// GetFiles return the files needed by this operation
145func (op *OpBase) GetFiles() []git.Hash {
146 return nil
147}
148
149// Validate check the OpBase for errors
150func opBaseValidate(op Operation, opType OperationType) error {
151 if op.base().OperationType != opType {
152 return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType)
153 }
154
155 if op.Time().Unix() == 0 {
156 return fmt.Errorf("time not set")
157 }
158
159 if op.base().Author == nil {
160 return fmt.Errorf("author not set")
161 }
162
163 if err := op.base().Author.Validate(); err != nil {
164 return errors.Wrap(err, "author")
165 }
166
167 for _, hash := range op.GetFiles() {
168 if !hash.IsValid() {
169 return fmt.Errorf("file with invalid hash %v", hash)
170 }
171 }
172
173 return nil
174}
175
176// SetMetadata store arbitrary metadata about the operation
177func (op *OpBase) SetMetadata(key string, value string) {
178 if op.Metadata == nil {
179 op.Metadata = make(map[string]string)
180 }
181
182 op.Metadata[key] = value
183 op.id = entity.UnsetId
184}
185
186// GetMetadata retrieve arbitrary metadata about the operation
187func (op *OpBase) GetMetadata(key string) (string, bool) {
188 val, ok := op.Metadata[key]
189
190 if ok {
191 return val, true
192 }
193
194 // extraMetadata can't replace the original operations value if any
195 val, ok = op.extraMetadata[key]
196
197 return val, ok
198}
199
200// AllMetadata return all metadata for this operation
201func (op *OpBase) AllMetadata() map[string]string {
202 result := make(map[string]string)
203
204 for key, val := range op.extraMetadata {
205 result[key] = val
206 }
207
208 // Original metadata take precedence
209 for key, val := range op.Metadata {
210 result[key] = val
211 }
212
213 return result
214}
215
216// GetAuthor return author identity
217func (op *OpBase) GetAuthor() identity.Interface {
218 return op.Author
219}