bug: add a new BugExerpt that hold a subset of a bug state for efficient sorting and retrieval

Michael Muré created

Change summary

bug/bug.go                    | 10 ++++
bug/interface.go              |  7 ++
bug/operation.go              | 13 ++++-
bug/operations/add_comment.go |  2 
bug/operations/create.go      |  2 
bug/operations/create_test.go |  2 
bug/snapshot.go               | 11 ++++
cache/bug_excerpt.go          | 92 +++++++++++++++++++++++++++++++++++++
graphql/graph/gen_graph.go    | 41 +++++++++++++---
graphql/resolvers/bug.go      |  5 ++
termui/bug_table.go           |  2 
11 files changed, 171 insertions(+), 16 deletions(-)

Detailed changes

bug/bug.go 🔗

@@ -579,6 +579,16 @@ func formatHumanId(id string) string {
 	return fmt.Sprintf(format, id)
 }
 
+// CreateLamportTime return the Lamport time of creation
+func (bug *Bug) CreateLamportTime() util.LamportTime {
+	return bug.createTime
+}
+
+// EditLamportTime return the Lamport time of the last edit
+func (bug *Bug) EditLamportTime() util.LamportTime {
+	return bug.editTime
+}
+
 // Lookup for the very first operation of the bug.
 // For a valid Bug, this operation should be a CreateOp
 func (bug *Bug) FirstOp() Operation {

bug/interface.go 🔗

@@ -2,6 +2,7 @@ package bug
 
 import (
 	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util"
 )
 
 type Interface interface {
@@ -38,6 +39,12 @@ type Interface interface {
 
 	// Compile a bug in a easily usable snapshot
 	Compile() Snapshot
+
+	// CreateLamportTime return the Lamport time of creation
+	CreateLamportTime() util.LamportTime
+
+	// EditLamportTime return the Lamport time of the last edit
+	EditLamportTime() util.LamportTime
 }
 
 func bugFromInterface(bug Interface) *Bug {

bug/operation.go 🔗

@@ -23,6 +23,8 @@ type Operation interface {
 	OpType() OperationType
 	// Time return the time when the operation was added
 	Time() time.Time
+	// unixTime return the unix timestamp when the operation was added
+	UnixTime() int64
 	// Apply the operation to a Snapshot to create the final state
 	Apply(snapshot Snapshot) Snapshot
 	// Files return the files needed by this operation
@@ -36,7 +38,7 @@ type Operation interface {
 type OpBase struct {
 	OperationType OperationType
 	Author        Person
-	UnixTime      int64
+	unixTime      int64
 }
 
 // NewOpBase is the constructor for an OpBase
@@ -44,7 +46,7 @@ func NewOpBase(opType OperationType, author Person) OpBase {
 	return OpBase{
 		OperationType: opType,
 		Author:        author,
-		UnixTime:      time.Now().Unix(),
+		unixTime:      time.Now().Unix(),
 	}
 }
 
@@ -55,7 +57,12 @@ func (op OpBase) OpType() OperationType {
 
 // Time return the time when the operation was added
 func (op OpBase) Time() time.Time {
-	return time.Unix(op.UnixTime, 0)
+	return time.Unix(op.unixTime, 0)
+}
+
+// unixTime return the unix timestamp when the operation was added
+func (op OpBase) UnixTime() int64 {
+	return op.unixTime
 }
 
 // Files return the files needed by this operation

bug/operations/add_comment.go 🔗

@@ -21,7 +21,7 @@ func (op AddCommentOperation) Apply(snapshot bug.Snapshot) bug.Snapshot {
 		Message:  op.Message,
 		Author:   op.Author,
 		Files:    op.files,
-		UnixTime: op.UnixTime,
+		UnixTime: op.UnixTime(),
 	}
 
 	snapshot.Comments = append(snapshot.Comments, comment)

bug/operations/create.go 🔗

@@ -22,7 +22,7 @@ func (op CreateOperation) Apply(snapshot bug.Snapshot) bug.Snapshot {
 		{
 			Message:  op.Message,
 			Author:   op.Author,
-			UnixTime: op.UnixTime,
+			UnixTime: op.UnixTime(),
 		},
 	}
 	snapshot.Author = op.Author

bug/operations/create_test.go 🔗

@@ -21,7 +21,7 @@ func TestCreate(t *testing.T) {
 	expected := bug.Snapshot{
 		Title: "title",
 		Comments: []bug.Comment{
-			{Author: rene, Message: "message", UnixTime: create.UnixTime},
+			{Author: rene, Message: "message", UnixTime: create.UnixTime()},
 		},
 		Author:    rene,
 		CreatedAt: create.Time(),

bug/snapshot.go 🔗

@@ -37,10 +37,19 @@ func (snap Snapshot) Summary() string {
 }
 
 // Return the last time a bug was modified
-func (snap Snapshot) LastEdit() time.Time {
+func (snap Snapshot) LastEditTime() time.Time {
 	if len(snap.Operations) == 0 {
 		return time.Unix(0, 0)
 	}
 
 	return snap.Operations[len(snap.Operations)-1].Time()
 }
+
+// Return the last timestamp a bug was modified
+func (snap Snapshot) LastEditUnix() int64 {
+	if len(snap.Operations) == 0 {
+		return 0
+	}
+
+	return snap.Operations[len(snap.Operations)-1].UnixTime()
+}

cache/bug_excerpt.go 🔗

@@ -0,0 +1,92 @@
+package cache
+
+import (
+	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/util"
+)
+
+// BugExcerpt hold a subset of the bug values to be able to sort and filter bugs
+// efficiently without having to read and compile each raw bugs.
+type BugExcerpt struct {
+	Id string
+
+	CreateLamportTime util.LamportTime
+	EditLamportTime   util.LamportTime
+	CreateUnixTime    int64
+	EditUnixTime      int64
+
+	Status bug.Status
+	Author bug.Person
+}
+
+func NewBugExcerpt(b *bug.Bug, snap bug.Snapshot) BugExcerpt {
+	return BugExcerpt{
+		Id:                b.Id(),
+		CreateLamportTime: b.CreateLamportTime(),
+		EditLamportTime:   b.EditLamportTime(),
+		CreateUnixTime:    b.FirstOp().UnixTime(),
+		EditUnixTime:      snap.LastEditUnix(),
+		Status:            snap.Status,
+		Author:            snap.Author,
+	}
+}
+
+/*
+ * Sorting
+ */
+
+type BugsByCreationTime []*BugExcerpt
+
+func (b BugsByCreationTime) Len() int {
+	return len(b)
+}
+
+func (b BugsByCreationTime) Less(i, j int) bool {
+	if b[i].CreateLamportTime < b[j].CreateLamportTime {
+		return true
+	}
+
+	if b[i].CreateLamportTime > b[j].CreateLamportTime {
+		return false
+	}
+
+	// When the logical clocks are identical, that means we had a concurrent
+	// edition. In this case we rely on the timestamp. While the timestamp might
+	// be incorrect due to a badly set clock, the drift in sorting is bounded
+	// by the first sorting using the logical clock. That means that if users
+	// synchronize their bugs regularly, the timestamp will rarely be used, and
+	// should still provide a kinda accurate sorting when needed.
+	return b[i].CreateUnixTime < b[j].CreateUnixTime
+}
+
+func (b BugsByCreationTime) Swap(i, j int) {
+	b[i], b[j] = b[j], b[i]
+}
+
+type BugsByEditTime []*BugExcerpt
+
+func (b BugsByEditTime) Len() int {
+	return len(b)
+}
+
+func (b BugsByEditTime) Less(i, j int) bool {
+	if b[i].EditLamportTime < b[j].EditLamportTime {
+		return true
+	}
+
+	if b[i].EditLamportTime > b[j].EditLamportTime {
+		return false
+	}
+
+	// When the logical clocks are identical, that means we had a concurrent
+	// edition. In this case we rely on the timestamp. While the timestamp might
+	// be incorrect due to a badly set clock, the drift in sorting is bounded
+	// by the first sorting using the logical clock. That means that if users
+	// synchronize their bugs regularly, the timestamp will rarely be used, and
+	// should still provide a kinda accurate sorting when needed.
+	return b[i].EditUnixTime < b[j].EditUnixTime
+}
+
+func (b BugsByEditTime) Swap(i, j int) {
+	b[i], b[j] = b[j], b[i]
+}

graphql/graph/gen_graph.go 🔗

@@ -34,6 +34,7 @@ type Resolvers interface {
 
 	Bug_status(ctx context.Context, obj *bug.Snapshot) (models.Status, error)
 
+	Bug_lastEdit(ctx context.Context, obj *bug.Snapshot) (time.Time, error)
 	Bug_comments(ctx context.Context, obj *bug.Snapshot, after *string, before *string, first *int, last *int) (models.CommentConnection, error)
 	Bug_operations(ctx context.Context, obj *bug.Snapshot, after *string, before *string, first *int, last *int) (models.OperationConnection, error)
 
@@ -78,6 +79,7 @@ type AddCommentOperationResolver interface {
 type BugResolver interface {
 	Status(ctx context.Context, obj *bug.Snapshot) (models.Status, error)
 
+	LastEdit(ctx context.Context, obj *bug.Snapshot) (time.Time, error)
 	Comments(ctx context.Context, obj *bug.Snapshot, after *string, before *string, first *int, last *int) (models.CommentConnection, error)
 	Operations(ctx context.Context, obj *bug.Snapshot, after *string, before *string, first *int, last *int) (models.OperationConnection, error)
 }
@@ -124,6 +126,10 @@ func (s shortMapper) Bug_status(ctx context.Context, obj *bug.Snapshot) (models.
 	return s.r.Bug().Status(ctx, obj)
 }
 
+func (s shortMapper) Bug_lastEdit(ctx context.Context, obj *bug.Snapshot) (time.Time, error) {
+	return s.r.Bug().LastEdit(ctx, obj)
+}
+
 func (s shortMapper) Bug_comments(ctx context.Context, obj *bug.Snapshot, after *string, before *string, first *int, last *int) (models.CommentConnection, error) {
 	return s.r.Bug().Comments(ctx, obj, after, before, first, last)
 }
@@ -494,14 +500,33 @@ func (ec *executionContext) _Bug_createdAt(ctx context.Context, field graphql.Co
 }
 
 func (ec *executionContext) _Bug_lastEdit(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {
-	rctx := graphql.GetResolverContext(ctx)
-	rctx.Object = "Bug"
-	rctx.Args = nil
-	rctx.Field = field
-	rctx.PushField(field.Alias)
-	defer rctx.Pop()
-	res := obj.LastEdit()
-	return graphql.MarshalTime(res)
+	ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{
+		Object: "Bug",
+		Args:   nil,
+		Field:  field,
+	})
+	return graphql.Defer(func() (ret graphql.Marshaler) {
+		defer func() {
+			if r := recover(); r != nil {
+				userErr := ec.Recover(ctx, r)
+				ec.Error(ctx, userErr)
+				ret = graphql.Null
+			}
+		}()
+
+		resTmp, err := ec.ResolverMiddleware(ctx, func(ctx context.Context) (interface{}, error) {
+			return ec.resolvers.Bug_lastEdit(ctx, obj)
+		})
+		if err != nil {
+			ec.Error(ctx, err)
+			return graphql.Null
+		}
+		if resTmp == nil {
+			return graphql.Null
+		}
+		res := resTmp.(time.Time)
+		return graphql.MarshalTime(res)
+	})
 }
 
 func (ec *executionContext) _Bug_comments(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {

graphql/resolvers/bug.go 🔗

@@ -2,6 +2,7 @@ package resolvers
 
 import (
 	"context"
+	"time"
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/graphql/connections"
@@ -67,3 +68,7 @@ func (bugResolver) Operations(ctx context.Context, obj *bug.Snapshot, after *str
 
 	return connections.BugOperationCon(obj.Operations, edger, conMaker, input)
 }
+
+func (bugResolver) LastEdit(ctx context.Context, obj *bug.Snapshot) (time.Time, error) {
+	return obj.LastEditTime(), nil
+}

termui/bug_table.go 🔗

@@ -290,7 +290,7 @@ func (bt *bugTable) render(v *gocui.View, maxX int) {
 		title := util.LeftPaddedString(snap.Title, columnWidths["title"], 2)
 		author := util.LeftPaddedString(person.Name, columnWidths["author"], 2)
 		summary := util.LeftPaddedString(snap.Summary(), columnWidths["summary"], 2)
-		lastEdit := util.LeftPaddedString(humanize.Time(snap.LastEdit()), columnWidths["lastEdit"], 2)
+		lastEdit := util.LeftPaddedString(humanize.Time(snap.LastEditTime()), columnWidths["lastEdit"], 2)
 
 		fmt.Fprintf(v, "%s %s %s %s %s %s\n",
 			util.Cyan(id),