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