diff --git a/bug/bug.go b/bug/bug.go index 1137ecfa..2c71d71b 100644 --- a/bug/bug.go +++ b/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 { diff --git a/bug/interface.go b/bug/interface.go index af10b895..79333d07 100644 --- a/bug/interface.go +++ b/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 { diff --git a/bug/operation.go b/bug/operation.go index cdf87931..7d71e352 100644 --- a/bug/operation.go +++ b/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 diff --git a/bug/operations/add_comment.go b/bug/operations/add_comment.go index b4126a8e..5ecc471a 100644 --- a/bug/operations/add_comment.go +++ b/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) diff --git a/bug/operations/create.go b/bug/operations/create.go index ecbafb6f..5fc939dd 100644 --- a/bug/operations/create.go +++ b/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 diff --git a/bug/operations/create_test.go b/bug/operations/create_test.go index a20472d3..319cdb7f 100644 --- a/bug/operations/create_test.go +++ b/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(), diff --git a/bug/snapshot.go b/bug/snapshot.go index 7f1d6099..1ef4534b 100644 --- a/bug/snapshot.go +++ b/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() +} diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go new file mode 100644 index 00000000..23ac459b --- /dev/null +++ b/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] +} diff --git a/graphql/graph/gen_graph.go b/graphql/graph/gen_graph.go index 06c65e84..798962e3 100644 --- a/graphql/graph/gen_graph.go +++ b/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 { diff --git a/graphql/resolvers/bug.go b/graphql/resolvers/bug.go index b3385243..858feb16 100644 --- a/graphql/resolvers/bug.go +++ b/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 +} diff --git a/termui/bug_table.go b/termui/bug_table.go index 169dd7c1..1158f768 100644 --- a/termui/bug_table.go +++ b/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),