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