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