bug: define a hash-based identifier for an operation

Michael Muré created

Change summary

bug/bug_test.go        |  3 +++
bug/op_add_comment.go  |  4 ++++
bug/op_create.go       |  4 ++++
bug/op_label_change.go |  5 +++++
bug/op_set_status.go   |  5 +++++
bug/op_set_title.go    |  5 +++++
bug/operation.go       | 40 ++++++++++++++++++++++++++++++++++------
bug/operation_pack.go  |  3 +++
tests/graphql_test.go  |  2 +-
9 files changed, 64 insertions(+), 7 deletions(-)

Detailed changes

bug/bug_test.go 🔗

@@ -74,6 +74,9 @@ func TestBugSerialisation(t *testing.T) {
 
 	// ignore some fields
 	bug2.packs[0].commitHash = bug1.packs[0].commitHash
+	for i := range bug1.packs[0].Operations {
+		bug2.packs[0].Operations[i].base().hash = bug1.packs[0].Operations[i].base().hash
+	}
 
 	deep.CompareUnexportedFields = true
 	if diff := deep.Equal(bug1, bug2); diff != nil {

bug/op_add_comment.go 🔗

@@ -22,6 +22,10 @@ func (op AddCommentOperation) base() *OpBase {
 	return op.OpBase
 }
 
+func (op AddCommentOperation) Hash() (git.Hash, error) {
+	return hashOperation(op)
+}
+
 func (op AddCommentOperation) Apply(snapshot Snapshot) Snapshot {
 	comment := Comment{
 		Message:  op.Message,

bug/op_create.go 🔗

@@ -23,6 +23,10 @@ func (op CreateOperation) base() *OpBase {
 	return op.OpBase
 }
 
+func (op CreateOperation) Hash() (git.Hash, error) {
+	return hashOperation(op)
+}
+
 func (op CreateOperation) Apply(snapshot Snapshot) Snapshot {
 	snapshot.Title = op.Title
 	snapshot.Comments = []Comment{

bug/op_label_change.go 🔗

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"sort"
 
+	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/pkg/errors"
 )
 
@@ -20,6 +21,10 @@ func (op LabelChangeOperation) base() *OpBase {
 	return op.OpBase
 }
 
+func (op LabelChangeOperation) Hash() (git.Hash, error) {
+	return hashOperation(op)
+}
+
 // Apply apply the operation
 func (op LabelChangeOperation) Apply(snapshot Snapshot) Snapshot {
 	// Add in the set

bug/op_set_status.go 🔗

@@ -1,6 +1,7 @@
 package bug
 
 import (
+	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/pkg/errors"
 )
 
@@ -17,6 +18,10 @@ func (op SetStatusOperation) base() *OpBase {
 	return op.OpBase
 }
 
+func (op SetStatusOperation) Hash() (git.Hash, error) {
+	return hashOperation(op)
+}
+
 func (op SetStatusOperation) Apply(snapshot Snapshot) Snapshot {
 	snapshot.Status = op.Status
 

bug/op_set_title.go 🔗

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/text"
 )
 
@@ -21,6 +22,10 @@ func (op SetTitleOperation) base() *OpBase {
 	return op.OpBase
 }
 
+func (op SetTitleOperation) Hash() (git.Hash, error) {
+	return hashOperation(op)
+}
+
 func (op SetTitleOperation) Apply(snapshot Snapshot) Snapshot {
 	snapshot.Title = op.Title
 

bug/operation.go 🔗

@@ -1,11 +1,13 @@
 package bug
 
 import (
-	"github.com/MichaelMure/git-bug/util/git"
-	"github.com/pkg/errors"
-
+	"crypto/sha256"
+	"encoding/json"
 	"fmt"
 	"time"
+
+	"github.com/MichaelMure/git-bug/util/git"
+	"github.com/pkg/errors"
 )
 
 // OperationType is an operation type identifier
@@ -24,6 +26,8 @@ const (
 type Operation interface {
 	// base return the OpBase of the Operation, for package internal use
 	base() *OpBase
+	// Hash return the hash of the operation
+	Hash() (git.Hash, error)
 	// Time return the time when the operation was added
 	Time() time.Time
 	// GetUnixTime return the unix timestamp when the operation was added
@@ -40,11 +44,35 @@ type Operation interface {
 	GetMetadata(key string) (string, bool)
 }
 
+func hashRaw(data []byte) git.Hash {
+	hasher := sha256.New()
+	return git.Hash(fmt.Sprintf("%x", hasher.Sum(data)))
+}
+
+// hash compute the hash of the serialized operation
+func hashOperation(op Operation) (git.Hash, error) {
+	base := op.base()
+
+	if base.hash != "" {
+		return base.hash, nil
+	}
+
+	data, err := json.Marshal(op)
+	if err != nil {
+		return "", err
+	}
+
+	base.hash = hashRaw(data)
+
+	return base.hash, nil
+}
+
 // OpBase implement the common code for all operations
 type OpBase struct {
-	OperationType OperationType     `json:"type"`
-	Author        Person            `json:"author"`
-	UnixTime      int64             `json:"timestamp"`
+	OperationType OperationType `json:"type"`
+	Author        Person        `json:"author"`
+	UnixTime      int64         `json:"timestamp"`
+	hash          git.Hash
 	Metadata      map[string]string `json:"metadata,omitempty"`
 }
 

bug/operation_pack.go 🔗

@@ -62,6 +62,9 @@ func (opp *OperationPack) UnmarshalJSON(data []byte) error {
 			return err
 		}
 
+		// Compute the hash of the operation
+		op.base().hash = hashRaw(raw)
+
 		opp.Operations = append(opp.Operations, op)
 	}
 

tests/graphql_test.go 🔗

@@ -111,7 +111,7 @@ func TestQueries(t *testing.T) {
 				Nodes    []struct {
 					Author    Person
 					CreatedAt string `json:"createdAt"`
-					HumandId  string `json:"humanId"`
+					HumanId   string `json:"humanId"`
 					Id        string
 					LastEdit  string `json:"lastEdit"`
 					Status    string