1package bug
2
3import (
4 "crypto/sha256"
5 "encoding/json"
6 "fmt"
7 "time"
8
9 "github.com/MichaelMure/git-bug/identity"
10
11 "github.com/MichaelMure/git-bug/util/git"
12 "github.com/pkg/errors"
13)
14
15// OperationType is an operation type identifier
16type OperationType int
17
18const (
19 _ OperationType = iota
20 CreateOp
21 SetTitleOp
22 AddCommentOp
23 SetStatusOp
24 LabelChangeOp
25 EditCommentOp
26 NoOpOp
27 SetMetadataOp
28)
29
30const unsetIDMarker = "unset"
31
32// Operation define the interface to fulfill for an edit operation of a Bug
33type Operation interface {
34 // base return the OpBase of the Operation, for package internal use
35 base() *OpBase
36 // ID return the identifier of the operation, to be used for back references
37 ID() string
38 // Time return the time when the operation was added
39 Time() time.Time
40 // GetUnixTime return the unix timestamp when the operation was added
41 GetUnixTime() int64
42 // GetFiles return the files needed by this operation
43 GetFiles() []git.Hash
44 // Apply the operation to a Snapshot to create the final state
45 Apply(snapshot *Snapshot)
46 // Validate check if the operation is valid (ex: a title is a single line)
47 Validate() error
48 // SetMetadata store arbitrary metadata about the operation
49 SetMetadata(key string, value string)
50 // GetMetadata retrieve arbitrary metadata about the operation
51 GetMetadata(key string) (string, bool)
52 // AllMetadata return all metadata for this operation
53 AllMetadata() map[string]string
54 // GetAuthor return the author identity
55 GetAuthor() identity.Interface
56}
57
58func hashRaw(data []byte) string {
59 hasher := sha256.New()
60 // Write can't fail
61 _, _ = hasher.Write(data)
62 return fmt.Sprintf("%x", hasher.Sum(nil))
63}
64
65func idOperation(op Operation) string {
66 base := op.base()
67
68 if base.id == "" {
69 // something went really wrong
70 panic("op's id not set")
71 }
72 if base.id == "unset" {
73 // This means we are trying to get the op's ID *before* it has been stored, for instance when
74 // adding multiple ops in one go in an OperationPack.
75 // As the ID is computed based on the actual bytes written on the disk, we are going to predict
76 // those and then get the ID. This is safe as it will be the exact same code writing on disk later.
77
78 data, err := json.Marshal(op)
79 if err != nil {
80 panic(err)
81 }
82
83 base.id = hashRaw(data)
84 }
85 return base.id
86}
87
88func IDIsValid(id string) bool {
89 // IDs have the same format as a git hash
90 if len(id) != 40 && len(id) != 64 {
91 return false
92 }
93 for _, r := range id {
94 if (r < 'a' || r > 'z') && (r < '0' || r > '9') {
95 return false
96 }
97 }
98 return true
99}
100
101// OpBase implement the common code for all operations
102type OpBase struct {
103 OperationType OperationType
104 Author identity.Interface
105 UnixTime int64
106 Metadata map[string]string
107 // Not serialized. Store the op's id in memory.
108 id string
109 // Not serialized. Store the extra metadata in memory,
110 // compiled from SetMetadataOperation.
111 extraMetadata map[string]string
112}
113
114// newOpBase is the constructor for an OpBase
115func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
116 return OpBase{
117 OperationType: opType,
118 Author: author,
119 UnixTime: unixTime,
120 id: unsetIDMarker,
121 }
122}
123
124func (op OpBase) MarshalJSON() ([]byte, error) {
125 return json.Marshal(struct {
126 OperationType OperationType `json:"type"`
127 Author identity.Interface `json:"author"`
128 UnixTime int64 `json:"timestamp"`
129 Metadata map[string]string `json:"metadata,omitempty"`
130 }{
131 OperationType: op.OperationType,
132 Author: op.Author,
133 UnixTime: op.UnixTime,
134 Metadata: op.Metadata,
135 })
136}
137
138func (op *OpBase) UnmarshalJSON(data []byte) error {
139 // Compute the ID when loading the op from disk.
140 op.id = hashRaw(data)
141
142 aux := struct {
143 OperationType OperationType `json:"type"`
144 Author json.RawMessage `json:"author"`
145 UnixTime int64 `json:"timestamp"`
146 Metadata map[string]string `json:"metadata,omitempty"`
147 }{}
148
149 if err := json.Unmarshal(data, &aux); err != nil {
150 return err
151 }
152
153 // delegate the decoding of the identity
154 author, err := identity.UnmarshalJSON(aux.Author)
155 if err != nil {
156 return err
157 }
158
159 op.OperationType = aux.OperationType
160 op.Author = author
161 op.UnixTime = aux.UnixTime
162 op.Metadata = aux.Metadata
163
164 return nil
165}
166
167// Time return the time when the operation was added
168func (op *OpBase) Time() time.Time {
169 return time.Unix(op.UnixTime, 0)
170}
171
172// GetUnixTime return the unix timestamp when the operation was added
173func (op *OpBase) GetUnixTime() int64 {
174 return op.UnixTime
175}
176
177// GetFiles return the files needed by this operation
178func (op *OpBase) GetFiles() []git.Hash {
179 return nil
180}
181
182// Validate check the OpBase for errors
183func opBaseValidate(op Operation, opType OperationType) error {
184 if op.base().OperationType != opType {
185 return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType)
186 }
187
188 if op.GetUnixTime() == 0 {
189 return fmt.Errorf("time not set")
190 }
191
192 if op.base().Author == nil {
193 return fmt.Errorf("author not set")
194 }
195
196 if err := op.base().Author.Validate(); err != nil {
197 return errors.Wrap(err, "author")
198 }
199
200 for _, hash := range op.GetFiles() {
201 if !hash.IsValid() {
202 return fmt.Errorf("file with invalid hash %v", hash)
203 }
204 }
205
206 return nil
207}
208
209// SetMetadata store arbitrary metadata about the operation
210func (op *OpBase) SetMetadata(key string, value string) {
211 if op.Metadata == nil {
212 op.Metadata = make(map[string]string)
213 }
214
215 op.Metadata[key] = value
216 op.id = unsetIDMarker
217}
218
219// GetMetadata retrieve arbitrary metadata about the operation
220func (op *OpBase) GetMetadata(key string) (string, bool) {
221 val, ok := op.Metadata[key]
222
223 if ok {
224 return val, true
225 }
226
227 // extraMetadata can't replace the original operations value if any
228 val, ok = op.extraMetadata[key]
229
230 return val, ok
231}
232
233// AllMetadata return all metadata for this operation
234func (op *OpBase) AllMetadata() map[string]string {
235 result := make(map[string]string)
236
237 for key, val := range op.extraMetadata {
238 result[key] = val
239 }
240
241 // Original metadata take precedence
242 for key, val := range op.Metadata {
243 result[key] = val
244 }
245
246 return result
247}
248
249// GetAuthor return author identity
250func (op *OpBase) GetAuthor() identity.Interface {
251 return op.Author
252}