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