bug: harmonize how time are used, fix some issues in command special formats

Michael Muré created

This assume that the convertion from time.Time <--> Unix timestamp is lossless which seems to be.

Change summary

bridge/github/export.go    |  2 
bridge/gitlab/export.go    |  2 
bridge/jira/export.go      |  2 
bug/op_create.go           |  2 
bug/op_create_test.go      |  2 
bug/operation.go           | 14 ++------
bug/snapshot.go            | 13 +------
cache/bug_excerpt.go       | 21 +++++++++---
cache/repo_cache.go        | 12 ++++--
commands/json_common.go    | 55 +++++++++++++++++++++++++++++++++
commands/ls.go             | 33 +++++++++----------
commands/show.go           | 66 +++++++++++++++++++--------------------
commands/user_ls.go        | 33 --------------------
graphql/models/lazy_bug.go |  8 ++--
termui/bug_table.go        |  3 -
termui/show_bug.go         |  2 
16 files changed, 142 insertions(+), 128 deletions(-)

Detailed changes

bridge/github/export.go 🔗

@@ -173,7 +173,7 @@ func (ge *githubExporter) ExportAll(ctx context.Context, repo *cache.RepoCache,
 
 				// ignore issues created before since date
 				// TODO: compare the Lamport time instead of using the unix time
-				if snapshot.CreatedAt.Before(since) {
+				if snapshot.CreateTime.Before(since) {
 					out <- core.NewExportNothing(b.Id(), "bug created before the since date")
 					continue
 				}

bridge/gitlab/export.go 🔗

@@ -131,7 +131,7 @@ func (ge *gitlabExporter) ExportAll(ctx context.Context, repo *cache.RepoCache,
 
 				// ignore issues created before since date
 				// TODO: compare the Lamport time instead of using the unix time
-				if snapshot.CreatedAt.Before(since) {
+				if snapshot.CreateTime.Before(since) {
 					out <- core.NewExportNothing(b.Id(), "bug created before the since date")
 					continue
 				}

bridge/jira/export.go 🔗

@@ -165,7 +165,7 @@ func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, si
 
 				// ignore issues whose last modification date is before the query date
 				// TODO: compare the Lamport time instead of using the unix time
-				if snapshot.CreatedAt.Before(since) {
+				if snapshot.CreateTime.Before(since) {
 					out <- core.NewExportNothing(b.Id(), "bug created before the since date")
 					continue
 				}

bug/op_create.go 🔗

@@ -48,7 +48,7 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) {
 
 	snapshot.Comments = []Comment{comment}
 	snapshot.Author = op.Author
-	snapshot.CreatedAt = op.Time()
+	snapshot.CreateTime = op.Time()
 
 	snapshot.Timeline = []TimelineItem{
 		&CreateTimelineItem{

bug/op_create_test.go 🔗

@@ -38,7 +38,7 @@ func TestCreate(t *testing.T) {
 		Author:       rene,
 		Participants: []identity.Interface{rene},
 		Actors:       []identity.Interface{rene},
-		CreatedAt:    create.Time(),
+		CreateTime:   create.Time(),
 		Timeline: []TimelineItem{
 			&CreateTimelineItem{
 				CommentTimelineItem: NewCommentTimelineItem(id, comment),

bug/operation.go 🔗

@@ -36,8 +36,6 @@ type Operation interface {
 	Id() entity.Id
 	// Time return the time when the operation was added
 	Time() time.Time
-	// GetUnixTime return the unix timestamp when the operation was added
-	GetUnixTime() int64
 	// GetFiles return the files needed by this operation
 	GetFiles() []git.Hash
 	// Apply the operation to a Snapshot to create the final state
@@ -89,8 +87,9 @@ func idOperation(op Operation) entity.Id {
 type OpBase struct {
 	OperationType OperationType      `json:"type"`
 	Author        identity.Interface `json:"author"`
-	UnixTime      int64              `json:"timestamp"`
-	Metadata      map[string]string  `json:"metadata,omitempty"`
+	// TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
+	UnixTime int64             `json:"timestamp"`
+	Metadata map[string]string `json:"metadata,omitempty"`
 	// Not serialized. Store the op's id in memory.
 	id entity.Id
 	// Not serialized. Store the extra metadata in memory,
@@ -142,11 +141,6 @@ func (op *OpBase) Time() time.Time {
 	return time.Unix(op.UnixTime, 0)
 }
 
-// GetUnixTime return the unix timestamp when the operation was added
-func (op *OpBase) GetUnixTime() int64 {
-	return op.UnixTime
-}
-
 // GetFiles return the files needed by this operation
 func (op *OpBase) GetFiles() []git.Hash {
 	return nil
@@ -158,7 +152,7 @@ func opBaseValidate(op Operation, opType OperationType) error {
 		return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType)
 	}
 
-	if op.GetUnixTime() == 0 {
+	if op.Time().Unix() == 0 {
 		return fmt.Errorf("time not set")
 	}
 

bug/snapshot.go 🔗

@@ -19,7 +19,7 @@ type Snapshot struct {
 	Author       identity.Interface
 	Actors       []identity.Interface
 	Participants []identity.Interface
-	CreatedAt    time.Time
+	CreateTime   time.Time
 
 	Timeline []TimelineItem
 
@@ -32,7 +32,7 @@ func (snap *Snapshot) Id() entity.Id {
 }
 
 // Return the last time a bug was modified
-func (snap *Snapshot) LastEditTime() time.Time {
+func (snap *Snapshot) EditTime() time.Time {
 	if len(snap.Operations) == 0 {
 		return time.Unix(0, 0)
 	}
@@ -40,15 +40,6 @@ func (snap *Snapshot) LastEditTime() time.Time {
 	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].GetUnixTime()
-}
-
 // GetCreateMetadata return the creation metadata
 func (snap *Snapshot) GetCreateMetadata(key string) (string, bool) {
 	return snap.Operations[0].GetMetadata(key)

cache/bug_excerpt.go 🔗

@@ -3,6 +3,7 @@ package cache
 import (
 	"encoding/gob"
 	"fmt"
+	"time"
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/entity"
@@ -22,8 +23,8 @@ type BugExcerpt struct {
 
 	CreateLamportTime lamport.Time
 	EditLamportTime   lamport.Time
-	CreateUnixTime    int64
-	EditUnixTime      int64
+	createUnixTime    int64
+	editUnixTime      int64
 
 	Status       bug.Status
 	Labels       []bug.Label
@@ -79,8 +80,8 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
 		Id:                b.Id(),
 		CreateLamportTime: b.CreateLamportTime(),
 		EditLamportTime:   b.EditLamportTime(),
-		CreateUnixTime:    b.FirstOp().GetUnixTime(),
-		EditUnixTime:      snap.LastEditUnix(),
+		createUnixTime:    b.FirstOp().Time().Unix(),
+		editUnixTime:      snap.EditTime().Unix(),
 		Status:            snap.Status,
 		Labels:            snap.Labels,
 		Actors:            actorsIds,
@@ -105,6 +106,14 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
 	return e
 }
 
+func (b *BugExcerpt) CreateTime() time.Time {
+	return time.Unix(b.createUnixTime, 0)
+}
+
+func (b *BugExcerpt) EditTime() time.Time {
+	return time.Unix(b.editUnixTime, 0)
+}
+
 /*
  * Sorting
  */
@@ -144,7 +153,7 @@ func (b BugsByCreationTime) Less(i, j int) bool {
 	// 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
+	return b[i].createUnixTime < b[j].createUnixTime
 }
 
 func (b BugsByCreationTime) Swap(i, j int) {
@@ -172,7 +181,7 @@ func (b BugsByEditTime) Less(i, j int) bool {
 	// 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
+	return b[i].editUnixTime < b[j].editUnixTime
 }
 
 func (b BugsByEditTime) Swap(i, j int) {

cache/repo_cache.go 🔗

@@ -29,7 +29,8 @@ const identityCacheFile = "identity-cache"
 
 // 1: original format
 // 2: added cache for identities with a reference in the bug cache
-const formatVersion = 2
+// 3: CreateUnixTime --> createUnixTime, EditUnixTime --> editUnixTime
+const formatVersion = 3
 
 type ErrInvalidCacheFormat struct {
 	message string
@@ -99,10 +100,13 @@ func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, error
 	if err == nil {
 		return c, nil
 	}
-	if _, ok := err.(ErrInvalidCacheFormat); ok {
+	if _, ok := err.(ErrInvalidCacheFormat); !ok {
+		// Actual error
 		return nil, err
 	}
 
+	// We have an outdated cache format, rebuilding it.
+
 	err = c.buildCache()
 	if err != nil {
 		return nil, err
@@ -254,7 +258,7 @@ func (c *RepoCache) loadBugCache() error {
 		return err
 	}
 
-	if aux.Version != 2 {
+	if aux.Version != formatVersion {
 		return ErrInvalidCacheFormat{
 			message: fmt.Sprintf("unknown cache format version %v", aux.Version),
 		}
@@ -286,7 +290,7 @@ func (c *RepoCache) loadIdentityCache() error {
 		return err
 	}
 
-	if aux.Version != 2 {
+	if aux.Version != formatVersion {
 		return ErrInvalidCacheFormat{
 			message: fmt.Sprintf("unknown cache format version %v", aux.Version),
 		}

commands/json_common.go 🔗

@@ -0,0 +1,55 @@
+package commands
+
+import (
+	"time"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/lamport"
+)
+
+type JSONIdentity struct {
+	Id      string `json:"id"`
+	HumanId string `json:"human_id"`
+	Name    string `json:"name"`
+	Login   string `json:"login"`
+}
+
+func NewJSONIdentity(i identity.Interface) JSONIdentity {
+	return JSONIdentity{
+		Id:      i.Id().String(),
+		HumanId: i.Id().Human(),
+		Name:    i.Name(),
+		Login:   i.Login(),
+	}
+}
+
+func NewJSONIdentityFromExcerpt(excerpt *cache.IdentityExcerpt) JSONIdentity {
+	return JSONIdentity{
+		Id:      excerpt.Id.String(),
+		HumanId: excerpt.Id.Human(),
+		Name:    excerpt.Name,
+		Login:   excerpt.Login,
+	}
+}
+
+func NewJSONIdentityFromLegacyExcerpt(excerpt *cache.LegacyAuthorExcerpt) JSONIdentity {
+	return JSONIdentity{
+		Name:  excerpt.Name,
+		Login: excerpt.Login,
+	}
+}
+
+type JSONTime struct {
+	Timestamp int64        `json:"timestamp"`
+	Time      time.Time    `json:"time"`
+	Lamport   lamport.Time `json:"lamport,omitempty"`
+}
+
+func NewJSONTime(t time.Time, l lamport.Time) JSONTime {
+	return JSONTime{
+		Timestamp: t.Unix(),
+		Time:      t,
+		Lamport:   l,
+	}
+}

commands/ls.go 🔗

@@ -4,7 +4,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"strings"
-	"time"
 
 	text "github.com/MichaelMure/go-term-text"
 	"github.com/spf13/cobra"
@@ -75,10 +74,10 @@ func runLsBug(_ *cobra.Command, args []string) error {
 }
 
 type JSONBugExcerpt struct {
-	Id           string    `json:"id"`
-	HumanId      string    `json:"human_id"`
-	CreationTime time.Time `json:"creation_time"`
-	LastEdited   time.Time `json:"last_edited"`
+	Id         string   `json:"id"`
+	HumanId    string   `json:"human_id"`
+	CreateTime JSONTime `json:"create_time"`
+	EditTime   JSONTime `json:"edit_time"`
 
 	Status       string         `json:"status"`
 	Labels       []bug.Label    `json:"labels"`
@@ -95,15 +94,15 @@ func lsJsonFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerpt)
 	jsonBugs := make([]JSONBugExcerpt, len(bugExcerpts))
 	for i, b := range bugExcerpts {
 		jsonBug := JSONBugExcerpt{
-			Id:           b.Id.String(),
-			HumanId:      b.Id.Human(),
-			CreationTime: time.Unix(b.CreateUnixTime, 0),
-			LastEdited:   time.Unix(b.EditUnixTime, 0),
-			Status:       b.Status.String(),
-			Labels:       b.Labels,
-			Title:        b.Title,
-			Comments:     b.LenComments,
-			Metadata:     b.CreateMetadata,
+			Id:         b.Id.String(),
+			HumanId:    b.Id.Human(),
+			CreateTime: NewJSONTime(b.CreateTime(), b.CreateLamportTime),
+			EditTime:   NewJSONTime(b.EditTime(), b.EditLamportTime),
+			Status:     b.Status.String(),
+			Labels:     b.Labels,
+			Title:      b.Title,
+			Comments:   b.LenComments,
+			Metadata:   b.CreateMetadata,
 		}
 
 		if b.AuthorId != "" {
@@ -225,15 +224,13 @@ func lsOrgmodeFormatter(backend *cache.RepoCache, bugExcerpts []*cache.BugExcerp
 		fmt.Printf("* %s %s [%s] %s: %s %s\n",
 			b.Id.Human(),
 			status,
-			time.Unix(b.CreateUnixTime, 0),
+			b.CreateTime(),
 			name,
 			title,
 			labelsString,
 		)
 
-		fmt.Printf("** Last Edited: %s\n",
-			time.Unix(b.EditUnixTime, 0).String(),
-		)
+		fmt.Printf("** Last Edited: %s\n", b.EditTime().String())
 
 		fmt.Printf("** Actors:\n")
 		for _, element := range b.Actors {

commands/show.go 🔗

@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"strings"
-	"time"
 
 	"github.com/spf13/cobra"
 
@@ -47,9 +46,9 @@ func runShowBug(_ *cobra.Command, args []string) error {
 		case "authorEmail":
 			fmt.Printf("%s\n", snapshot.Author.Email())
 		case "createTime":
-			fmt.Printf("%s\n", snapshot.CreatedAt.String())
+			fmt.Printf("%s\n", snapshot.CreateTime.String())
 		case "lastEdit":
-			fmt.Printf("%s\n", snapshot.LastEditTime().String())
+			fmt.Printf("%s\n", snapshot.EditTime().String())
 		case "humanId":
 			fmt.Printf("%s\n", snapshot.Id().Human())
 		case "id":
@@ -101,11 +100,11 @@ func showDefaultFormatter(snapshot *bug.Snapshot) error {
 
 	fmt.Printf("%s opened this issue %s\n",
 		colors.Magenta(snapshot.Author.DisplayName()),
-		snapshot.CreatedAt.String(),
+		snapshot.CreateTime.String(),
 	)
 
 	fmt.Printf("This was last edited at %s\n\n",
-		snapshot.LastEditTime().String(),
+		snapshot.EditTime().String(),
 	)
 
 	// Labels
@@ -166,37 +165,45 @@ func showDefaultFormatter(snapshot *bug.Snapshot) error {
 }
 
 type JSONBugSnapshot struct {
-	Id           string    `json:"id"`
-	HumanId      string    `json:"human_id"`
-	CreationTime time.Time `json:"creation_time"`
-	LastEdited   time.Time `json:"last_edited"`
-
+	Id           string         `json:"id"`
+	HumanId      string         `json:"human_id"`
+	CreateTime   JSONTime       `json:"create_time"`
+	EditTime     JSONTime       `json:"edit_time"`
 	Status       string         `json:"status"`
 	Labels       []bug.Label    `json:"labels"`
 	Title        string         `json:"title"`
 	Author       JSONIdentity   `json:"author"`
 	Actors       []JSONIdentity `json:"actors"`
 	Participants []JSONIdentity `json:"participants"`
-
-	Comments []JSONComment `json:"comments"`
+	Comments     []JSONComment  `json:"comments"`
 }
 
 type JSONComment struct {
-	Id      int          `json:"id"`
+	Id      string       `json:"id"`
+	HumanId string       `json:"human_id"`
 	Author  JSONIdentity `json:"author"`
 	Message string       `json:"message"`
 }
 
+func NewJSONComment(comment bug.Comment) JSONComment {
+	return JSONComment{
+		Id:      comment.Id().String(),
+		HumanId: comment.Id().Human(),
+		Author:  NewJSONIdentity(comment.Author),
+		Message: comment.Message,
+	}
+}
+
 func showJsonFormatter(snapshot *bug.Snapshot) error {
 	jsonBug := JSONBugSnapshot{
-		Id:           snapshot.Id().String(),
-		HumanId:      snapshot.Id().Human(),
-		CreationTime: snapshot.CreatedAt,
-		LastEdited:   snapshot.LastEditTime(),
-		Status:       snapshot.Status.String(),
-		Labels:       snapshot.Labels,
-		Title:        snapshot.Title,
-		Author:       NewJSONIdentity(snapshot.Author),
+		Id:         snapshot.Id().String(),
+		HumanId:    snapshot.Id().Human(),
+		CreateTime: NewJSONTime(snapshot.CreateTime, 0),
+		EditTime:   NewJSONTime(snapshot.EditTime(), 0),
+		Status:     snapshot.Status.String(),
+		Labels:     snapshot.Labels,
+		Title:      snapshot.Title,
+		Author:     NewJSONIdentity(snapshot.Author),
 	}
 
 	jsonBug.Actors = make([]JSONIdentity, len(snapshot.Actors))
@@ -209,18 +216,9 @@ func showJsonFormatter(snapshot *bug.Snapshot) error {
 		jsonBug.Participants[i] = NewJSONIdentity(element)
 	}
 
+	jsonBug.Comments = make([]JSONComment, len(snapshot.Comments))
 	for i, comment := range snapshot.Comments {
-		var message string
-		if comment.Message == "" {
-			message = "No description provided."
-		} else {
-			message = comment.Message
-		}
-		jsonBug.Comments = append(jsonBug.Comments, JSONComment{
-			Id:      i,
-			Author:  NewJSONIdentity(comment.Author),
-			Message: message,
-		})
+		jsonBug.Comments[i] = NewJSONComment(comment)
 	}
 
 	jsonObject, _ := json.MarshalIndent(jsonBug, "", "    ")
@@ -242,11 +240,11 @@ func showOrgmodeFormatter(snapshot *bug.Snapshot) error {
 	)
 
 	fmt.Printf("* Creation Time: %s\n",
-		snapshot.CreatedAt.String(),
+		snapshot.CreateTime.String(),
 	)
 
 	fmt.Printf("* Last Edit: %s\n",
-		snapshot.LastEditTime().String(),
+		snapshot.EditTime().String(),
 	)
 
 	// Labels

commands/user_ls.go 🔗

@@ -7,7 +7,6 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/colors"
 	"github.com/MichaelMure/git-bug/util/interrupt"
 )
@@ -44,38 +43,6 @@ func runUserLs(_ *cobra.Command, _ []string) error {
 	}
 }
 
-type JSONIdentity struct {
-	Id      string `json:"id"`
-	HumanId string `json:"human_id"`
-	Name    string `json:"name"`
-	Login   string `json:"login"`
-}
-
-func NewJSONIdentity(i identity.Interface) JSONIdentity {
-	return JSONIdentity{
-		Id:      i.Id().String(),
-		HumanId: i.Id().Human(),
-		Name:    i.Name(),
-		Login:   i.Login(),
-	}
-}
-
-func NewJSONIdentityFromExcerpt(excerpt *cache.IdentityExcerpt) JSONIdentity {
-	return JSONIdentity{
-		Id:      excerpt.Id.String(),
-		HumanId: excerpt.Id.Human(),
-		Name:    excerpt.Name,
-		Login:   excerpt.Login,
-	}
-}
-
-func NewJSONIdentityFromLegacyExcerpt(excerpt *cache.LegacyAuthorExcerpt) JSONIdentity {
-	return JSONIdentity{
-		Name:  excerpt.Name,
-		Login: excerpt.Login,
-	}
-}
-
 func userLsDefaultFormatter(users []*cache.IdentityExcerpt) error {
 	for _, user := range users {
 		fmt.Printf("%s %s\n",

graphql/models/lazy_bug.go 🔗

@@ -81,7 +81,7 @@ func (lb *lazyBug) Id() entity.Id {
 }
 
 func (lb *lazyBug) LastEdit() time.Time {
-	return time.Unix(lb.excerpt.EditUnixTime, 0)
+	return lb.excerpt.EditTime()
 }
 
 func (lb *lazyBug) Status() bug.Status {
@@ -133,7 +133,7 @@ func (lb *lazyBug) Participants() ([]IdentityWrapper, error) {
 }
 
 func (lb *lazyBug) CreatedAt() time.Time {
-	return time.Unix(lb.excerpt.CreateUnixTime, 0)
+	return lb.excerpt.CreateTime()
 }
 
 func (lb *lazyBug) Timeline() ([]bug.TimelineItem, error) {
@@ -163,7 +163,7 @@ func NewLoadedBug(snap *bug.Snapshot) *loadedBug {
 }
 
 func (l *loadedBug) LastEdit() time.Time {
-	return l.Snapshot.LastEditTime()
+	return l.Snapshot.EditTime()
 }
 
 func (l *loadedBug) Status() bug.Status {
@@ -203,7 +203,7 @@ func (l *loadedBug) Participants() ([]IdentityWrapper, error) {
 }
 
 func (l *loadedBug) CreatedAt() time.Time {
-	return l.Snapshot.CreatedAt
+	return l.Snapshot.CreateTime
 }
 
 func (l *loadedBug) Timeline() ([]bug.TimelineItem, error) {

termui/bug_table.go 🔗

@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"fmt"
 	"strings"
-	"time"
 
 	"github.com/MichaelMure/go-term-text"
 	"github.com/awesome-gocui/gocui"
@@ -315,7 +314,7 @@ func (bt *bugTable) render(v *gocui.View, maxX int) {
 			authorDisplayName = excerpt.LegacyAuthor.DisplayName()
 		}
 
-		lastEditTime := time.Unix(excerpt.EditUnixTime, 0)
+		lastEditTime := excerpt.EditTime()
 
 		id := text.LeftPadMaxLine(excerpt.Id.Human(), columnWidths["id"], 1)
 		status := text.LeftPadMaxLine(excerpt.Status.String(), columnWidths["status"], 1)

termui/show_bug.go 🔗

@@ -219,7 +219,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 		colors.Bold(snap.Title),
 		colors.Yellow(snap.Status),
 		colors.Magenta(snap.Author.DisplayName()),
-		snap.CreatedAt.Format(timeLayout),
+		snap.CreateTime.Format(timeLayout),
 		edited,
 	)
 	bugHeader, lines := text.Wrap(bugHeader, maxX)