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
 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}