op_create.go

  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}