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