diff options
Diffstat (limited to 'bug')
-rw-r--r-- | bug/bug_test.go | 7 | ||||
-rw-r--r-- | bug/op_add_comment.go | 28 | ||||
-rw-r--r-- | bug/op_create.go | 43 | ||||
-rw-r--r-- | bug/op_create_test.go | 20 | ||||
-rw-r--r-- | bug/op_edit_comment.go | 123 | ||||
-rw-r--r-- | bug/op_edit_comment_test.go | 53 | ||||
-rw-r--r-- | bug/op_label_change.go | 16 | ||||
-rw-r--r-- | bug/op_set_status.go | 18 | ||||
-rw-r--r-- | bug/op_set_title.go | 22 | ||||
-rw-r--r-- | bug/operation.go | 10 | ||||
-rw-r--r-- | bug/operation_pack.go | 14 | ||||
-rw-r--r-- | bug/operation_test.go | 2 | ||||
-rw-r--r-- | bug/snapshot.go | 30 | ||||
-rw-r--r-- | bug/timeline.go | 62 |
14 files changed, 378 insertions, 70 deletions
diff --git a/bug/bug_test.go b/bug/bug_test.go index 6504da1a..a7759ad8 100644 --- a/bug/bug_test.go +++ b/bug/bug_test.go @@ -78,6 +78,13 @@ func TestBugSerialisation(t *testing.T) { bug2.packs[0].Operations[i].base().hash = bug1.packs[0].Operations[i].base().hash } + // check hashes + for i := range bug1.packs[0].Operations { + if !bug2.packs[0].Operations[i].base().hash.IsValid() { + t.Fatal("invalid hash") + } + } + deep.CompareUnexportedFields = true if diff := deep.Equal(bug1, bug2); diff != nil { t.Fatal(diff) diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index e5622073..4594ba70 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -7,10 +7,9 @@ import ( "github.com/MichaelMure/git-bug/util/text" ) -// AddCommentOperation will add a new comment in the bug - -var _ Operation = AddCommentOperation{} +var _ Operation = &AddCommentOperation{} +// AddCommentOperation will add a new comment in the bug type AddCommentOperation struct { *OpBase Message string `json:"message"` @@ -18,15 +17,15 @@ type AddCommentOperation struct { Files []git.Hash `json:"files"` } -func (op AddCommentOperation) base() *OpBase { +func (op *AddCommentOperation) base() *OpBase { return op.OpBase } -func (op AddCommentOperation) Hash() (git.Hash, error) { +func (op *AddCommentOperation) Hash() (git.Hash, error) { return hashOperation(op) } -func (op AddCommentOperation) Apply(snapshot *Snapshot) { +func (op *AddCommentOperation) Apply(snapshot *Snapshot) { comment := Comment{ Message: op.Message, Author: op.Author, @@ -35,13 +34,22 @@ func (op AddCommentOperation) Apply(snapshot *Snapshot) { } snapshot.Comments = append(snapshot.Comments, comment) + + hash, err := op.Hash() + if err != nil { + // Should never error unless a programming error happened + // (covered in OpBase.Validate()) + panic(err) + } + + snapshot.Timeline = append(snapshot.Timeline, NewCommentTimelineItem(hash, comment)) } -func (op AddCommentOperation) GetFiles() []git.Hash { +func (op *AddCommentOperation) GetFiles() []git.Hash { return op.Files } -func (op AddCommentOperation) Validate() error { +func (op *AddCommentOperation) Validate() error { if err := opBaseValidate(op, AddCommentOp); err != nil { return err } @@ -57,8 +65,8 @@ func (op AddCommentOperation) Validate() error { return nil } -func NewAddCommentOp(author Person, unixTime int64, message string, files []git.Hash) AddCommentOperation { - return AddCommentOperation{ +func NewAddCommentOp(author Person, unixTime int64, message string, files []git.Hash) *AddCommentOperation { + return &AddCommentOperation{ OpBase: newOpBase(AddCommentOp, author, unixTime), Message: message, Files: files, diff --git a/bug/op_create.go b/bug/op_create.go index 70ca7242..5c41eb7c 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -8,10 +8,9 @@ import ( "github.com/MichaelMure/git-bug/util/text" ) -// CreateOperation define the initial creation of a bug - -var _ Operation = CreateOperation{} +var _ Operation = &CreateOperation{} +// CreateOperation define the initial creation of a bug type CreateOperation struct { *OpBase Title string `json:"title"` @@ -19,32 +18,44 @@ type CreateOperation struct { Files []git.Hash `json:"files"` } -func (op CreateOperation) base() *OpBase { +func (op *CreateOperation) base() *OpBase { return op.OpBase } -func (op CreateOperation) Hash() (git.Hash, error) { +func (op *CreateOperation) Hash() (git.Hash, error) { return hashOperation(op) } -func (op CreateOperation) Apply(snapshot *Snapshot) { +func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.Title = op.Title - snapshot.Comments = []Comment{ - { - Message: op.Message, - Author: op.Author, - UnixTime: op.UnixTime, - }, + + comment := Comment{ + Message: op.Message, + Author: op.Author, + UnixTime: op.UnixTime, } + + snapshot.Comments = []Comment{comment} snapshot.Author = op.Author snapshot.CreatedAt = op.Time() + + hash, err := op.Hash() + if err != nil { + // Should never error unless a programming error happened + // (covered in OpBase.Validate()) + panic(err) + } + + snapshot.Timeline = []TimelineItem{ + NewCreateTimelineItem(hash, comment), + } } -func (op CreateOperation) GetFiles() []git.Hash { +func (op *CreateOperation) GetFiles() []git.Hash { return op.Files } -func (op CreateOperation) Validate() error { +func (op *CreateOperation) Validate() error { if err := opBaseValidate(op, CreateOp); err != nil { return err } @@ -68,8 +79,8 @@ func (op CreateOperation) Validate() error { return nil } -func NewCreateOp(author Person, unixTime int64, title, message string, files []git.Hash) CreateOperation { - return CreateOperation{ +func NewCreateOp(author Person, unixTime int64, title, message string, files []git.Hash) *CreateOperation { + return &CreateOperation{ OpBase: newOpBase(CreateOp, author, unixTime), Title: title, Message: message, diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 2c5ae35b..e9a36cf8 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -1,9 +1,10 @@ package bug import ( - "reflect" "testing" "time" + + "github.com/go-test/deep" ) func TestCreate(t *testing.T) { @@ -20,16 +21,27 @@ func TestCreate(t *testing.T) { create.Apply(&snapshot) + hash, err := create.Hash() + if err != nil { + t.Fatal(err) + } + + comment := Comment{Author: rene, Message: "message", UnixTime: create.UnixTime} + expected := Snapshot{ Title: "title", Comments: []Comment{ - {Author: rene, Message: "message", UnixTime: create.UnixTime}, + comment, }, Author: rene, CreatedAt: create.Time(), + Timeline: []TimelineItem{ + NewCreateTimelineItem(hash, comment), + }, } - if !reflect.DeepEqual(snapshot, expected) { - t.Fatalf("%v different than %v", snapshot, expected) + deep.CompareUnexportedFields = true + if diff := deep.Equal(snapshot, expected); diff != nil { + t.Fatal(diff) } } diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go new file mode 100644 index 00000000..c6cfab2b --- /dev/null +++ b/bug/op_edit_comment.go @@ -0,0 +1,123 @@ +package bug + +import ( + "fmt" + + "github.com/MichaelMure/git-bug/util/git" + "github.com/MichaelMure/git-bug/util/text" +) + +var _ Operation = &EditCommentOperation{} + +// EditCommentOperation will change a comment in the bug +type EditCommentOperation struct { + *OpBase + Target git.Hash `json:"target"` + Message string `json:"message"` + Files []git.Hash `json:"files"` +} + +func (op *EditCommentOperation) base() *OpBase { + return op.OpBase +} + +func (op *EditCommentOperation) Hash() (git.Hash, error) { + return hashOperation(op) +} + +func (op *EditCommentOperation) Apply(snapshot *Snapshot) { + // Todo: currently any message can be edited, even by a different author + // crypto signature are needed. + + var target TimelineItem + var commentIndex int + + for i, item := range snapshot.Timeline { + h, err := item.Hash() + + if err != nil { + // Should never happen, we control what goes into the timeline + panic(err) + } + + if h == op.Target { + target = snapshot.Timeline[i] + break + } + + // Track the index in the []Comment + switch item.(type) { + case *CreateTimelineItem, *CommentTimelineItem: + commentIndex++ + } + } + + if target == nil { + // Target not found, edit is a no-op + return + } + + switch target.(type) { + case *CreateTimelineItem: + item := target.(*CreateTimelineItem) + newComment := item.LastState() + newComment.Message = op.Message + item.History = append(item.History, newComment) + + case *CommentTimelineItem: + item := target.(*CommentTimelineItem) + newComment := item.LastState() + newComment.Message = op.Message + item.History = append(item.History, newComment) + } + + snapshot.Comments[commentIndex].Message = op.Message + snapshot.Comments[commentIndex].Files = op.Files +} + +func (op *EditCommentOperation) GetFiles() []git.Hash { + return op.Files +} + +func (op *EditCommentOperation) Validate() error { + if err := opBaseValidate(op, EditCommentOp); err != nil { + return err + } + + if !op.Target.IsValid() { + return fmt.Errorf("target hash is invalid") + } + + if text.Empty(op.Message) { + return fmt.Errorf("message is empty") + } + + if !text.Safe(op.Message) { + return fmt.Errorf("message is not fully printable") + } + + return nil +} + +func NewEditCommentOp(author Person, unixTime int64, target git.Hash, message string, files []git.Hash) *EditCommentOperation { + return &EditCommentOperation{ + OpBase: newOpBase(EditCommentOp, author, unixTime), + Target: target, + Message: message, + Files: files, + } +} + +// Convenience function to apply the operation +func EditComment(b Interface, author Person, unixTime int64, target git.Hash, message string) error { + return EditCommentWithFiles(b, author, unixTime, target, message, nil) +} + +func EditCommentWithFiles(b Interface, author Person, unixTime int64, target git.Hash, message string, files []git.Hash) error { + editCommentOp := NewEditCommentOp(author, unixTime, target, message, files) + if err := editCommentOp.Validate(); err != nil { + return err + } + b.Append(editCommentOp) + return nil +} diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go new file mode 100644 index 00000000..9c32051d --- /dev/null +++ b/bug/op_edit_comment_test.go @@ -0,0 +1,53 @@ +package bug + +import ( + "testing" + "time" + + "gotest.tools/assert" +) + +func TestEdit(t *testing.T) { + snapshot := Snapshot{} + + var rene = Person{ + Name: "René Descartes", + Email: "rene@descartes.fr", + } + + unix := time.Now().Unix() + + create := NewCreateOp(rene, unix, "title", "create", nil) + create.Apply(&snapshot) + + hash1, err := create.Hash() + if err != nil { + t.Fatal(err) + } + + comment := NewAddCommentOp(rene, unix, "comment", nil) + comment.Apply(&snapshot) + + hash2, err := comment.Hash() + if err != nil { + t.Fatal(err) + } + + edit := NewEditCommentOp(rene, unix, hash1, "create edited", nil) + edit.Apply(&snapshot) + + assert.Equal(t, len(snapshot.Timeline), 2) + assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) + assert.Equal(t, len(snapshot.Timeline[1].(*CommentTimelineItem).History), 1) + assert.Equal(t, snapshot.Comments[0].Message, "create edited") + assert.Equal(t, snapshot.Comments[1].Message, "comment") + + edit2 := NewEditCommentOp(rene, unix, hash2, "comment edited", nil) + edit2.Apply(&snapshot) + + assert.Equal(t, len(snapshot.Timeline), 2) + assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) + assert.Equal(t, len(snapshot.Timeline[1].(*CommentTimelineItem).History), 2) + assert.Equal(t, snapshot.Comments[0].Message, "create edited") + assert.Equal(t, snapshot.Comments[1].Message, "comment edited") +} diff --git a/bug/op_label_change.go b/bug/op_label_change.go index 8909cf56..5f2dbd6f 100644 --- a/bug/op_label_change.go +++ b/bug/op_label_change.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" ) -var _ Operation = LabelChangeOperation{} +var _ Operation = &LabelChangeOperation{} // LabelChangeOperation define a Bug operation to add or remove labels type LabelChangeOperation struct { @@ -17,16 +17,16 @@ type LabelChangeOperation struct { Removed []Label `json:"removed"` } -func (op LabelChangeOperation) base() *OpBase { +func (op *LabelChangeOperation) base() *OpBase { return op.OpBase } -func (op LabelChangeOperation) Hash() (git.Hash, error) { +func (op *LabelChangeOperation) Hash() (git.Hash, error) { return hashOperation(op) } // Apply apply the operation -func (op LabelChangeOperation) Apply(snapshot *Snapshot) { +func (op *LabelChangeOperation) Apply(snapshot *Snapshot) { // Add in the set AddLoop: for _, added := range op.Added { @@ -54,9 +54,11 @@ AddLoop: sort.Slice(snapshot.Labels, func(i, j int) bool { return string(snapshot.Labels[i]) < string(snapshot.Labels[j]) }) + + snapshot.Timeline = append(snapshot.Timeline, op) } -func (op LabelChangeOperation) Validate() error { +func (op *LabelChangeOperation) Validate() error { if err := opBaseValidate(op, LabelChangeOp); err != nil { return err } @@ -80,8 +82,8 @@ func (op LabelChangeOperation) Validate() error { return nil } -func NewLabelChangeOperation(author Person, unixTime int64, added, removed []Label) LabelChangeOperation { - return LabelChangeOperation{ +func NewLabelChangeOperation(author Person, unixTime int64, added, removed []Label) *LabelChangeOperation { + return &LabelChangeOperation{ OpBase: newOpBase(LabelChangeOp, author, unixTime), Added: added, Removed: removed, diff --git a/bug/op_set_status.go b/bug/op_set_status.go index 1fb9cab6..cdfa25e7 100644 --- a/bug/op_set_status.go +++ b/bug/op_set_status.go @@ -5,28 +5,28 @@ import ( "github.com/pkg/errors" ) -// SetStatusOperation will change the status of a bug - -var _ Operation = SetStatusOperation{} +var _ Operation = &SetStatusOperation{} +// SetStatusOperation will change the status of a bug type SetStatusOperation struct { *OpBase Status Status `json:"status"` } -func (op SetStatusOperation) base() *OpBase { +func (op *SetStatusOperation) base() *OpBase { return op.OpBase } -func (op SetStatusOperation) Hash() (git.Hash, error) { +func (op *SetStatusOperation) Hash() (git.Hash, error) { return hashOperation(op) } -func (op SetStatusOperation) Apply(snapshot *Snapshot) { +func (op *SetStatusOperation) Apply(snapshot *Snapshot) { snapshot.Status = op.Status + snapshot.Timeline = append(snapshot.Timeline, op) } -func (op SetStatusOperation) Validate() error { +func (op *SetStatusOperation) Validate() error { if err := opBaseValidate(op, SetStatusOp); err != nil { return err } @@ -38,8 +38,8 @@ func (op SetStatusOperation) Validate() error { return nil } -func NewSetStatusOp(author Person, unixTime int64, status Status) SetStatusOperation { - return SetStatusOperation{ +func NewSetStatusOp(author Person, unixTime int64, status Status) *SetStatusOperation { + return &SetStatusOperation{ OpBase: newOpBase(SetStatusOp, author, unixTime), Status: status, } diff --git a/bug/op_set_title.go b/bug/op_set_title.go index 6049ebd0..74467ec2 100644 --- a/bug/op_set_title.go +++ b/bug/op_set_title.go @@ -8,29 +8,29 @@ import ( "github.com/MichaelMure/git-bug/util/text" ) -// SetTitleOperation will change the title of a bug - -var _ Operation = SetTitleOperation{} +var _ Operation = &SetTitleOperation{} +// SetTitleOperation will change the title of a bug type SetTitleOperation struct { *OpBase Title string `json:"title"` Was string `json:"was"` } -func (op SetTitleOperation) base() *OpBase { +func (op *SetTitleOperation) base() *OpBase { return op.OpBase } -func (op SetTitleOperation) Hash() (git.Hash, error) { +func (op *SetTitleOperation) Hash() (git.Hash, error) { return hashOperation(op) } -func (op SetTitleOperation) Apply(snapshot *Snapshot) { +func (op *SetTitleOperation) Apply(snapshot *Snapshot) { snapshot.Title = op.Title + snapshot.Timeline = append(snapshot.Timeline, op) } -func (op SetTitleOperation) Validate() error { +func (op *SetTitleOperation) Validate() error { if err := opBaseValidate(op, SetTitleOp); err != nil { return err } @@ -58,8 +58,8 @@ func (op SetTitleOperation) Validate() error { return nil } -func NewSetTitleOp(author Person, unixTime int64, title string, was string) SetTitleOperation { - return SetTitleOperation{ +func NewSetTitleOp(author Person, unixTime int64, title string, was string) *SetTitleOperation { + return &SetTitleOperation{ OpBase: newOpBase(SetTitleOp, author, unixTime), Title: title, Was: was, @@ -80,9 +80,9 @@ func SetTitle(b Interface, author Person, unixTime int64, title string) error { var was string if lastTitleOp != nil { - was = lastTitleOp.(SetTitleOperation).Title + was = lastTitleOp.(*SetTitleOperation).Title } else { - was = b.FirstOp().(CreateOperation).Title + was = b.FirstOp().(*CreateOperation).Title } setTitleOp := NewSetTitleOp(author, unixTime, title, was) diff --git a/bug/operation.go b/bug/operation.go index d7d5af51..84dd91ee 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -20,13 +20,14 @@ const ( AddCommentOp SetStatusOp LabelChangeOp + EditCommentOp ) // Operation define the interface to fulfill for an edit operation of a Bug type Operation interface { // base return the OpBase of the Operation, for package internal use base() *OpBase - // Hash return the hash of the operation + // Hash return the hash of the operation, to be used for back references Hash() (git.Hash, error) // Time return the time when the operation was added Time() time.Time @@ -46,7 +47,8 @@ type Operation interface { func hashRaw(data []byte) git.Hash { hasher := sha256.New() - return git.Hash(fmt.Sprintf("%x", hasher.Sum(data))) + hasher.Write(data) + return git.Hash(fmt.Sprintf("%x", hasher.Sum(nil))) } // hash compute the hash of the serialized operation @@ -106,6 +108,10 @@ func opBaseValidate(op Operation, opType OperationType) error { return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType) } + if _, err := op.Hash(); err != nil { + return errors.Wrap(err, "op is not serializable") + } + if op.GetUnixTime() == 0 { return fmt.Errorf("time not set") } diff --git a/bug/operation_pack.go b/bug/operation_pack.go index 5238ea60..f33d94bf 100644 --- a/bug/operation_pack.go +++ b/bug/operation_pack.go @@ -74,23 +74,27 @@ func (opp *OperationPack) UnmarshalJSON(data []byte) error { func (opp *OperationPack) unmarshalOp(raw []byte, _type OperationType) (Operation, error) { switch _type { case CreateOp: - op := CreateOperation{} + op := &CreateOperation{} err := json.Unmarshal(raw, &op) return op, err case SetTitleOp: - op := SetTitleOperation{} + op := &SetTitleOperation{} err := json.Unmarshal(raw, &op) return op, err case AddCommentOp: - op := AddCommentOperation{} + op := &AddCommentOperation{} err := json.Unmarshal(raw, &op) return op, err case SetStatusOp: - op := SetStatusOperation{} + op := &SetStatusOperation{} err := json.Unmarshal(raw, &op) return op, err case LabelChangeOp: - op := LabelChangeOperation{} + op := &LabelChangeOperation{} + err := json.Unmarshal(raw, &op) + return op, err + case EditCommentOp: + op := &EditCommentOperation{} err := json.Unmarshal(raw, &op) return op, err default: diff --git a/bug/operation_test.go b/bug/operation_test.go index 8561adf3..098cf138 100644 --- a/bug/operation_test.go +++ b/bug/operation_test.go @@ -36,7 +36,7 @@ func TestValidate(t *testing.T) { NewSetStatusOp(Person{Name: "René Descartes", Email: "rene@descartes.fr\u001b"}, unix, ClosedStatus), NewSetStatusOp(Person{Name: "René \nDescartes", Email: "rene@descartes.fr"}, unix, ClosedStatus), NewSetStatusOp(Person{Name: "René Descartes", Email: "rene@\ndescartes.fr"}, unix, ClosedStatus), - CreateOperation{OpBase: &OpBase{ + &CreateOperation{OpBase: &OpBase{ Author: rene, UnixTime: 0, OperationType: CreateOp, diff --git a/bug/snapshot.go b/bug/snapshot.go index 59dcae7e..df39ff46 100644 --- a/bug/snapshot.go +++ b/bug/snapshot.go @@ -3,6 +3,8 @@ package bug import ( "fmt" "time" + + "github.com/MichaelMure/git-bug/util/git" ) // Snapshot is a compiled form of the Bug data structure used for storage and merge @@ -16,20 +18,22 @@ type Snapshot struct { Author Person CreatedAt time.Time + Timeline []TimelineItem + Operations []Operation } // Return the Bug identifier -func (snap Snapshot) Id() string { +func (snap *Snapshot) Id() string { return snap.id } // Return the Bug identifier truncated for human consumption -func (snap Snapshot) HumanId() string { +func (snap *Snapshot) HumanId() string { return fmt.Sprintf("%.8s", snap.id) } -func (snap Snapshot) Summary() string { +func (snap *Snapshot) Summary() string { return fmt.Sprintf("C:%d L:%d", len(snap.Comments)-1, len(snap.Labels), @@ -37,7 +41,7 @@ func (snap Snapshot) Summary() string { } // Return the last time a bug was modified -func (snap Snapshot) LastEditTime() time.Time { +func (snap *Snapshot) LastEditTime() time.Time { if len(snap.Operations) == 0 { return time.Unix(0, 0) } @@ -46,10 +50,26 @@ func (snap Snapshot) LastEditTime() time.Time { } // Return the last timestamp a bug was modified -func (snap Snapshot) LastEditUnix() int64 { +func (snap *Snapshot) LastEditUnix() int64 { if len(snap.Operations) == 0 { return 0 } return snap.Operations[len(snap.Operations)-1].GetUnixTime() } + +// SearchTimelineItem will search in the timeline for an item matching the given hash +func (snap *Snapshot) SearchTimelineItem(hash git.Hash) (TimelineItem, error) { + for i := range snap.Timeline { + h, err := snap.Timeline[i].Hash() + if err != nil { + return nil, err + } + + if h == hash { + return snap.Timeline[i], nil + } + } + + return nil, fmt.Errorf("timeline item not found") +} diff --git a/bug/timeline.go b/bug/timeline.go new file mode 100644 index 00000000..0f79958b --- /dev/null +++ b/bug/timeline.go @@ -0,0 +1,62 @@ +package bug + +import "github.com/MichaelMure/git-bug/util/git" + +type TimelineItem interface { + // Hash return the hash of the item + Hash() (git.Hash, error) +} + +// CreateTimelineItem replace a Create operation in the Timeline and hold its edition history +type CreateTimelineItem struct { + hash git.Hash + History []Comment +} + +func NewCreateTimelineItem(hash git.Hash, comment Comment) *CreateTimelineItem { + return &CreateTimelineItem{ + hash: hash, + History: []Comment{ + comment, + }, + } +} + +func (c *CreateTimelineItem) Hash() (git.Hash, error) { + return c.hash, nil +} + +func (c *CreateTimelineItem) LastState() Comment { + if len(c.History) == 0 { + panic("no history yet") + } + + return c.History[len(c.History)-1] +} + +// CommentTimelineItem replace a Comment in the Timeline and hold its edition history +type CommentTimelineItem struct { + hash git.Hash + History []Comment +} + +func NewCommentTimelineItem(hash git.Hash, comment Comment) *CommentTimelineItem { + return &CommentTimelineItem{ + hash: hash, + History: []Comment{ + comment, + }, + } +} + +func (c *CommentTimelineItem) Hash() (git.Hash, error) { + return c.hash, nil +} + +func (c *CommentTimelineItem) LastState() Comment { + if len(c.History) == 0 { + panic("no history yet") + } + + return c.History[len(c.History)-1] +} |