1package bug
2
3import (
4 "crypto/rand"
5 "encoding/json"
6 "fmt"
7 "strings"
8
9 "github.com/MichaelMure/git-bug/entity"
10 "github.com/MichaelMure/git-bug/identity"
11 "github.com/MichaelMure/git-bug/repository"
12 "github.com/MichaelMure/git-bug/util/text"
13 "github.com/MichaelMure/git-bug/util/timestamp"
14)
15
16var _ Operation = &CreateOperation{}
17
18// CreateOperation define the initial creation of a bug
19type CreateOperation struct {
20 OpBase
21 // mandatory random bytes to ensure a better randomness of the data of the first
22 // operation of a bug, used to later generate the ID
23 // len(Nonce) should be > 20 and < 64 bytes
24 Nonce []byte `json:"nonce"`
25 Title string `json:"title"`
26 Message string `json:"message"`
27 Files []repository.Hash `json:"files"`
28}
29
30func (op *CreateOperation) Id() entity.Id {
31 return idOperation(op, &op.OpBase)
32}
33
34// OVERRIDE
35func (op *CreateOperation) SetMetadata(key string, value string) {
36 // sanity check: we make sure we are not in the following scenario:
37 // - the bug is created with a first operation
38 // - Id() is used
39 // - metadata are added, which will change the Id
40 // - Id() is used again
41
42 if op.id != entity.UnsetId {
43 panic("usage of Id() after changing the first operation")
44 }
45
46 op.OpBase.SetMetadata(key, value)
47}
48
49func (op *CreateOperation) Apply(snapshot *Snapshot) {
50 // sanity check: will fail when adding a second Create
51 if snapshot.id != "" && snapshot.id != entity.UnsetId && snapshot.id != op.Id() {
52 panic("adding a second Create operation")
53 }
54
55 snapshot.id = op.Id()
56
57 snapshot.addActor(op.Author_)
58 snapshot.addParticipant(op.Author_)
59
60 snapshot.Title = op.Title
61
62 commentId := entity.CombineIds(snapshot.Id(), op.Id())
63 comment := Comment{
64 id: commentId,
65 Message: op.Message,
66 Author: op.Author_,
67 UnixTime: timestamp.Timestamp(op.UnixTime),
68 }
69
70 snapshot.Comments = []Comment{comment}
71 snapshot.Author = op.Author_
72 snapshot.CreateTime = op.Time()
73
74 snapshot.Timeline = []TimelineItem{
75 &CreateTimelineItem{
76 CommentTimelineItem: NewCommentTimelineItem(commentId, comment),
77 },
78 }
79}
80
81func (op *CreateOperation) GetFiles() []repository.Hash {
82 return op.Files
83}
84
85func (op *CreateOperation) Validate() error {
86 if err := op.OpBase.Validate(op, CreateOp); err != nil {
87 return err
88 }
89
90 if len(op.Nonce) > 64 {
91 return fmt.Errorf("create nonce is too big")
92 }
93 if len(op.Nonce) < 20 {
94 return fmt.Errorf("create nonce is too small")
95 }
96
97 if text.Empty(op.Title) {
98 return fmt.Errorf("title is empty")
99 }
100 if strings.Contains(op.Title, "\n") {
101 return fmt.Errorf("title should be a single line")
102 }
103 if !text.Safe(op.Title) {
104 return fmt.Errorf("title is not fully printable")
105 }
106
107 if !text.Safe(op.Message) {
108 return fmt.Errorf("message is not fully printable")
109 }
110
111 return nil
112}
113
114// UnmarshalJSON is a two step JSON unmarshalling
115// This workaround is necessary to avoid the inner OpBase.MarshalJSON
116// overriding the outer op's MarshalJSON
117func (op *CreateOperation) UnmarshalJSON(data []byte) error {
118 // Unmarshal OpBase and the op separately
119
120 base := OpBase{}
121 err := json.Unmarshal(data, &base)
122 if err != nil {
123 return err
124 }
125
126 aux := struct {
127 Nonce []byte `json:"nonce"`
128 Title string `json:"title"`
129 Message string `json:"message"`
130 Files []repository.Hash `json:"files"`
131 }{}
132
133 err = json.Unmarshal(data, &aux)
134 if err != nil {
135 return err
136 }
137
138 op.OpBase = base
139 op.Nonce = aux.Nonce
140 op.Title = aux.Title
141 op.Message = aux.Message
142 op.Files = aux.Files
143
144 return nil
145}
146
147// Sign post method for gqlgen
148func (op *CreateOperation) IsAuthored() {}
149
150func makeNonce(len int) []byte {
151 result := make([]byte, len)
152 _, err := rand.Read(result)
153 if err != nil {
154 panic(err)
155 }
156 return result
157}
158
159func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation {
160 return &CreateOperation{
161 OpBase: newOpBase(CreateOp, author, unixTime),
162 Nonce: makeNonce(20),
163 Title: title,
164 Message: message,
165 Files: files,
166 }
167}
168
169// CreateTimelineItem replace a Create operation in the Timeline and hold its edition history
170type CreateTimelineItem struct {
171 CommentTimelineItem
172}
173
174// Sign post method for gqlgen
175func (c *CreateTimelineItem) IsAuthored() {}
176
177// Convenience function to apply the operation
178func Create(author identity.Interface, unixTime int64, title, message string) (*Bug, *CreateOperation, error) {
179 return CreateWithFiles(author, unixTime, title, message, nil)
180}
181
182func CreateWithFiles(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) (*Bug, *CreateOperation, error) {
183 newBug := NewBug()
184 createOp := NewCreateOp(author, unixTime, title, message, files)
185
186 if err := createOp.Validate(); err != nil {
187 return nil, createOp, err
188 }
189
190 newBug.Append(createOp)
191
192 return newBug, createOp, nil
193}