From f0167a1d6068d22af5ffff260e2848f7c14a71b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 29 Mar 2023 15:40:10 +0200 Subject: [PATCH] invert package dependency between identity<-->entity --- api/graphql/models/lazy_bug.go | 2 +- api/graphql/resolvers/mutation.go | 18 ++-- bridge/github/export.go | 4 +- bridge/github/export_test.go | 10 +-- bridge/github/import_integration_test.go | 6 +- bridge/github/import_test.go | 4 +- bridge/gitlab/export.go | 4 +- bridge/gitlab/export_test.go | 10 +-- bridge/gitlab/import.go | 4 +- bridge/gitlab/import_test.go | 2 +- bridge/jira/export.go | 4 +- bridge/jira/import.go | 4 +- cache/bug_cache.go | 23 +++--- cache/bug_excerpt.go | 2 +- cache/bug_subcache.go | 7 +- cache/cached.go | 15 +++- cache/identity_cache.go | 2 +- cache/identity_subcache.go | 2 +- cache/repo_cache.go | 3 + cache/subcache.go | 4 +- commands/bug/bug_comment.go | 2 +- commands/bug/bug_label.go | 2 +- commands/bug/bug_select.go | 2 +- commands/bug/bug_show.go | 2 +- commands/bug/bug_status.go | 2 +- commands/bug/bug_title.go | 2 +- commands/bug/bug_title_edit.go | 2 +- commands/bug/completion.go | 2 +- commands/cmdjson/json_common.go | 4 +- entities/bug/bug.go | 9 +- entities/bug/op_add_comment.go | 8 +- entities/bug/op_create.go | 8 +- entities/bug/op_edit_comment.go | 8 +- entities/bug/operation.go | 4 +- entities/bug/snapshot.go | 9 +- entities/identity/identity.go | 32 ++++---- entities/identity/identity_actions.go | 40 ++++----- entities/identity/identity_actions_test.go | 4 +- entities/identity/identity_stub.go | 10 +-- entities/identity/identity_test.go | 8 +- entities/identity/identity_user.go | 16 ++-- entities/identity/interface.go | 4 +- entities/identity/resolver.go | 6 +- entities/identity/version.go | 20 ++--- entities/identity/version_test.go | 4 +- entity/{interface.go => boostrap/entity.go} | 5 +- entity/boostrap/err.go | 84 +++++++++++++++++++ entity/boostrap/id.go | 81 ++++++++++++++++++ entity/boostrap/merge.go | 91 +++++++++++++++++++++ entity/boostrap/refs.go | 20 +++++ entity/boostrap/resolver.go | 13 +++ entity/{ => boostrap}/streamed.go | 7 +- entity/dag/common_test.go | 4 +- entity/dag/entity.go | 17 ++-- entity/dag/entity_actions.go | 6 +- entity/dag/entity_actions_test.go | 3 +- entity/dag/example_test.go | 4 +- entity/dag/op_noop.go | 8 +- entity/dag/op_set_metadata.go | 8 +- entity/dag/op_set_metadata_test.go | 8 +- entity/dag/operation.go | 81 ++---------------- entity/dag/operation_pack.go | 2 +- entity/dag/operation_pack_test.go | 5 +- entity/{dag/interface.go => entity.go} | 47 ++++++----- entity/err.go | 65 ++------------- entity/id.go | 76 ++--------------- entity/id_interleaved.go | 6 +- entity/identity.go | 73 +++++++++++++++++ entity/merge.go | 88 +++----------------- entity/operations.go | 76 +++++++++++++++++ entity/refs.go | 19 ++--- entity/resolver.go | 11 +-- entity/snapshot.go | 14 ++++ termui/label_select.go | 4 +- termui/show_bug.go | 8 +- termui/termui.go | 2 +- 76 files changed, 762 insertions(+), 514 deletions(-) rename entity/{interface.go => boostrap/entity.go} (92%) create mode 100644 entity/boostrap/err.go create mode 100644 entity/boostrap/id.go create mode 100644 entity/boostrap/merge.go create mode 100644 entity/boostrap/refs.go create mode 100644 entity/boostrap/resolver.go rename entity/{ => boostrap}/streamed.go (59%) rename entity/{dag/interface.go => entity.go} (71%) create mode 100644 entity/identity.go create mode 100644 entity/operations.go create mode 100644 entity/snapshot.go diff --git a/api/graphql/models/lazy_bug.go b/api/graphql/models/lazy_bug.go index 4b0b598ef129185d0bd6973a049e7c35f1abd95f..ae9959ab30350b0e3de458a51fa261677482ac02 100644 --- a/api/graphql/models/lazy_bug.go +++ b/api/graphql/models/lazy_bug.go @@ -63,7 +63,7 @@ func (lb *lazyBug) load() error { return err } - lb.snap = b.Snapshot() + lb.snap = b.Compile() return nil } diff --git a/api/graphql/resolvers/mutation.go b/api/graphql/resolvers/mutation.go index 32e1fa7cf1e82eedceb5c116652ef58ad7ccb3bb..a57736f33ed398d31d88d275009ed7795a4423ab 100644 --- a/api/graphql/resolvers/mutation.go +++ b/api/graphql/resolvers/mutation.go @@ -62,7 +62,7 @@ func (r mutationResolver) NewBug(ctx context.Context, input models.NewBugInput) return &models.NewBugPayload{ ClientMutationID: input.ClientMutationID, - Bug: models.NewLoadedBug(b.Snapshot()), + Bug: models.NewLoadedBug(b.Compile()), Operation: op, }, nil } @@ -94,7 +94,7 @@ func (r mutationResolver) AddComment(ctx context.Context, input models.AddCommen return &models.AddCommentPayload{ ClientMutationID: input.ClientMutationID, - Bug: models.NewLoadedBug(b.Snapshot()), + Bug: models.NewLoadedBug(b.Compile()), Operation: op, }, nil } @@ -131,7 +131,7 @@ func (r mutationResolver) AddCommentAndClose(ctx context.Context, input models.A return &models.AddCommentAndCloseBugPayload{ ClientMutationID: input.ClientMutationID, - Bug: models.NewLoadedBug(b.Snapshot()), + Bug: models.NewLoadedBug(b.Compile()), CommentOperation: opAddComment, StatusOperation: opClose, }, nil @@ -169,7 +169,7 @@ func (r mutationResolver) AddCommentAndReopen(ctx context.Context, input models. return &models.AddCommentAndReopenBugPayload{ ClientMutationID: input.ClientMutationID, - Bug: models.NewLoadedBug(b.Snapshot()), + Bug: models.NewLoadedBug(b.Compile()), CommentOperation: opAddComment, StatusOperation: opReopen, }, nil @@ -209,7 +209,7 @@ func (r mutationResolver) EditComment(ctx context.Context, input models.EditComm return &models.EditCommentPayload{ ClientMutationID: input.ClientMutationID, - Bug: models.NewLoadedBug(b.Snapshot()), + Bug: models.NewLoadedBug(b.Compile()), Operation: op, }, nil } @@ -248,7 +248,7 @@ func (r mutationResolver) ChangeLabels(ctx context.Context, input *models.Change return &models.ChangeLabelPayload{ ClientMutationID: input.ClientMutationID, - Bug: models.NewLoadedBug(b.Snapshot()), + Bug: models.NewLoadedBug(b.Compile()), Operation: op, Results: resultsPtr, }, nil @@ -277,7 +277,7 @@ func (r mutationResolver) OpenBug(ctx context.Context, input models.OpenBugInput return &models.OpenBugPayload{ ClientMutationID: input.ClientMutationID, - Bug: models.NewLoadedBug(b.Snapshot()), + Bug: models.NewLoadedBug(b.Compile()), Operation: op, }, nil } @@ -305,7 +305,7 @@ func (r mutationResolver) CloseBug(ctx context.Context, input models.CloseBugInp return &models.CloseBugPayload{ ClientMutationID: input.ClientMutationID, - Bug: models.NewLoadedBug(b.Snapshot()), + Bug: models.NewLoadedBug(b.Compile()), Operation: op, }, nil } @@ -338,7 +338,7 @@ func (r mutationResolver) SetTitle(ctx context.Context, input models.SetTitleInp return &models.SetTitlePayload{ ClientMutationID: input.ClientMutationID, - Bug: models.NewLoadedBug(b.Snapshot()), + Bug: models.NewLoadedBug(b.Compile()), Operation: op, }, nil } diff --git a/bridge/github/export.go b/bridge/github/export.go index 0d340b497fb9c34a9baaa32eb59c72303a0b5df0..e7234b39f90bea09da9f0084a447551b75dccf09 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -175,7 +175,7 @@ func (ge *githubExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, return default: - snapshot := b.Snapshot() + snapshot := b.Compile() // ignore issues created before since date // TODO: compare the Lamport time instead of using the unix time @@ -197,7 +197,7 @@ func (ge *githubExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, // exportBug publish bugs and related events func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out chan<- core.ExportResult) { - snapshot := b.Snapshot() + snapshot := b.Compile() var bugUpdated bool var bugGithubID string diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index e06457d4f9bd58f45b986d2387918aca6a839d23..ae18ed8528494694dc049d140af9843442c9b3c4 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -249,10 +249,10 @@ func TestGithubPushPull(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // for each operation a SetMetadataOperation will be added // so number of operations should double - require.Len(t, tt.bug.Snapshot().Operations, tt.numOrOp*2) + require.Len(t, tt.bug.Compile().Operations, tt.numOrOp*2) // verify operation have correct metadata - for _, op := range tt.bug.Snapshot().Operations { + for _, op := range tt.bug.Compile().Operations { // Check if the originals operations (*not* SetMetadata) are tagged properly if _, ok := op.(dag.OperationDoesntChangeSnapshot); !ok { _, haveIDMetadata := op.GetMetadata(metaKeyGithubId) @@ -264,7 +264,7 @@ func TestGithubPushPull(t *testing.T) { } // get bug github ID - bugGithubID, ok := tt.bug.Snapshot().GetCreateMetadata(metaKeyGithubId) + bugGithubID, ok := tt.bug.Compile().GetCreateMetadata(metaKeyGithubId) require.True(t, ok) // retrieve bug from backendTwo @@ -272,10 +272,10 @@ func TestGithubPushPull(t *testing.T) { require.NoError(t, err) // verify bug have same number of original operations - require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp) + require.Len(t, importedBug.Compile().Operations, tt.numOrOp) // verify bugs are tagged with origin=github - issueOrigin, ok := importedBug.Snapshot().GetCreateMetadata(core.MetaKeyOrigin) + issueOrigin, ok := importedBug.Compile().GetCreateMetadata(core.MetaKeyOrigin) require.True(t, ok) require.Equal(t, issueOrigin, target) diff --git a/bridge/github/import_integration_test.go b/bridge/github/import_integration_test.go index 8c411d8dde48be7c9dfe530b9619d248dcfbb9e2..d3a6b5f650a318e3709de73a0b6650a20f0aa987 100644 --- a/bridge/github/import_integration_test.go +++ b/bridge/github/import_integration_test.go @@ -54,14 +54,14 @@ func TestGithubImporterIntegration(t *testing.T) { b1, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/1") require.NoError(t, err) - ops1 := b1.Snapshot().Operations + ops1 := b1.Compile().Operations require.Equal(t, "marcus", ops1[0].Author().Name()) require.Equal(t, "title 1", ops1[0].(*bug.CreateOperation).Title) require.Equal(t, "body text 1", ops1[0].(*bug.CreateOperation).Message) b3, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/3") require.NoError(t, err) - ops3 := b3.Snapshot().Operations + ops3 := b3.Compile().Operations require.Equal(t, "issue 3 comment 1", ops3[1].(*bug.AddCommentOperation).Message) require.Equal(t, "issue 3 comment 2", ops3[2].(*bug.AddCommentOperation).Message) require.Equal(t, []bug.Label{"bug"}, ops3[3].(*bug.LabelChangeOperation).Added) @@ -69,7 +69,7 @@ func TestGithubImporterIntegration(t *testing.T) { b4, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/4") require.NoError(t, err) - ops4 := b4.Snapshot().Operations + ops4 := b4.Compile().Operations require.Equal(t, "edited", ops4[1].(*bug.EditCommentOperation).Message) } diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index b0ff2f99c647208d3982cc774a20d31bedc03f50..eacb3955105cfbf3dc0b62cae5d49cdc475a77c1 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -178,8 +178,8 @@ func TestGithubImporter(t *testing.T) { b, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGithubUrl, tt.url) require.NoError(t, err) - ops := b.Snapshot().Operations - require.Len(t, tt.bug.Operations, len(b.Snapshot().Operations)) + ops := b.Compile().Operations + require.Len(t, tt.bug.Operations, len(b.Compile().Operations)) for i, op := range tt.bug.Operations { require.IsType(t, ops[i], op) diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go index 3beaec383ccb68cc3bd5d092cc6851dc3d9e00ed..3f6f00b225b0bf84693c0ec3c4ae3a503f062f5c 100644 --- a/bridge/gitlab/export.go +++ b/bridge/gitlab/export.go @@ -128,7 +128,7 @@ func (ge *gitlabExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, return } - snapshot := b.Snapshot() + snapshot := b.Compile() // ignore issues created before since date // TODO: compare the Lamport time instead of using the unix time @@ -150,7 +150,7 @@ func (ge *gitlabExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, // exportBug publish bugs and related events func (ge *gitlabExporter) exportBug(ctx context.Context, b *cache.BugCache, out chan<- core.ExportResult) { - snapshot := b.Snapshot() + snapshot := b.Compile() var bugUpdated bool var err error diff --git a/bridge/gitlab/export_test.go b/bridge/gitlab/export_test.go index 7c82682242a41bc4a8a5f20e13a9db0259e4aeaf..d8fd7ece5bc2948c6fc68e6d3e0080f806bfb4b1 100644 --- a/bridge/gitlab/export_test.go +++ b/bridge/gitlab/export_test.go @@ -245,10 +245,10 @@ func TestGitlabPushPull(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // for each operation a SetMetadataOperation will be added // so number of operations should double - require.Len(t, tt.bug.Snapshot().Operations, tt.numOpExp) + require.Len(t, tt.bug.Compile().Operations, tt.numOpExp) // verify operation have correct metadata - for _, op := range tt.bug.Snapshot().Operations { + for _, op := range tt.bug.Compile().Operations { // Check if the originals operations (*not* SetMetadata) are tagged properly if _, ok := op.(dag.OperationDoesntChangeSnapshot); !ok { _, haveIDMetadata := op.GetMetadata(metaKeyGitlabId) @@ -260,7 +260,7 @@ func TestGitlabPushPull(t *testing.T) { } // get bug gitlab ID - bugGitlabID, ok := tt.bug.Snapshot().GetCreateMetadata(metaKeyGitlabId) + bugGitlabID, ok := tt.bug.Compile().GetCreateMetadata(metaKeyGitlabId) require.True(t, ok) // retrieve bug from backendTwo @@ -268,10 +268,10 @@ func TestGitlabPushPull(t *testing.T) { require.NoError(t, err) // verify bug have same number of original operations - require.Len(t, importedBug.Snapshot().Operations, tt.numOpImp) + require.Len(t, importedBug.Compile().Operations, tt.numOpImp) // verify bugs are tagged with origin=gitlab - issueOrigin, ok := importedBug.Snapshot().GetCreateMetadata(core.MetaKeyOrigin) + issueOrigin, ok := importedBug.Compile().GetCreateMetadata(core.MetaKeyOrigin) require.True(t, ok) require.Equal(t, issueOrigin, target) diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go index e4330b4c8a5f089beda85cfbe94cae8e9cb1d9c1..b5b7b7555c8db787a676d52ffb3d88321abd9b14 100644 --- a/bridge/gitlab/import.go +++ b/bridge/gitlab/import.go @@ -198,7 +198,7 @@ func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCa gi.out <- core.NewImportStatusChange(b.Id(), op.Id()) case EventDescriptionChanged: - firstComment := b.Snapshot().Comments[0] + firstComment := b.Compile().Comments[0] // since gitlab doesn't provide the issue history // we should check for "changed the description" notes and compare issue texts // TODO: Check only one time and ignore next 'description change' within one issue @@ -247,7 +247,7 @@ func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCa // if comment was already exported // search for last comment update - comment, err := b.Snapshot().SearchCommentByOpId(id) + comment, err := b.Compile().SearchCommentByOpId(id) if err != nil { return err } diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go index bed93a800e2bba4c646df1e4c7f18762eb1d2ea8..e2b7b8dfe3997071ced7cd7dde900bc64def00b1 100644 --- a/bridge/gitlab/import_test.go +++ b/bridge/gitlab/import_test.go @@ -133,7 +133,7 @@ func TestGitlabImport(t *testing.T) { b, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGitlabUrl, tt.url) require.NoError(t, err) - ops := b.Snapshot().Operations + ops := b.Compile().Operations require.Len(t, tt.bug.Operations, len(ops)) for i, op := range tt.bug.Operations { diff --git a/bridge/jira/export.go b/bridge/jira/export.go index 95f9e28c1e00096e93ff51e6b06d621620e8c5e9..7639c6ec7034a9d2005038d8e37f7ca4c84247ab 100644 --- a/bridge/jira/export.go +++ b/bridge/jira/export.go @@ -161,7 +161,7 @@ func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, si return default: - snapshot := b.Snapshot() + snapshot := b.Compile() // ignore issues whose last modification date is before the query date // TODO: compare the Lamport time instead of using the unix time @@ -189,7 +189,7 @@ func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, si // exportBug publish bugs and related events func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, out chan<- core.ExportResult) error { - snapshot := b.Snapshot() + snapshot := b.Compile() var bugJiraID string diff --git a/bridge/jira/import.go b/bridge/jira/import.go index d8a5f8ddffb364c3fde256a90405069d7749f903..fa6d39b95c2419432b6321586852adda893ba10e 100644 --- a/bridge/jira/import.go +++ b/bridge/jira/import.go @@ -124,7 +124,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si out <- core.NewImportError(commentIter.Err, "") } - snapshot := b.Snapshot() + snapshot := b.Compile() opIdx := 0 var changelogIter *ChangeLogIterator @@ -466,7 +466,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e // title but it's actually the body opr, isRightType := potentialOp.(*bug.EditCommentOperation) if isRightType && - opr.Target == b.Snapshot().Operations[0].Id() && + opr.Target == b.Compile().Operations[0].Id() && opr.Message == item.ToString { _, err := b.SetMetadata(opr.Id(), map[string]string{ metaKeyJiraDerivedId: entry.ID, diff --git a/cache/bug_cache.go b/cache/bug_cache.go index 3466f18626953b6547e03f28fa57f567a9f7cd0c..8f4bca27fdd9cec19a5671496de5006a28bf7f3e 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -5,7 +5,6 @@ import ( "time" "github.com/MichaelMure/git-bug/entities/bug" - "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/repository" @@ -13,6 +12,8 @@ import ( var ErrNoMatchingOp = fmt.Errorf("no matching operation found") +var _ bug.Interface = &BugCache{} + // BugCache is a wrapper around a Bug. It provides multiple functions: // // 1. Provide a higher level API to use than the raw API from Bug. @@ -46,7 +47,7 @@ func (c *BugCache) AddCommentWithFiles(message string, files []repository.Hash) return c.AddCommentRaw(author, time.Now().Unix(), message, files, nil) } -func (c *BugCache) AddCommentRaw(author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *bug.AddCommentOperation, error) { +func (c *BugCache) AddCommentRaw(author entity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *bug.AddCommentOperation, error) { c.mu.Lock() commentId, op, err := bug.AddComment(c.entity, author, unixTime, message, files, metadata) c.mu.Unlock() @@ -65,7 +66,7 @@ func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelCh return c.ChangeLabelsRaw(author, time.Now().Unix(), added, removed, nil) } -func (c *BugCache) ChangeLabelsRaw(author identity.Interface, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, *bug.LabelChangeOperation, error) { +func (c *BugCache) ChangeLabelsRaw(author entity.Interface, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, *bug.LabelChangeOperation, error) { c.mu.Lock() changes, op, err := bug.ChangeLabels(c.entity, author, unixTime, added, removed, metadata) c.mu.Unlock() @@ -84,7 +85,7 @@ func (c *BugCache) ForceChangeLabels(added []string, removed []string) (*bug.Lab return c.ForceChangeLabelsRaw(author, time.Now().Unix(), added, removed, nil) } -func (c *BugCache) ForceChangeLabelsRaw(author identity.Interface, unixTime int64, added []string, removed []string, metadata map[string]string) (*bug.LabelChangeOperation, error) { +func (c *BugCache) ForceChangeLabelsRaw(author entity.Interface, unixTime int64, added []string, removed []string, metadata map[string]string) (*bug.LabelChangeOperation, error) { c.mu.Lock() op, err := bug.ForceChangeLabels(c.entity, author, unixTime, added, removed, metadata) c.mu.Unlock() @@ -103,7 +104,7 @@ func (c *BugCache) Open() (*bug.SetStatusOperation, error) { return c.OpenRaw(author, time.Now().Unix(), nil) } -func (c *BugCache) OpenRaw(author identity.Interface, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) { +func (c *BugCache) OpenRaw(author entity.Interface, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) { c.mu.Lock() op, err := bug.Open(c.entity, author, unixTime, metadata) c.mu.Unlock() @@ -122,7 +123,7 @@ func (c *BugCache) Close() (*bug.SetStatusOperation, error) { return c.CloseRaw(author, time.Now().Unix(), nil) } -func (c *BugCache) CloseRaw(author identity.Interface, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) { +func (c *BugCache) CloseRaw(author entity.Interface, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) { c.mu.Lock() op, err := bug.Close(c.entity, author, unixTime, metadata) c.mu.Unlock() @@ -141,7 +142,7 @@ func (c *BugCache) SetTitle(title string) (*bug.SetTitleOperation, error) { return c.SetTitleRaw(author, time.Now().Unix(), title, nil) } -func (c *BugCache) SetTitleRaw(author identity.Interface, unixTime int64, title string, metadata map[string]string) (*bug.SetTitleOperation, error) { +func (c *BugCache) SetTitleRaw(author entity.Interface, unixTime int64, title string, metadata map[string]string) (*bug.SetTitleOperation, error) { c.mu.Lock() op, err := bug.SetTitle(c.entity, author, unixTime, title, metadata) c.mu.Unlock() @@ -162,7 +163,7 @@ func (c *BugCache) EditCreateComment(body string) (entity.CombinedId, *bug.EditC } // EditCreateCommentRaw is a convenience function to edit the body of a bug (the first comment) -func (c *BugCache) EditCreateCommentRaw(author identity.Interface, unixTime int64, body string, metadata map[string]string) (entity.CombinedId, *bug.EditCommentOperation, error) { +func (c *BugCache) EditCreateCommentRaw(author entity.Interface, unixTime int64, body string, metadata map[string]string) (entity.CombinedId, *bug.EditCommentOperation, error) { c.mu.Lock() commentId, op, err := bug.EditCreateComment(c.entity, author, unixTime, body, nil, metadata) c.mu.Unlock() @@ -181,8 +182,8 @@ func (c *BugCache) EditComment(target entity.CombinedId, message string) (*bug.E return c.EditCommentRaw(author, time.Now().Unix(), target, message, nil) } -func (c *BugCache) EditCommentRaw(author identity.Interface, unixTime int64, target entity.CombinedId, message string, metadata map[string]string) (*bug.EditCommentOperation, error) { - comment, err := c.Snapshot().SearchComment(target) +func (c *BugCache) EditCommentRaw(author entity.Interface, unixTime int64, target entity.CombinedId, message string, metadata map[string]string) (*bug.EditCommentOperation, error) { + comment, err := c.Compile().SearchComment(target) if err != nil { return nil, err } @@ -208,7 +209,7 @@ func (c *BugCache) SetMetadata(target entity.Id, newMetadata map[string]string) return c.SetMetadataRaw(author, time.Now().Unix(), target, newMetadata) } -func (c *BugCache) SetMetadataRaw(author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*bug.Snapshot], error) { +func (c *BugCache) SetMetadataRaw(author entity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*bug.Snapshot], error) { c.mu.Lock() op, err := bug.SetMetadata(c.entity, author, unixTime, target, newMetadata) c.mu.Unlock() diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index 26b7ec742bcb0d58bce60f11aa163a2413293634..d73c0e56bb72df60bf075ac6443c841e2f18064d 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -39,7 +39,7 @@ type BugExcerpt struct { } func NewBugExcerpt(b *BugCache) *BugExcerpt { - snap := b.Snapshot() + snap := b.Compile() participantsIds := make([]entity.Id, 0, len(snap.Participants)) for _, participant := range snap.Participants { participantsIds = append(participantsIds, participant.Id()) diff --git a/cache/bug_subcache.go b/cache/bug_subcache.go index 21c9a6d28e31eab2e749797ab749b775d12a77a6..fb3e4f4258bf7a0709a53ddc0d8e5401e483ba61 100644 --- a/cache/bug_subcache.go +++ b/cache/bug_subcache.go @@ -6,7 +6,6 @@ import ( "time" "github.com/MichaelMure/git-bug/entities/bug" - "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/query" "github.com/MichaelMure/git-bug/repository" @@ -25,7 +24,7 @@ func NewRepoCacheBug(repo repository.ClockedRepo, } makeIndexData := func(b *BugCache) []string { - snap := b.Snapshot() + snap := b.Compile() var res []string for _, comment := range snap.Comments { res = append(res, comment.Message) @@ -90,7 +89,7 @@ func (c *RepoCacheBug) ResolveComment(prefix string) (*BugCache, entity.Combined return nil, entity.UnsetCombinedId, err } - for _, comment := range b.Snapshot().Comments { + for _, comment := range b.Compile().Comments { if comment.CombinedId().HasPrefix(prefix) { matchingBugIds = append(matchingBugIds, bugId) matchingBug = b @@ -235,7 +234,7 @@ func (c *RepoCacheBug) NewWithFiles(title string, message string, files []reposi // NewRaw create a new bug with attached files for the message, as // well as metadata for the Create operation. // The new bug is written in the repository (commit) -func (c *RepoCacheBug) NewRaw(author identity.Interface, unixTime int64, title string, message string, files []repository.Hash, metadata map[string]string) (*BugCache, *bug.CreateOperation, error) { +func (c *RepoCacheBug) NewRaw(author entity.Interface, unixTime int64, title string, message string, files []repository.Hash, metadata map[string]string) (*BugCache, *bug.CreateOperation, error) { b, op, err := bug.Create(author, unixTime, title, message, files, metadata) if err != nil { return nil, nil, err diff --git a/cache/cached.go b/cache/cached.go index 9f9e170d4580c16d9861d7b3843e277d6cc440fd..99f384878c8111b30ec6d4479b12be855bea3389 100644 --- a/cache/cached.go +++ b/cache/cached.go @@ -9,6 +9,7 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) +var _ dag.Interface[dag.Snapshot, dag.Operation] = &CachedEntityBase[dag.Snapshot, dag.Operation]{} var _ CacheEntity = &CachedEntityBase[dag.Snapshot, dag.Operation]{} // CachedEntityBase provide the base function of an entity managed by the cache. @@ -25,7 +26,7 @@ func (e *CachedEntityBase[SnapT, OpT]) Id() entity.Id { return e.entity.Id() } -func (e *CachedEntityBase[SnapT, OpT]) Snapshot() SnapT { +func (e *CachedEntityBase[SnapT, OpT]) Compile() SnapT { e.mu.RLock() defer e.mu.RUnlock() return e.entity.Compile() @@ -66,6 +67,18 @@ func (e *CachedEntityBase[SnapT, OpT]) Validate() error { return e.entity.Validate() } +func (e *CachedEntityBase[SnapT, OpT]) Append(op OpT) { + e.mu.Lock() + defer e.mu.Unlock() + e.entity.Append(op) +} + +func (e *CachedEntityBase[SnapT, OpT]) Operations() []OpT { + e.mu.RLock() + defer e.mu.RUnlock() + return e.entity.Operations() +} + func (e *CachedEntityBase[SnapT, OpT]) Commit() error { e.mu.Lock() err := e.entity.Commit(e.repo) diff --git a/cache/identity_cache.go b/cache/identity_cache.go index 466b6150be6a8f991c38b4016f5382c5a8b3a147..243a5be350784411453fa9332c41ee53ed19e864 100644 --- a/cache/identity_cache.go +++ b/cache/identity_cache.go @@ -8,7 +8,7 @@ import ( "github.com/MichaelMure/git-bug/repository" ) -var _ identity.Interface = &IdentityCache{} +var _ entity.Interface = &IdentityCache{} var _ CacheEntity = &IdentityCache{} // IdentityCache is a wrapper around an Identity for caching. diff --git a/cache/identity_subcache.go b/cache/identity_subcache.go index 05a9135852efa956abf03db12b267e60a8992fd3..7f3be01dc9f1639c485d3f56a0b1d7737467423e 100644 --- a/cache/identity_subcache.go +++ b/cache/identity_subcache.go @@ -41,7 +41,7 @@ func NewRepoCacheIdentity(repo repository.ClockedRepo, }, Remove: identity.Remove, RemoveAll: identity.RemoveAll, - MergeAll: func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult { + MergeAll: func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor entity.Interface) <-chan entity.MergeResult { return identity.MergeAll(repo, remote) }, } diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 19b6e266001bceee6661efef1a76b236855b09a2..3021aade0fb8bb17a01eb365e67e893ed0676be8 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -7,6 +7,7 @@ import ( "strconv" "sync" + "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/multierr" @@ -97,6 +98,8 @@ func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, chan c.resolvers = entity.Resolvers{ &IdentityCache{}: entity.ResolverFunc[*IdentityCache](c.identities.Resolve), &IdentityExcerpt{}: entity.ResolverFunc[*IdentityExcerpt](c.identities.ResolveExcerpt), + + bug.Interface(nil): entity.ResolverFunc[*BugCache](c.bugs.Resolve), &BugCache{}: entity.ResolverFunc[*BugCache](c.bugs.Resolve), &BugExcerpt{}: entity.ResolverFunc[*BugExcerpt](c.bugs.ResolveExcerpt), } diff --git a/cache/subcache.go b/cache/subcache.go index b0ba6e529bc1347baca95da6168dc71016ebc22f..c1a550c3cbb9af52d626c0a46c39c8633fce4546 100644 --- a/cache/subcache.go +++ b/cache/subcache.go @@ -9,7 +9,6 @@ import ( "github.com/pkg/errors" - "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" ) @@ -35,7 +34,7 @@ type Actions[EntityT entity.Interface] struct { ReadAllWithResolver func(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan entity.StreamedEntity[EntityT] Remove func(repo repository.ClockedRepo, id entity.Id) error RemoveAll func(repo repository.ClockedRepo) error - MergeAll func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult + MergeAll func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor entity.Interface) <-chan entity.MergeResult } var _ cacheMgmt = &SubCache[entity.Interface, Excerpt, CacheEntity]{} @@ -597,7 +596,6 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) entityUpdated(id entity.Id) error return errors.New("entity missing from cache") } sc.lru.Get(id) - // sc.excerpts[id] = bug2.NewBugExcerpt(b.bug, b.Snapshot()) sc.excerpts[id] = sc.makeExcerpt(e) sc.mu.Unlock() diff --git a/commands/bug/bug_comment.go b/commands/bug/bug_comment.go index b037f688b54a55a0e890ae937a560448986b03b0..a27027ec081b68fe9938df053bf9dd0f7dd255ae 100644 --- a/commands/bug/bug_comment.go +++ b/commands/bug/bug_comment.go @@ -31,7 +31,7 @@ func runBugComment(env *execenv.Env, args []string) error { return err } - snap := b.Snapshot() + snap := b.Compile() for i, comment := range snap.Comments { if i != 0 { diff --git a/commands/bug/bug_label.go b/commands/bug/bug_label.go index ec076922d015506214abbbf4d3c340cf6f5590d0..e55171ce69134f03ff852e6f7a3e54cc378c0b9b 100644 --- a/commands/bug/bug_label.go +++ b/commands/bug/bug_label.go @@ -29,7 +29,7 @@ func runBugLabel(env *execenv.Env, args []string) error { return err } - snap := b.Snapshot() + snap := b.Compile() for _, l := range snap.Labels { env.Out.Println(l) diff --git a/commands/bug/bug_select.go b/commands/bug/bug_select.go index c93cd7b1a23868d0b27040d38c999014f35fe81f..96063a07498dbb0ff2862246aa00fd77bd910542 100644 --- a/commands/bug/bug_select.go +++ b/commands/bug/bug_select.go @@ -59,7 +59,7 @@ func runBugSelect(env *execenv.Env, args []string) error { return err } - env.Out.Printf("selected bug %s: %s\n", b.Id().Human(), b.Snapshot().Title) + env.Out.Printf("selected bug %s: %s\n", b.Id().Human(), b.Compile().Title) return nil } diff --git a/commands/bug/bug_show.go b/commands/bug/bug_show.go index ef20df2a3954bff9f8613e121f9e2bb214b3134a..c2696f2f85aabd7bd7dab484b0197debae1df841 100644 --- a/commands/bug/bug_show.go +++ b/commands/bug/bug_show.go @@ -52,7 +52,7 @@ func runBugShow(env *execenv.Env, opts bugShowOptions, args []string) error { return err } - snap := b.Snapshot() + snap := b.Compile() if len(snap.Comments) == 0 { return errors.New("invalid bug: no comment") diff --git a/commands/bug/bug_status.go b/commands/bug/bug_status.go index c8542240b72294e8c23a34ede567700308d4b6e6..dcc21e143ef7bea497cf26753e98a29790b451a9 100644 --- a/commands/bug/bug_status.go +++ b/commands/bug/bug_status.go @@ -29,7 +29,7 @@ func runBugStatus(env *execenv.Env, args []string) error { return err } - snap := b.Snapshot() + snap := b.Compile() env.Out.Println(snap.Status) diff --git a/commands/bug/bug_title.go b/commands/bug/bug_title.go index af959fb734efb3824fa66377d48e14a6ad3050b1..289b353a25d7794eea1ac8009bdaa18999d4c242 100644 --- a/commands/bug/bug_title.go +++ b/commands/bug/bug_title.go @@ -28,7 +28,7 @@ func runBugTitle(env *execenv.Env, args []string) error { return err } - snap := b.Snapshot() + snap := b.Compile() env.Out.Println(snap.Title) diff --git a/commands/bug/bug_title_edit.go b/commands/bug/bug_title_edit.go index 2d6bafca1d12bd4a32194d84d0c9818f57fd8683..b6e83ca0b095fb8aec4ebdb3f44ac8897f58fe4e 100644 --- a/commands/bug/bug_title_edit.go +++ b/commands/bug/bug_title_edit.go @@ -43,7 +43,7 @@ func runBugTitleEdit(env *execenv.Env, opts bugTitleEditOptions, args []string) return err } - snap := b.Snapshot() + snap := b.Compile() if opts.title == "" { if opts.nonInteractive { diff --git a/commands/bug/completion.go b/commands/bug/completion.go index 62bf658a77d61a32e6cb4222054d5f89e063dedf..070d097dac1d205158b1ecd89a42f6ac6e91f8b0 100644 --- a/commands/bug/completion.go +++ b/commands/bug/completion.go @@ -59,7 +59,7 @@ func BugAndLabelsCompletion(env *execenv.Env, addOrRemove bool) completion.Valid return completion.HandleError(err) } - snap := b.Snapshot() + snap := b.Compile() seenLabels := map[bug.Label]bool{} for _, label := range cleanArgs { diff --git a/commands/cmdjson/json_common.go b/commands/cmdjson/json_common.go index 34077915f1701123a0987a0492746eff4be685cc..aa2e117646ec5f769161de108ce78895c4a3a25c 100644 --- a/commands/cmdjson/json_common.go +++ b/commands/cmdjson/json_common.go @@ -4,7 +4,7 @@ import ( "time" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/entities/identity" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -15,7 +15,7 @@ type Identity struct { Login string `json:"login"` } -func NewIdentity(i identity.Interface) Identity { +func NewIdentity(i entity.Interface) Identity { return Identity{ Id: i.Id().String(), HumanId: i.Id().Human(), diff --git a/entities/bug/bug.go b/entities/bug/bug.go index deb00c7cb8612744dc513fe334ee0ee42d9f361d..ab873ed8f635cb97e1b4759d1f27b9bced56a9b0 100644 --- a/entities/bug/bug.go +++ b/entities/bug/bug.go @@ -7,12 +7,13 @@ import ( "github.com/MichaelMure/git-bug/entities/common" "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/repository" ) var _ Interface = &Bug{} -var _ entity.Interface = &Bug{} +var _ entity.Interface[*Snapshot, Operation] = &Bug{} // 1: original format // 2: no more legacy identities @@ -33,7 +34,7 @@ var def = dag.Definition{ var ClockLoader = dag.ClockLoader(def) type Interface interface { - dag.Interface[*Snapshot, Operation] + entity.Interface[*Snapshot, Operation] } // Bug holds the data of a bug thread, organized in a way close to @@ -69,12 +70,12 @@ func ReadWithResolver(repo repository.ClockedRepo, resolvers entity.Resolvers, i } // ReadAll read and parse all local bugs -func ReadAll(repo repository.ClockedRepo) <-chan entity.StreamedEntity[*Bug] { +func ReadAll(repo repository.ClockedRepo) <-chan bootstrap.StreamedEntity[*Bug] { return dag.ReadAll(def, wrapper, repo, simpleResolvers(repo)) } // ReadAllWithResolver read and parse all local bugs -func ReadAllWithResolver(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan entity.StreamedEntity[*Bug] { +func ReadAllWithResolver(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan bootstrap.StreamedEntity[*Bug] { return dag.ReadAll(def, wrapper, repo, resolvers) } diff --git a/entities/bug/op_add_comment.go b/entities/bug/op_add_comment.go index 5edef9d06b2d8f42bd9caae7a435c6c40d0d41e8..17cc5dd0575ab8f7496c075009832e851a296b70 100644 --- a/entities/bug/op_add_comment.go +++ b/entities/bug/op_add_comment.go @@ -12,7 +12,7 @@ import ( ) var _ Operation = &AddCommentOperation{} -var _ dag.OperationWithFiles = &AddCommentOperation{} +var _ entity.OperationWithFiles = &AddCommentOperation{} // AddCommentOperation will add a new comment in the bug type AddCommentOperation struct { @@ -63,6 +63,12 @@ func (op *AddCommentOperation) Validate() error { return fmt.Errorf("message is not fully printable") } + for _, file := range op.Files { + if !file.IsValid() { + return fmt.Errorf("invalid file hash") + } + } + return nil } diff --git a/entities/bug/op_create.go b/entities/bug/op_create.go index 2afea4061f86fa9ab001054fb84f6b376ad8f1be..63eb438d0e970d8bbc5722827f9110451e8743f6 100644 --- a/entities/bug/op_create.go +++ b/entities/bug/op_create.go @@ -12,7 +12,7 @@ import ( ) var _ Operation = &CreateOperation{} -var _ dag.OperationWithFiles = &CreateOperation{} +var _ entity.OperationWithFiles = &CreateOperation{} // CreateOperation define the initial creation of a bug type CreateOperation struct { @@ -80,6 +80,12 @@ func (op *CreateOperation) Validate() error { return fmt.Errorf("message is not fully printable") } + for _, file := range op.Files { + if !file.IsValid() { + return fmt.Errorf("invalid file hash") + } + } + return nil } diff --git a/entities/bug/op_edit_comment.go b/entities/bug/op_edit_comment.go index 6ac09fd7ad578761edef1c0eee45509ba239fdca..788d16d923d1cb75979c6305dc80d3d508402b1d 100644 --- a/entities/bug/op_edit_comment.go +++ b/entities/bug/op_edit_comment.go @@ -15,7 +15,7 @@ import ( ) var _ Operation = &EditCommentOperation{} -var _ dag.OperationWithFiles = &EditCommentOperation{} +var _ entity.OperationWithFiles = &EditCommentOperation{} // EditCommentOperation will change a comment in the bug type EditCommentOperation struct { @@ -98,6 +98,12 @@ func (op *EditCommentOperation) Validate() error { return fmt.Errorf("message is not fully printable") } + for _, file := range op.Files { + if !file.IsValid() { + return fmt.Errorf("invalid file hash") + } + } + return nil } diff --git a/entities/bug/operation.go b/entities/bug/operation.go index 04365046cbbaab4460a62a341df829304e542aa7..08aba2d722bed36d9d5f23f9cebd0be4a4a6996c 100644 --- a/entities/bug/operation.go +++ b/entities/bug/operation.go @@ -9,7 +9,7 @@ import ( ) const ( - _ dag.OperationType = iota + _ entity.OperationType = iota CreateOp SetTitleOp AddCommentOp @@ -29,7 +29,7 @@ var _ Operation = &dag.SetMetadataOperation[*Snapshot]{} func operationUnmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (dag.Operation, error) { var t struct { - OperationType dag.OperationType `json:"type"` + OperationType entity.OperationType `json:"type"` } if err := json.Unmarshal(raw, &t); err != nil { diff --git a/entities/bug/snapshot.go b/entities/bug/snapshot.go index 5c260d85f87099447810ae30e41cb379d86e3937..9dbc78625b09ec2174c1bbb3d1812f94c4d152ab 100644 --- a/entities/bug/snapshot.go +++ b/entities/bug/snapshot.go @@ -7,10 +7,9 @@ import ( "github.com/MichaelMure/git-bug/entities/common" "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/entity/dag" ) -var _ dag.Snapshot = &Snapshot{} +var _ entity.Snapshot = &Snapshot{} // Snapshot is a compiled form of the Bug data structure used for storage and merge type Snapshot struct { @@ -27,7 +26,7 @@ type Snapshot struct { Timeline []TimelineItem - Operations []dag.Operation + Operations []entity.Operation } // Id returns the Bug identifier @@ -39,11 +38,11 @@ func (snap *Snapshot) Id() entity.Id { return snap.id } -func (snap *Snapshot) AllOperations() []dag.Operation { +func (snap *Snapshot) AllOperations() []entity.Operation { return snap.Operations } -func (snap *Snapshot) AppendOperation(op dag.Operation) { +func (snap *Snapshot) AppendOperation(op entity.Operation) { snap.Operations = append(snap.Operations, op) } diff --git a/entities/identity/identity.go b/entities/identity/identity.go index 91564ffa518f4069a1ccfd72f44e9d91800ed772..9effd14668f6ef32816e2149ea7331013cc644f0 100644 --- a/entities/identity/identity.go +++ b/entities/identity/identity.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" - "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" "github.com/MichaelMure/git-bug/util/timestamp" @@ -29,7 +29,7 @@ var ErrNoIdentitySet = errors.New("No identity is set.\n" + var ErrMultipleIdentitiesSet = errors.New("multiple user identities set") var _ Interface = &Identity{} -var _ entity.Interface = &Identity{} +var _ bootstrap.Entity = &Identity{} type Identity struct { // all the successive version of the identity @@ -87,7 +87,7 @@ func (i *Identity) UnmarshalJSON(data []byte) error { } // ReadLocal load a local Identity from the identities data available in git -func ReadLocal(repo repository.Repo, id entity.Id) (*Identity, error) { +func ReadLocal(repo repository.Repo, id bootstrap.Id) (*Identity, error) { ref := fmt.Sprintf("%s%s", identityRefPattern, id) return read(repo, ref) } @@ -100,7 +100,7 @@ func ReadRemote(repo repository.Repo, remote string, id string) (*Identity, erro // read will load and parse an identity from git func read(repo repository.Repo, ref string) (*Identity, error) { - id := entity.RefToId(ref) + id := bootstrap.RefToId(ref) if err := id.Validate(); err != nil { return nil, errors.Wrap(err, "invalid ref") @@ -108,7 +108,7 @@ func read(repo repository.Repo, ref string) (*Identity, error) { hashes, err := repo.ListCommits(ref) if err != nil { - return nil, entity.NewErrNotFound(Typename) + return nil, bootstrap.NewErrNotFound(Typename) } if len(hashes) == 0 { return nil, fmt.Errorf("empty identity") @@ -155,36 +155,36 @@ func read(repo repository.Repo, ref string) (*Identity, error) { } // ListLocalIds list all the available local identity ids -func ListLocalIds(repo repository.Repo) ([]entity.Id, error) { +func ListLocalIds(repo repository.Repo) ([]bootstrap.Id, error) { refs, err := repo.ListRefs(identityRefPattern) if err != nil { return nil, err } - return entity.RefsToIds(refs), nil + return bootstrap.RefsToIds(refs), nil } // ReadAllLocal read and parse all local Identity -func ReadAllLocal(repo repository.ClockedRepo) <-chan entity.StreamedEntity[*Identity] { +func ReadAllLocal(repo repository.ClockedRepo) <-chan bootstrap.StreamedEntity[*Identity] { return readAll(repo, identityRefPattern) } // ReadAllRemote read and parse all remote Identity for a given remote -func ReadAllRemote(repo repository.ClockedRepo, remote string) <-chan entity.StreamedEntity[*Identity] { +func ReadAllRemote(repo repository.ClockedRepo, remote string) <-chan bootstrap.StreamedEntity[*Identity] { refPrefix := fmt.Sprintf(identityRemoteRefPattern, remote) return readAll(repo, refPrefix) } // readAll read and parse all available bug with a given ref prefix -func readAll(repo repository.ClockedRepo, refPrefix string) <-chan entity.StreamedEntity[*Identity] { - out := make(chan entity.StreamedEntity[*Identity]) +func readAll(repo repository.ClockedRepo, refPrefix string) <-chan bootstrap.StreamedEntity[*Identity] { + out := make(chan bootstrap.StreamedEntity[*Identity]) go func() { defer close(out) refs, err := repo.ListRefs(refPrefix) if err != nil { - out <- entity.StreamedEntity[*Identity]{Err: err} + out <- bootstrap.StreamedEntity[*Identity]{Err: err} return } @@ -195,11 +195,11 @@ func readAll(repo repository.ClockedRepo, refPrefix string) <-chan entity.Stream i, err := read(repo, ref) if err != nil { - out <- entity.StreamedEntity[*Identity]{Err: err} + out <- bootstrap.StreamedEntity[*Identity]{Err: err} return } - out <- entity.StreamedEntity[*Identity]{ + out <- bootstrap.StreamedEntity[*Identity]{ Entity: i, CurrentEntity: current, TotalEntities: total, @@ -425,7 +425,7 @@ func (i *Identity) lastVersion() *version { } // Id return the Identity identifier -func (i *Identity) Id() entity.Id { +func (i *Identity) Id() bootstrap.Id { // id is the id of the first version return i.versions[0].Id() } @@ -534,7 +534,7 @@ func (i *Identity) SetMetadata(key string, value string) { i.versions = append(i.versions, i.lastVersion().Clone()) } // if Id() has been called, we can't change the first version anymore, so we create a new version - if len(i.versions) == 1 && i.versions[0].id != entity.UnsetId && i.versions[0].id != "" { + if len(i.versions) == 1 && i.versions[0].id != bootstrap.UnsetId && i.versions[0].id != "" { i.versions = append(i.versions, i.lastVersion().Clone()) } diff --git a/entities/identity/identity_actions.go b/entities/identity/identity_actions.go index 07560dc04231a622e1f518148b6952429c7d1bfd..2f8e27e01d6fcaa0169fe3d42d7aa0499a33e336 100644 --- a/entities/identity/identity_actions.go +++ b/entities/identity/identity_actions.go @@ -6,7 +6,7 @@ import ( "github.com/pkg/errors" - "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" ) @@ -33,7 +33,7 @@ func Pull(repo repository.ClockedRepo, remote string) error { if merge.Err != nil { return merge.Err } - if merge.Status == entity.MergeStatusInvalid { + if merge.Status == bootstrap.MergeStatusInvalid { return errors.Errorf("merge failure: %s", merge.Reason) } } @@ -42,8 +42,8 @@ func Pull(repo repository.ClockedRepo, remote string) error { } // MergeAll will merge all the available remote identity -func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeResult { - out := make(chan entity.MergeResult) +func MergeAll(repo repository.ClockedRepo, remote string) <-chan bootstrap.MergeResult { + out := make(chan bootstrap.MergeResult) go func() { defer close(out) @@ -52,29 +52,29 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes remoteRefs, err := repo.ListRefs(remoteRefSpec) if err != nil { - out <- entity.MergeResult{Err: err} + out <- bootstrap.MergeResult{Err: err} return } for _, remoteRef := range remoteRefs { refSplit := strings.Split(remoteRef, "/") - id := entity.Id(refSplit[len(refSplit)-1]) + id := bootstrap.Id(refSplit[len(refSplit)-1]) if err := id.Validate(); err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error()) + out <- bootstrap.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error()) continue } remoteIdentity, err := read(repo, remoteRef) if err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote identity is not readable").Error()) + out <- bootstrap.NewMergeInvalidStatus(id, errors.Wrap(err, "remote identity is not readable").Error()) continue } // Check for error in remote data if err := remoteIdentity.Validate(); err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote identity is invalid").Error()) + out <- bootstrap.NewMergeInvalidStatus(id, errors.Wrap(err, "remote identity is invalid").Error()) continue } @@ -82,7 +82,7 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes localExist, err := repo.RefExist(localRef) if err != nil { - out <- entity.NewMergeError(err, id) + out <- bootstrap.NewMergeError(err, id) continue } @@ -91,32 +91,32 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes err := repo.CopyRef(remoteRef, localRef) if err != nil { - out <- entity.NewMergeError(err, id) + out <- bootstrap.NewMergeError(err, id) return } - out <- entity.NewMergeNewStatus(id, remoteIdentity) + out <- bootstrap.NewMergeNewStatus(id, remoteIdentity) continue } localIdentity, err := read(repo, localRef) if err != nil { - out <- entity.NewMergeError(errors.Wrap(err, "local identity is not readable"), id) + out <- bootstrap.NewMergeError(errors.Wrap(err, "local identity is not readable"), id) return } updated, err := localIdentity.Merge(repo, remoteIdentity) if err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error()) + out <- bootstrap.NewMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error()) return } if updated { - out <- entity.NewMergeUpdatedStatus(id, localIdentity) + out <- bootstrap.NewMergeUpdatedStatus(id, localIdentity) } else { - out <- entity.NewMergeNothingStatus(id) + out <- bootstrap.NewMergeNothingStatus(id) } } }() @@ -128,7 +128,7 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes // It is left as a responsibility to the caller to make sure that this identities is not // linked from another entity, otherwise it would break it. // Remove is idempotent. -func Remove(repo repository.ClockedRepo, id entity.Id) error { +func Remove(repo repository.ClockedRepo, id bootstrap.Id) error { var fullMatches []string refs, err := repo.ListRefs(identityRefPattern + id.String()) @@ -136,7 +136,7 @@ func Remove(repo repository.ClockedRepo, id entity.Id) error { return err } if len(refs) > 1 { - return entity.NewErrMultipleMatch(Typename, entity.RefsToIds(refs)) + return bootstrap.NewErrMultipleMatch(Typename, bootstrap.RefsToIds(refs)) } if len(refs) == 1 { // we have the identity locally @@ -155,7 +155,7 @@ func Remove(repo repository.ClockedRepo, id entity.Id) error { return err } if len(remoteRefs) > 1 { - return entity.NewErrMultipleMatch(Typename, entity.RefsToIds(refs)) + return bootstrap.NewErrMultipleMatch(Typename, bootstrap.RefsToIds(refs)) } if len(remoteRefs) == 1 { // found the identity in a remote @@ -164,7 +164,7 @@ func Remove(repo repository.ClockedRepo, id entity.Id) error { } if len(fullMatches) == 0 { - return entity.NewErrNotFound(Typename) + return bootstrap.NewErrNotFound(Typename) } for _, ref := range fullMatches { diff --git a/entities/identity/identity_actions_test.go b/entities/identity/identity_actions_test.go index e9626cb97a8f5783ee5aad48ac95c6728ffd951b..c91815eb43d445957cf8da4710ca301aef0b134a 100644 --- a/entities/identity/identity_actions_test.go +++ b/entities/identity/identity_actions_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" ) @@ -146,7 +146,7 @@ func TestIdentityPushPull(t *testing.T) { } } -func allIdentities(t testing.TB, identities <-chan entity.StreamedEntity[*Identity]) []*Identity { +func allIdentities(t testing.TB, identities <-chan bootstrap.StreamedEntity[*Identity]) []*Identity { var result []*Identity for streamed := range identities { if streamed.Err != nil { diff --git a/entities/identity/identity_stub.go b/entities/identity/identity_stub.go index fb5c90a5c8684356be2b7801f0fa73d184ee31b8..67a9db5314ac295d1050cb910acb632f25bcf229 100644 --- a/entities/identity/identity_stub.go +++ b/entities/identity/identity_stub.go @@ -3,7 +3,7 @@ package identity import ( "encoding/json" - "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" "github.com/MichaelMure/git-bug/util/timestamp" @@ -17,13 +17,13 @@ var _ Interface = &IdentityStub{} // When this JSON is deserialized, an IdentityStub is returned instead, to be replaced // later by the proper Identity, loaded from the Repo. type IdentityStub struct { - id entity.Id + id bootstrap.Id } func (i *IdentityStub) MarshalJSON() ([]byte, error) { // TODO: add a type marker return json.Marshal(struct { - Id entity.Id `json:"id"` + Id bootstrap.Id `json:"id"` }{ Id: i.id, }) @@ -31,7 +31,7 @@ func (i *IdentityStub) MarshalJSON() ([]byte, error) { func (i *IdentityStub) UnmarshalJSON(data []byte) error { aux := struct { - Id entity.Id `json:"id"` + Id bootstrap.Id `json:"id"` }{} if err := json.Unmarshal(data, &aux); err != nil { @@ -44,7 +44,7 @@ func (i *IdentityStub) UnmarshalJSON(data []byte) error { } // Id return the Identity identifier -func (i *IdentityStub) Id() entity.Id { +func (i *IdentityStub) Id() bootstrap.Id { return i.id } diff --git a/entities/identity/identity_test.go b/entities/identity/identity_test.go index 85d5385bc1dac54e26b4265c25d63174f5d21915..e4ecfbe91bc277b3bc62f9f09763262347a2bed3 100644 --- a/entities/identity/identity_test.go +++ b/entities/identity/identity_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -279,13 +279,13 @@ func TestIdentityRemove(t *testing.T) { require.NoError(t, err) _, err = ReadLocal(repo, rene.Id()) - require.ErrorAs(t, entity.ErrNotFound{}, err) + require.ErrorAs(t, bootstrap.ErrNotFound{}, err) _, err = ReadRemote(repo, "remoteA", string(rene.Id())) - require.ErrorAs(t, entity.ErrNotFound{}, err) + require.ErrorAs(t, bootstrap.ErrNotFound{}, err) _, err = ReadRemote(repo, "remoteB", string(rene.Id())) - require.ErrorAs(t, entity.ErrNotFound{}, err) + require.ErrorAs(t, bootstrap.ErrNotFound{}, err) ids, err := ListLocalIds(repo) require.NoError(t, err) diff --git a/entities/identity/identity_user.go b/entities/identity/identity_user.go index f9e39bb22ad55e38ff563630aa5500d3a36e2be0..a4b7bba354607cb4e41532d5bfbf30c014c420de 100644 --- a/entities/identity/identity_user.go +++ b/entities/identity/identity_user.go @@ -6,7 +6,7 @@ import ( "github.com/pkg/errors" - "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" ) @@ -27,7 +27,7 @@ func GetUserIdentity(repo repository.Repo) (*Identity, error) { } i, err := ReadLocal(repo, id) - if entity.IsErrNotFound(err) { + if bootstrap.IsErrNotFound(err) { innerErr := repo.LocalConfig().RemoveAll(identityConfigKey) if innerErr != nil { _, _ = fmt.Fprintln(os.Stderr, errors.Wrap(innerErr, "can't clear user identity").Error()) @@ -38,22 +38,22 @@ func GetUserIdentity(repo repository.Repo) (*Identity, error) { return i, nil } -func GetUserIdentityId(repo repository.Repo) (entity.Id, error) { +func GetUserIdentityId(repo repository.Repo) (bootstrap.Id, error) { val, err := repo.LocalConfig().ReadString(identityConfigKey) if errors.Is(err, repository.ErrNoConfigEntry) { - return entity.UnsetId, ErrNoIdentitySet + return bootstrap.UnsetId, ErrNoIdentitySet } if errors.Is(err, repository.ErrMultipleConfigEntry) { - return entity.UnsetId, ErrMultipleIdentitiesSet + return bootstrap.UnsetId, ErrMultipleIdentitiesSet } if err != nil { - return entity.UnsetId, err + return bootstrap.UnsetId, err } - var id = entity.Id(val) + var id = bootstrap.Id(val) if err := id.Validate(); err != nil { - return entity.UnsetId, err + return bootstrap.UnsetId, err } return id, nil diff --git a/entities/identity/interface.go b/entities/identity/interface.go index c6e22e0008dd81c107bff292078e3407c793468a..88234341b6fe7957554c4e949733fe356805087b 100644 --- a/entities/identity/interface.go +++ b/entities/identity/interface.go @@ -1,14 +1,14 @@ package identity import ( - "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" "github.com/MichaelMure/git-bug/util/timestamp" ) type Interface interface { - entity.Interface + bootstrap.Entity // Name return the last version of the name // Can be empty. diff --git a/entities/identity/resolver.go b/entities/identity/resolver.go index a4b676f349ef4ccae97d0d79017b87696a49b9f5..89a94edaf8bbd1ce29b0c21061da1aa7778ce0c4 100644 --- a/entities/identity/resolver.go +++ b/entities/identity/resolver.go @@ -1,11 +1,11 @@ package identity import ( - "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" ) -var _ entity.Resolver = &SimpleResolver{} +var _ bootstrap.Resolver = &SimpleResolver{} // SimpleResolver is a Resolver loading Identities directly from a Repo type SimpleResolver struct { @@ -16,6 +16,6 @@ func NewSimpleResolver(repo repository.Repo) *SimpleResolver { return &SimpleResolver{repo: repo} } -func (r *SimpleResolver) Resolve(id entity.Id) (entity.Resolved, error) { +func (r *SimpleResolver) Resolve(id bootstrap.Id) (bootstrap.Resolved, error) { return ReadLocal(r.repo, id) } diff --git a/entities/identity/version.go b/entities/identity/version.go index fe1565cff35276d4d09813732c8f68417c8e8759..2e30ead731573ecf1d40698519b198b89bc5b656 100644 --- a/entities/identity/version.go +++ b/entities/identity/version.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" - "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" "github.com/MichaelMure/git-bug/util/text" @@ -46,7 +46,7 @@ type version struct { metadata map[string]string // Not serialized. Store the version's id in memory. - id entity.Id + id bootstrap.Id // Not serialized commitHash repository.Hash } @@ -63,7 +63,7 @@ func newVersion(repo repository.RepoClock, name string, email string, login stri } return &version{ - id: entity.UnsetId, + id: bootstrap.UnsetId, name: name, email: email, login: login, @@ -91,12 +91,12 @@ type versionJSON struct { } // Id return the identifier of the version -func (v *version) Id() entity.Id { +func (v *version) Id() bootstrap.Id { if v.id == "" { // something went really wrong panic("version's id not set") } - if v.id == entity.UnsetId { + if v.id == bootstrap.UnsetId { // This means we are trying to get the version's Id *before* it has been stored. // As the Id is computed based on the actual bytes written on the disk, we are going to predict // those and then get the Id. This is safe as it will be the exact same code writing on disk later. @@ -104,7 +104,7 @@ func (v *version) Id() entity.Id { if err != nil { panic(err) } - v.id = entity.DeriveId(data) + v.id = bootstrap.DeriveId(data) } return v.id } @@ -116,7 +116,7 @@ func (v *version) Clone() *version { // reset some fields clone.commitHash = "" - clone.id = entity.UnsetId + clone.id = bootstrap.UnsetId clone.times = make(map[string]lamport.Time) for name, t := range v.times { @@ -159,10 +159,10 @@ func (v *version) UnmarshalJSON(data []byte) error { } if aux.FormatVersion != formatVersion { - return entity.NewErrInvalidFormat(aux.FormatVersion, formatVersion) + return bootstrap.NewErrInvalidFormat(aux.FormatVersion, formatVersion) } - v.id = entity.DeriveId(data) + v.id = bootstrap.DeriveId(data) v.times = aux.Times v.unixTime = aux.UnixTime v.name = aux.Name @@ -237,7 +237,7 @@ func (v *version) Write(repo repository.Repo) (repository.Hash, error) { } // make sure we set the Id when writing in the repo - v.id = entity.DeriveId(data) + v.id = bootstrap.DeriveId(data) return hash, nil } diff --git a/entities/identity/version_test.go b/entities/identity/version_test.go index 385ad4d7ca32d2dc355bb6e17559416c854434d4..2ad43f3d41c91858fb8274c3ac6baf6715e6f379 100644 --- a/entities/identity/version_test.go +++ b/entities/identity/version_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -44,7 +44,7 @@ func TestVersionJSON(t *testing.T) { before.SetMetadata("key2", "value2") expected := &version{ - id: entity.UnsetId, + id: bootstrap.UnsetId, name: "name", email: "email", login: "login", diff --git a/entity/interface.go b/entity/boostrap/entity.go similarity index 92% rename from entity/interface.go rename to entity/boostrap/entity.go index 3035ac88d4a4328381ad13d2b449681d242a9f61..f62e2bea5d3c4eede8885b6566a2ae8b52f8ec48 100644 --- a/entity/interface.go +++ b/entity/boostrap/entity.go @@ -1,6 +1,6 @@ -package entity +package bootstrap -type Interface interface { +type Entity interface { // Id return the Entity identifier // // This Id need to be immutable without having to store the entity somewhere (ie, an entity only in memory @@ -9,6 +9,7 @@ type Interface interface { // the root of the entity. // It is acceptable to use such a hash and keep mutating that data as long as Id() is not called. Id() Id + // Validate check if the Entity data is valid Validate() error } diff --git a/entity/boostrap/err.go b/entity/boostrap/err.go new file mode 100644 index 0000000000000000000000000000000000000000..49e9da61ed5b67c15cc8a3a0caa6aef8fa1965f5 --- /dev/null +++ b/entity/boostrap/err.go @@ -0,0 +1,84 @@ +package bootstrap + +import ( + "fmt" + "strings" +) + +// ErrNotFound is to be returned when an entity, item, element is +// not found. +type ErrNotFound struct { + typename string +} + +func NewErrNotFound(typename string) *ErrNotFound { + return &ErrNotFound{typename: typename} +} + +func (e ErrNotFound) Error() string { + return fmt.Sprintf("%s doesn't exist", e.typename) +} + +func IsErrNotFound(err error) bool { + _, ok := err.(*ErrNotFound) + return ok +} + +// ErrMultipleMatch is to be returned when more than one entity, item, element +// is found, where only one was expected. +type ErrMultipleMatch struct { + typename string + Matching []Id +} + +func NewErrMultipleMatch(typename string, matching []Id) *ErrMultipleMatch { + return &ErrMultipleMatch{typename: typename, Matching: matching} +} + +func (e ErrMultipleMatch) Error() string { + matching := make([]string, len(e.Matching)) + + for i, match := range e.Matching { + matching[i] = match.String() + } + + return fmt.Sprintf("Multiple matching %s found:\n%s", + e.typename, + strings.Join(matching, "\n")) +} + +func IsErrMultipleMatch(err error) bool { + _, ok := err.(*ErrMultipleMatch) + return ok +} + +// ErrInvalidFormat is to be returned when reading on-disk data with an unexpected +// format or version. +type ErrInvalidFormat struct { + version uint + expected uint +} + +func NewErrInvalidFormat(version uint, expected uint) *ErrInvalidFormat { + return &ErrInvalidFormat{ + version: version, + expected: expected, + } +} + +func NewErrUnknownFormat(expected uint) *ErrInvalidFormat { + return &ErrInvalidFormat{ + version: 0, + expected: expected, + } +} + +func (e ErrInvalidFormat) Error() string { + if e.version == 0 { + return fmt.Sprintf("unreadable data, you likely have an outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade to format version %v", e.expected) + } + if e.version < e.expected { + return fmt.Sprintf("outdated repository format %v, please use https://github.com/MichaelMure/git-bug-migration to upgrade to format version %v", e.version, e.expected) + } + return fmt.Sprintf("your version of git-bug is too old for this repository (format version %v, expected %v), please upgrade to the latest version", e.version, e.expected) +} diff --git a/entity/boostrap/id.go b/entity/boostrap/id.go new file mode 100644 index 0000000000000000000000000000000000000000..5209a76730927279fad989e86a47b227453923b6 --- /dev/null +++ b/entity/boostrap/id.go @@ -0,0 +1,81 @@ +package bootstrap + +import ( + "crypto/sha256" + "fmt" + "io" + "strings" + + "github.com/pkg/errors" +) + +// sha-256 +const IdLength = 64 +const HumanIdLength = 7 + +const UnsetId = Id("unset") + +// Id is an identifier for an entity or part of an entity +type Id string + +// DeriveId generate an Id from the serialization of the object or part of the object. +func DeriveId(data []byte) Id { + // My understanding is that sha256 is enough to prevent collision (git use that, so ...?) + // If you read this code, I'd be happy to be schooled. + + sum := sha256.Sum256(data) + return Id(fmt.Sprintf("%x", sum)) +} + +// String return the identifier as a string +func (i Id) String() string { + return string(i) +} + +// Human return the identifier, shortened for human consumption +func (i Id) Human() string { + format := fmt.Sprintf("%%.%ds", HumanIdLength) + return fmt.Sprintf(format, i) +} + +func (i Id) HasPrefix(prefix string) bool { + return strings.HasPrefix(string(i), prefix) +} + +// UnmarshalGQL implement the Unmarshaler interface for gqlgen +func (i *Id) UnmarshalGQL(v interface{}) error { + _, ok := v.(string) + if !ok { + return fmt.Errorf("IDs must be strings") + } + + *i = v.(Id) + + if err := i.Validate(); err != nil { + return errors.Wrap(err, "invalid ID") + } + + return nil +} + +// MarshalGQL implement the Marshaler interface for gqlgen +func (i Id) MarshalGQL(w io.Writer) { + _, _ = w.Write([]byte(`"` + i.String() + `"`)) +} + +// Validate tell if the Id is valid +func (i Id) Validate() error { + // Special case to detect outdated repo + if len(i) == 40 { + return fmt.Errorf("outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade") + } + if len(i) != IdLength { + return fmt.Errorf("invalid length") + } + for _, r := range i { + if (r < 'a' || r > 'z') && (r < '0' || r > '9') { + return fmt.Errorf("invalid character") + } + } + return nil +} diff --git a/entity/boostrap/merge.go b/entity/boostrap/merge.go new file mode 100644 index 0000000000000000000000000000000000000000..95861785e000ac6b19e154006e2cb7f75b08f175 --- /dev/null +++ b/entity/boostrap/merge.go @@ -0,0 +1,91 @@ +package bootstrap + +import ( + "fmt" +) + +// MergeStatus represent the result of a merge operation of an entity +type MergeStatus int + +const ( + _ MergeStatus = iota + MergeStatusNew // a new Entity was created locally + MergeStatusInvalid // the remote data is invalid + MergeStatusUpdated // a local Entity has been updated + MergeStatusNothing // no changes were made to a local Entity (already up to date) + MergeStatusError // a terminal error happened +) + +// MergeResult hold the result of a merge operation on an Entity. +type MergeResult struct { + // Err is set when a terminal error occur in the process + Err error + + Id Id + Status MergeStatus + + // Only set for Invalid status + Reason string + + // Only set for New or Updated status + Entity Entity +} + +func (mr MergeResult) String() string { + switch mr.Status { + case MergeStatusNew: + return "new" + case MergeStatusInvalid: + return fmt.Sprintf("invalid data: %s", mr.Reason) + case MergeStatusUpdated: + return "updated" + case MergeStatusNothing: + return "nothing to do" + case MergeStatusError: + if mr.Id != "" { + return fmt.Sprintf("merge error on %s: %s", mr.Id, mr.Err.Error()) + } + return fmt.Sprintf("merge error: %s", mr.Err.Error()) + default: + panic("unknown merge status") + } +} + +func NewMergeNewStatus(id Id, entity Entity) MergeResult { + return MergeResult{ + Id: id, + Status: MergeStatusNew, + Entity: entity, + } +} + +func NewMergeInvalidStatus(id Id, reason string) MergeResult { + return MergeResult{ + Id: id, + Status: MergeStatusInvalid, + Reason: reason, + } +} + +func NewMergeUpdatedStatus(id Id, entity Entity) MergeResult { + return MergeResult{ + Id: id, + Status: MergeStatusUpdated, + Entity: entity, + } +} + +func NewMergeNothingStatus(id Id) MergeResult { + return MergeResult{ + Id: id, + Status: MergeStatusNothing, + } +} + +func NewMergeError(err error, id Id) MergeResult { + return MergeResult{ + Id: id, + Status: MergeStatusError, + Err: err, + } +} diff --git a/entity/boostrap/refs.go b/entity/boostrap/refs.go new file mode 100644 index 0000000000000000000000000000000000000000..daefe956ba922d59033709e0b5b7d91d358e003d --- /dev/null +++ b/entity/boostrap/refs.go @@ -0,0 +1,20 @@ +package bootstrap + +import "strings" + +// RefsToIds parse a slice of git references and return the corresponding Entity's Id. +func RefsToIds(refs []string) []Id { + ids := make([]Id, len(refs)) + + for i, ref := range refs { + ids[i] = RefToId(ref) + } + + return ids +} + +// RefToId parse a git reference and return the corresponding Entity's Id. +func RefToId(ref string) Id { + split := strings.Split(ref, "/") + return Id(split[len(split)-1]) +} diff --git a/entity/boostrap/resolver.go b/entity/boostrap/resolver.go new file mode 100644 index 0000000000000000000000000000000000000000..bd9ca14e6794e02a476beff4ee73dff9978c709a --- /dev/null +++ b/entity/boostrap/resolver.go @@ -0,0 +1,13 @@ +package bootstrap + +// Resolved is a minimal interface on which Resolver operates on. +// Notably, this operates on Entity and Excerpt in the cache. +type Resolved interface { + // Id returns the object identifier. + Id() Id +} + +// Resolver is an interface to find an Entity from its Id +type Resolver interface { + Resolve(id Id) (Resolved, error) +} diff --git a/entity/streamed.go b/entity/boostrap/streamed.go similarity index 59% rename from entity/streamed.go rename to entity/boostrap/streamed.go index 33124ef0fa821e8c03ac198fbd49366e77d1c4cc..96ea98c904529ca136e15d7936672e6d1646426e 100644 --- a/entity/streamed.go +++ b/entity/boostrap/streamed.go @@ -1,6 +1,9 @@ -package entity +package bootstrap -type StreamedEntity[EntityT Interface] struct { +// TODO: type alias not possible on generics for now +// https://github.com/golang/go/issues/46477 + +type StreamedEntity[EntityT Entity] struct { Err error Entity EntityT diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go index 51acfa49150d6e421d4685e7064f9da44adcf42f..a5cc4009bea05a1eaf503f34935a35af79c2c5ef 100644 --- a/entity/dag/common_test.go +++ b/entity/dag/common_test.go @@ -19,7 +19,7 @@ import ( */ const ( - _ OperationType = iota + _ entity.OperationType = iota Op1 Op2 ) @@ -61,7 +61,7 @@ func (op *op2) Validate() error { return nil } func unmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (Operation, error) { var t struct { - OperationType OperationType `json:"type"` + OperationType entity.OperationType `json:"type"` } if err := json.Unmarshal(raw, &t); err != nil { diff --git a/entity/dag/entity.go b/entity/dag/entity.go index f8dbd53df10b29c8d6e1e5dc2b9180058387e090..7c5c4d3c87d4cbc5b0257fd4f181e35b7f496bfd 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -11,6 +11,7 @@ import ( "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -59,7 +60,7 @@ func New(definition Definition) *Entity { } // Read will read and decode a stored local Entity from a repository -func Read[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (EntityT, error) { +func Read[EntityT entity.Bare](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (EntityT, error) { if err := id.Validate(); err != nil { return *new(EntityT), errors.Wrap(err, "invalid id") } @@ -70,7 +71,7 @@ func Read[EntityT entity.Interface](def Definition, wrapper func(e *Entity) Enti } // readRemote will read and decode a stored remote Entity from a repository -func readRemote[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, id entity.Id) (EntityT, error) { +func readRemote[EntityT entity.Bare](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, id entity.Id) (EntityT, error) { if err := id.Validate(); err != nil { return *new(EntityT), errors.Wrap(err, "invalid id") } @@ -81,7 +82,7 @@ func readRemote[EntityT entity.Interface](def Definition, wrapper func(e *Entity } // read fetch from git and decode an Entity at an arbitrary git reference. -func read[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, ref string) (EntityT, error) { +func read[EntityT entity.Bare](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, ref string) (EntityT, error) { rootHash, err := repo.ResolveRef(ref) if err == repository.ErrNotFound { return *new(EntityT), entity.NewErrNotFound(def.Typename) @@ -300,8 +301,8 @@ func readClockNoCheck(def Definition, repo repository.ClockedRepo, ref string) e } // ReadAll read and parse all local Entity -func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan entity.StreamedEntity[EntityT] { - out := make(chan entity.StreamedEntity[EntityT]) +func ReadAll[EntityT entity.Bare](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan bootstrap.StreamedEntity[EntityT] { + out := make(chan bootstrap.StreamedEntity[EntityT]) go func() { defer close(out) @@ -310,7 +311,7 @@ func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) E refs, err := repo.ListRefs(refPrefix) if err != nil { - out <- entity.StreamedEntity[EntityT]{Err: err} + out <- bootstrap.StreamedEntity[EntityT]{Err: err} return } @@ -321,11 +322,11 @@ func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) E e, err := read[EntityT](def, wrapper, repo, resolvers, ref) if err != nil { - out <- entity.StreamedEntity[EntityT]{Err: err} + out <- bootstrap.StreamedEntity[EntityT]{Err: err} return } - out <- entity.StreamedEntity[EntityT]{ + out <- bootstrap.StreamedEntity[EntityT]{ Entity: e, CurrentEntity: current, TotalEntities: total, diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index 5f0abec3d92d70996057ae0c9b38e723446cd3aa..3c3f819f7a8997b0e2de39c078037f6277ca0e9e 100644 --- a/entity/dag/entity_actions.go +++ b/entity/dag/entity_actions.go @@ -32,7 +32,7 @@ func Push(def Definition, repo repository.Repo, remote string) (string, error) { // Pull will do a Fetch + MergeAll // Contrary to MergeAll, this function will return an error if a merge fail. -func Pull[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) error { +func Pull[EntityT entity.Bare](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) error { _, err := Fetch(def, repo, remote) if err != nil { return err @@ -68,7 +68,7 @@ func Pull[EntityT entity.Interface](def Definition, wrapper func(e *Entity) Enti // // Note: an author is necessary for the case where a merge commit is created, as this commit will // have an author and may be signed if a signing key is available. -func MergeAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) <-chan entity.MergeResult { +func MergeAll[EntityT entity.Bare](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) <-chan entity.MergeResult { out := make(chan entity.MergeResult) go func() { @@ -91,7 +91,7 @@ func MergeAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) // merge perform a merge to make sure a local Entity is up-to-date. // See MergeAll for more details. -func merge[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remoteRef string, author identity.Interface) entity.MergeResult { +func merge[EntityT entity.Bare](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remoteRef string, author identity.Interface) entity.MergeResult { id := entity.RefToId(remoteRef) if err := id.Validate(); err != nil { diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go index 6181614bcc5794daab893d7fdf99c0ca999a44d1..811705460f08ae5b49fb8da3fbb1c11ca43a2f40 100644 --- a/entity/dag/entity_actions_test.go +++ b/entity/dag/entity_actions_test.go @@ -8,10 +8,11 @@ import ( "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" ) -func allEntities(t testing.TB, bugs <-chan entity.StreamedEntity[*Foo]) []*Foo { +func allEntities(t testing.TB, bugs <-chan bootstrap.StreamedEntity[*Foo]) []*Foo { t.Helper() var result []*Foo diff --git a/entity/dag/example_test.go b/entity/dag/example_test.go index a263eb2b73f7c1d45a197f07bb5e7eee753c607d..3a12db5fd5ce5a406d90c3a850b440718d7fbfae 100644 --- a/entity/dag/example_test.go +++ b/entity/dag/example_test.go @@ -66,7 +66,7 @@ type Operation interface { } const ( - _ dag.OperationType = iota + _ entity.OperationType = iota SetSignatureRequiredOp AddAdministratorOp RemoveAdministratorOp @@ -220,7 +220,7 @@ var def = dag.Definition{ // concrete Operations. If needed, we can use the resolver to connect to other entities. func operationUnmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (dag.Operation, error) { var t struct { - OperationType dag.OperationType `json:"type"` + OperationType entity.OperationType `json:"type"` } if err := json.Unmarshal(raw, &t); err != nil { diff --git a/entity/dag/op_noop.go b/entity/dag/op_noop.go index c2d896a644d96902b5b9f849d0a901bfbce198f9..d8a2a05aa988435c560636efdc7f6ba825b48ffc 100644 --- a/entity/dag/op_noop.go +++ b/entity/dag/op_noop.go @@ -5,17 +5,17 @@ import ( "github.com/MichaelMure/git-bug/entity" ) -var _ Operation = &NoOpOperation[Snapshot]{} -var _ OperationDoesntChangeSnapshot = &NoOpOperation[Snapshot]{} +var _ Operation = &NoOpOperation[entity.Snapshot]{} +var _ entity.OperationDoesntChangeSnapshot = &NoOpOperation[entity.Snapshot]{} // NoOpOperation is an operation that does not change the entity state. It can // however be used to store arbitrary metadata in the entity history, for example // to support a bridge feature. -type NoOpOperation[SnapT Snapshot] struct { +type NoOpOperation[SnapT entity.Snapshot] struct { OpBase } -func NewNoOpOp[SnapT Snapshot](opType OperationType, author identity.Interface, unixTime int64) *NoOpOperation[SnapT] { +func NewNoOpOp[SnapT entity.Snapshot](opType entity.OperationType, author identity.Interface, unixTime int64) *NoOpOperation[SnapT] { return &NoOpOperation[SnapT]{ OpBase: NewOpBase(opType, author, unixTime), } diff --git a/entity/dag/op_set_metadata.go b/entity/dag/op_set_metadata.go index 4d2d0f8c775800560d29ccdbe2dc898b66ac27d4..191761836d44f38ce2cf661a1f2cb0d57bdba960 100644 --- a/entity/dag/op_set_metadata.go +++ b/entity/dag/op_set_metadata.go @@ -10,16 +10,16 @@ import ( "github.com/MichaelMure/git-bug/util/text" ) -var _ Operation = &SetMetadataOperation[Snapshot]{} -var _ OperationDoesntChangeSnapshot = &SetMetadataOperation[Snapshot]{} +var _ Operation = &SetMetadataOperation[entity.Snapshot]{} +var _ entity.OperationDoesntChangeSnapshot = &SetMetadataOperation[entity.Snapshot]{} -type SetMetadataOperation[SnapT Snapshot] struct { +type SetMetadataOperation[SnapT entity.Snapshot] struct { OpBase Target entity.Id `json:"target"` NewMetadata map[string]string `json:"new_metadata"` } -func NewSetMetadataOp[SnapT Snapshot](opType OperationType, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) *SetMetadataOperation[SnapT] { +func NewSetMetadataOp[SnapT entity.Snapshot](opType entity.OperationType, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) *SetMetadataOperation[SnapT] { return &SetMetadataOperation[SnapT]{ OpBase: NewOpBase(opType, author, unixTime), Target: target, diff --git a/entity/dag/op_set_metadata_test.go b/entity/dag/op_set_metadata_test.go index a06f89daf9fb59f2dac106fa8efa8513a24361f1..591ce9b27b248846d60b185e15021fdef0e3ca5f 100644 --- a/entity/dag/op_set_metadata_test.go +++ b/entity/dag/op_set_metadata_test.go @@ -12,17 +12,17 @@ import ( "github.com/stretchr/testify/require" ) -var _ Snapshot = &snapshotMock{} +var _ entity.Snapshot = &snapshotMock{} type snapshotMock struct { - ops []Operation + ops []entity.Operation } -func (s *snapshotMock) AllOperations() []Operation { +func (s *snapshotMock) AllOperations() []entity.Operation { return s.ops } -func (s *snapshotMock) AppendOperation(op Operation) { +func (s *snapshotMock) AppendOperation(op entity.Operation) { s.ops = append(s.ops, op) } diff --git a/entity/dag/operation.go b/entity/dag/operation.go index f50d91b65d43dfc197b56104bd6ba6f298b58699..40bd7da8d86c9d68107f7575049e8456a7e3a11a 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -10,50 +10,11 @@ import ( "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" ) -// OperationType is an operation type identifier -type OperationType int - -// Operation is a piece of data defining a change to reflect on the state of an Entity. -// What this Operation or Entity's state looks like is not of the resort of this package as it only deals with the -// data structure and storage. +// Operation is an extended interface for an entity.Operation working with the dag package. type Operation interface { - // Id return the Operation identifier - // - // Some care need to be taken to define a correct Id derivation and enough entropy in the data used to avoid - // collisions. Notably: - // - the Id of the first Operation will be used as the Id of the Entity. Collision need to be avoided across entities - // of the same type (example: no collision within the "bug" namespace). - // - collisions can also happen within the set of Operations of an Entity. Simple Operation might not have enough - // entropy to yield unique Ids (example: two "close" operation within the same second, same author). - // If this is a concern, it is recommended to include a piece of random data in the operation's data, to guarantee - // a minimal amount of entropy and avoid collision. - // - // Author's note: I tried to find a clever way around that inelegance (stuffing random useless data into the stored - // structure is not exactly elegant), but I failed to find a proper way. Essentially, anything that would reuse some - // other data (parent operation's Id, lamport clock) or the graph structure (depth) impose that the Id would only - // make sense in the context of the graph and yield some deep coupling between Entity and Operation. This in turn - // make the whole thing even less elegant. - // - // A common way to derive an Id will be to use the entity.DeriveId() function on the serialized operation data. - Id() entity.Id - // Type return the type of the operation - Type() OperationType - // Validate check if the Operation data is valid - Validate() error - // Author returns the author of this operation - Author() identity.Interface - // Time return the time when the operation was added - Time() time.Time - - // SetMetadata store arbitrary metadata about the operation - SetMetadata(key string, value string) - // GetMetadata retrieve arbitrary metadata about the operation - GetMetadata(key string) (string, bool) - // AllMetadata return all metadata for this operation - AllMetadata() map[string]string + entity.Operation // setId allow to set the Id, used when unmarshalling only setId(id entity.Id) @@ -63,37 +24,13 @@ type Operation interface { setExtraMetadataImmutable(key string, value string) } -type OperationWithApply[SnapT Snapshot] interface { +type OperationWithApply[SnapT entity.Snapshot] interface { Operation // Apply the operation to a Snapshot to create the final state Apply(snapshot SnapT) } -// OperationWithFiles is an optional extension for an Operation that has files dependency, stored in git. -type OperationWithFiles interface { - // GetFiles return the files needed by this operation - // This implies that the Operation maintain and store internally the references to those files. This is how - // this information is read later, when loading from storage. - // For example, an operation that has a text value referencing some files would maintain a mapping (text ref --> - // hash). - GetFiles() []repository.Hash -} - -// OperationDoesntChangeSnapshot is an interface signaling that the Operation implementing it doesn't change the -// snapshot, for example a metadata operation that act on other operations. -type OperationDoesntChangeSnapshot interface { - DoesntChangeSnapshot() -} - -// Snapshot is the minimal interface that a snapshot need to implement -type Snapshot interface { - // AllOperations returns all the operations that have been applied to that snapshot, in order - AllOperations() []Operation - // AppendOperation add an operation in the list - AppendOperation(op Operation) -} - // OpBase implement the common feature that every Operation should support. type OpBase struct { // Not serialized. Store the op's id in memory. @@ -101,8 +38,8 @@ type OpBase struct { // Not serialized author identity.Interface - OperationType OperationType `json:"type"` - UnixTime int64 `json:"timestamp"` + OperationType entity.OperationType `json:"type"` + UnixTime int64 `json:"timestamp"` // mandatory random bytes to ensure a better randomness of the data used to later generate the ID // len(Nonce) should be > 20 and < 64 bytes @@ -115,7 +52,7 @@ type OpBase struct { extraMetadata map[string]string } -func NewOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase { +func NewOpBase(opType entity.OperationType, author identity.Interface, unixTime int64) OpBase { return OpBase{ OperationType: opType, author: author, @@ -155,7 +92,7 @@ func IdOperation(op Operation, base *OpBase) entity.Id { return base.id } -func (base *OpBase) Type() OperationType { +func (base *OpBase) Type() entity.OperationType { return base.OperationType } @@ -165,7 +102,7 @@ func (base *OpBase) Time() time.Time { } // Validate check the OpBase for errors -func (base *OpBase) Validate(op Operation, opType OperationType) error { +func (base *OpBase) Validate(op entity.Operation, opType entity.OperationType) error { if base.OperationType == 0 { return fmt.Errorf("operation type unset") } @@ -185,7 +122,7 @@ func (base *OpBase) Validate(op Operation, opType OperationType) error { return errors.Wrap(err, "author") } - if op, ok := op.(OperationWithFiles); ok { + if op, ok := op.(entity.OperationWithFiles); ok { for _, hash := range op.GetFiles() { if !hash.IsValid() { return fmt.Errorf("file with invalid hash %v", hash) diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index c999ff23cc2f4e89079f89704382ebdc74a81b34..85cceb4fa6612821635d4e3e07ca94d1cb971054 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -180,7 +180,7 @@ func (opp *operationPack) makeExtraTree() []repository.TreeEntry { added := make(map[repository.Hash]interface{}) for _, ops := range opp.Operations { - ops, ok := ops.(OperationWithFiles) + ops, ok := ops.(entity.OperationWithFiles) if !ok { continue } diff --git a/entity/dag/operation_pack_test.go b/entity/dag/operation_pack_test.go index bd8e8e03c0512866a970638c0193593782bffe7f..c8ed4c560da690c4524f6d2d7fed31e75883ea17 100644 --- a/entity/dag/operation_pack_test.go +++ b/entity/dag/operation_pack_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/entities/identity" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" ) @@ -107,11 +108,11 @@ func TestOperationPackFiles(t *testing.T) { } require.Equal(t, opp, opp2) - require.ElementsMatch(t, opp2.Operations[0].(OperationWithFiles).GetFiles(), []repository.Hash{ + require.ElementsMatch(t, opp2.Operations[0].(entity.OperationWithFiles).GetFiles(), []repository.Hash{ blobHash1, blobHash2, }) - require.ElementsMatch(t, opp2.Operations[1].(OperationWithFiles).GetFiles(), []repository.Hash{ + require.ElementsMatch(t, opp2.Operations[1].(entity.OperationWithFiles).GetFiles(), []repository.Hash{ blobHash2, }) diff --git a/entity/dag/interface.go b/entity/entity.go similarity index 71% rename from entity/dag/interface.go rename to entity/entity.go index 80abaced0a945a4f69c89301661a1ac2327ad0bd..9f5238584792a8fe754678bf5bb5178d4824a7ab 100644 --- a/entity/dag/interface.go +++ b/entity/entity.go @@ -1,14 +1,17 @@ -package dag +package entity import ( - "github.com/MichaelMure/git-bug/entity" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" ) -// Interface define the extended interface of a dag.Entity +type Bare bootstrap.Entity + +// Interface define the extended interface of an Entity type Interface[SnapT Snapshot, OpT Operation] interface { - entity.Interface + Bare + CompileToSnapshot[SnapT] // Validate checks if the Entity data is valid Validate() error @@ -19,25 +22,12 @@ type Interface[SnapT Snapshot, OpT Operation] interface { // Operations returns the ordered operations Operations() []OpT - // NeedCommit indicates that the in-memory state changed and need to be committed in the repository - NeedCommit() bool - - // Commit writes the staging area in Git and move the operations to the packs - Commit(repo repository.ClockedRepo) error - - // CommitAsNeeded execute a Commit only if necessary. This function is useful to avoid getting an error if the Entity - // is already in sync with the repository. - CommitAsNeeded(repo repository.ClockedRepo) error - // FirstOp lookup for the very first operation of the Entity. FirstOp() OpT - // LastOp lookup for the very last operation of the Entity. - // For a valid Entity, should never be nil - LastOp() OpT - - // Compile an Entity in an easily usable snapshot - Compile() SnapT + // // LastOp lookup for the very last operation of the Entity. + // // For a valid Entity, should never be nil + // LastOp() OpT // CreateLamportTime return the Lamport time of creation CreateLamportTime() lamport.Time @@ -45,3 +35,20 @@ type Interface[SnapT Snapshot, OpT Operation] interface { // EditLamportTime return the Lamport time of the last edit EditLamportTime() lamport.Time } + +type WithCommit[SnapT Snapshot, OpT Operation] interface { + Interface[SnapT, OpT] + Committer +} + +type Committer interface { + // NeedCommit indicates that the in-memory state changed and need to be committed in the repository + NeedCommit() bool + + // Commit writes the staging area in Git and move the operations to the packs + Commit(repo repository.ClockedRepo) error + + // CommitAsNeeded execute a Commit only if necessary. This function is useful to avoid getting an error if the Entity + // is already in sync with the repository. + CommitAsNeeded(repo repository.ClockedRepo) error +} diff --git a/entity/err.go b/entity/err.go index 4453d36ef8224ca6b301135c90a4980ce3213889..2fab8db3f67cc07401d7b832114b8d5d24a68dfa 100644 --- a/entity/err.go +++ b/entity/err.go @@ -1,23 +1,14 @@ package entity import ( - "fmt" - "strings" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" ) // ErrNotFound is to be returned when an entity, item, element is // not found. -type ErrNotFound struct { - typename string -} - -func NewErrNotFound(typename string) *ErrNotFound { - return &ErrNotFound{typename: typename} -} +type ErrNotFound = bootstrap.ErrNotFound -func (e ErrNotFound) Error() string { - return fmt.Sprintf("%s doesn't exist", e.typename) -} +var NewErrNotFound = bootstrap.NewErrNotFound func IsErrNotFound(err error) bool { _, ok := err.(*ErrNotFound) @@ -26,26 +17,9 @@ func IsErrNotFound(err error) bool { // ErrMultipleMatch is to be returned when more than one entity, item, element // is found, where only one was expected. -type ErrMultipleMatch struct { - typename string - Matching []Id -} - -func NewErrMultipleMatch(typename string, matching []Id) *ErrMultipleMatch { - return &ErrMultipleMatch{typename: typename, Matching: matching} -} - -func (e ErrMultipleMatch) Error() string { - matching := make([]string, len(e.Matching)) +type ErrMultipleMatch = bootstrap.ErrMultipleMatch - for i, match := range e.Matching { - matching[i] = match.String() - } - - return fmt.Sprintf("Multiple matching %s found:\n%s", - e.typename, - strings.Join(matching, "\n")) -} +var NewErrMultipleMatch = bootstrap.NewErrMultipleMatch func IsErrMultipleMatch(err error) bool { _, ok := err.(*ErrMultipleMatch) @@ -54,31 +28,8 @@ func IsErrMultipleMatch(err error) bool { // ErrInvalidFormat is to be returned when reading on-disk data with an unexpected // format or version. -type ErrInvalidFormat struct { - version uint - expected uint -} +type ErrInvalidFormat = bootstrap.ErrInvalidFormat -func NewErrInvalidFormat(version uint, expected uint) *ErrInvalidFormat { - return &ErrInvalidFormat{ - version: version, - expected: expected, - } -} +var NewErrInvalidFormat = bootstrap.NewErrInvalidFormat -func NewErrUnknownFormat(expected uint) *ErrInvalidFormat { - return &ErrInvalidFormat{ - version: 0, - expected: expected, - } -} - -func (e ErrInvalidFormat) Error() string { - if e.version == 0 { - return fmt.Sprintf("unreadable data, you likely have an outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade to format version %v", e.expected) - } - if e.version < e.expected { - return fmt.Sprintf("outdated repository format %v, please use https://github.com/MichaelMure/git-bug-migration to upgrade to format version %v", e.version, e.expected) - } - return fmt.Sprintf("your version of git-bug is too old for this repository (format version %v, expected %v), please upgrade to the latest version", e.version, e.expected) -} +var NewErrUnknownFormat = bootstrap.NewErrUnknownFormat diff --git a/entity/id.go b/entity/id.go index 0949bf924a8aa2b110b3e14fd4e809cb747edb8e..d79260b8cf4b4b019fe55a8ffcc5d7e1f03d63b1 100644 --- a/entity/id.go +++ b/entity/id.go @@ -1,81 +1,15 @@ package entity import ( - "crypto/sha256" - "fmt" - "io" - "strings" - - "github.com/pkg/errors" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" ) -// sha-256 -const idLength = 64 -const HumanIdLength = 7 +const HumanIdLength = bootstrap.HumanIdLength -const UnsetId = Id("unset") +const UnsetId = bootstrap.UnsetId // Id is an identifier for an entity or part of an entity -type Id string +type Id = bootstrap.Id // DeriveId generate an Id from the serialization of the object or part of the object. -func DeriveId(data []byte) Id { - // My understanding is that sha256 is enough to prevent collision (git use that, so ...?) - // If you read this code, I'd be happy to be schooled. - - sum := sha256.Sum256(data) - return Id(fmt.Sprintf("%x", sum)) -} - -// String return the identifier as a string -func (i Id) String() string { - return string(i) -} - -// Human return the identifier, shortened for human consumption -func (i Id) Human() string { - format := fmt.Sprintf("%%.%ds", HumanIdLength) - return fmt.Sprintf(format, i) -} - -func (i Id) HasPrefix(prefix string) bool { - return strings.HasPrefix(string(i), prefix) -} - -// UnmarshalGQL implement the Unmarshaler interface for gqlgen -func (i *Id) UnmarshalGQL(v interface{}) error { - _, ok := v.(string) - if !ok { - return fmt.Errorf("IDs must be strings") - } - - *i = v.(Id) - - if err := i.Validate(); err != nil { - return errors.Wrap(err, "invalid ID") - } - - return nil -} - -// MarshalGQL implement the Marshaler interface for gqlgen -func (i Id) MarshalGQL(w io.Writer) { - _, _ = w.Write([]byte(`"` + i.String() + `"`)) -} - -// Validate tell if the Id is valid -func (i Id) Validate() error { - // Special case to detect outdated repo - if len(i) == 40 { - return fmt.Errorf("outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade") - } - if len(i) != idLength { - return fmt.Errorf("invalid length") - } - for _, r := range i { - if (r < 'a' || r > 'z') && (r < '0' || r > '9') { - return fmt.Errorf("invalid character") - } - } - return nil -} +var DeriveId = bootstrap.DeriveId diff --git a/entity/id_interleaved.go b/entity/id_interleaved.go index 7ae6d72e94cd623fe0970b0f09c1817630ce782c..553aee2aec9f8a5cad5193dd184c3c426c8a190e 100644 --- a/entity/id_interleaved.go +++ b/entity/id_interleaved.go @@ -6,6 +6,8 @@ import ( "strings" "github.com/pkg/errors" + + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" ) const UnsetCombinedId = CombinedId("unset") @@ -57,7 +59,7 @@ func (ci CombinedId) Validate() error { if len(ci) == 40 { return fmt.Errorf("outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade") } - if len(ci) != idLength { + if len(ci) != bootstrap.IdLength { return fmt.Errorf("invalid length") } for _, r := range ci { @@ -113,7 +115,7 @@ func (ci CombinedId) SecondaryPrefix() string { func CombineIds(primary Id, secondary Id) CombinedId { var id strings.Builder - for i := 0; i < idLength; i++ { + for i := 0; i < bootstrap.IdLength; i++ { switch { default: id.WriteByte(primary[0]) diff --git a/entity/identity.go b/entity/identity.go new file mode 100644 index 0000000000000000000000000000000000000000..71e0a6e866fc88fb2cec3d409f86d31f757830b4 --- /dev/null +++ b/entity/identity.go @@ -0,0 +1,73 @@ +package entity + +// +// import ( +// "github.com/ProtonMail/go-crypto/openpgp" +// "github.com/ProtonMail/go-crypto/openpgp/packet" +// +// "github.com/MichaelMure/git-bug/repository" +// "github.com/MichaelMure/git-bug/util/lamport" +// "github.com/MichaelMure/git-bug/util/timestamp" +// ) +// +// type Key interface { +// Public() *packet.PublicKey +// Private() *packet.PrivateKey +// Validate() error +// Clone() Key +// PGPEntity() *openpgp.Entity +// } +// +// type Identity interface { +// Bare +// +// // Name return the last version of the name +// // Can be empty. +// Name() string +// +// // DisplayName return a non-empty string to display, representing the +// // identity, based on the non-empty values. +// DisplayName() string +// +// // Email return the last version of the email +// // Can be empty. +// Email() string +// +// // Login return the last version of the login +// // Can be empty. +// // Warning: this login can be defined when importing from a bridge but should *not* be +// // used to identify an identity as multiple bridge with different login can map to the same +// // identity. Use the metadata system for that usage instead. +// Login() string +// +// // AvatarUrl return the last version of the Avatar URL +// // Can be empty. +// AvatarUrl() string +// +// // Keys return the last version of the valid keys +// // Can be empty. +// Keys() []Key +// +// // SigningKey return the key that should be used to sign new messages. If no key is available, return nil. +// SigningKey(repo repository.RepoKeyring) (Key, error) +// +// // ValidKeysAtTime return the set of keys valid at a given lamport time for a given clock of another entity +// // Can be empty. +// ValidKeysAtTime(clockName string, time lamport.Time) []Key +// +// // LastModification return the timestamp at which the last version of the identity became valid. +// LastModification() timestamp.Timestamp +// +// // LastModificationLamports return the lamport times at which the last version of the identity became valid. +// LastModificationLamports() map[string]lamport.Time +// +// // IsProtected return true if the chain of git commits started to be signed. +// // If that's the case, only signed commit with a valid key for this identity can be added. +// IsProtected() bool +// +// // Validate check if the Identity data is valid +// Validate() error +// +// // NeedCommit indicate that the in-memory state changed and need to be committed in the repository +// NeedCommit() bool +// } diff --git a/entity/merge.go b/entity/merge.go index 0661b7fc16112c435cabffa73a1813f55663b3b3..0fee67278cc58300849bea346b4240e3dd6772ce 100644 --- a/entity/merge.go +++ b/entity/merge.go @@ -1,91 +1,29 @@ package entity import ( - "fmt" + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" ) // MergeStatus represent the result of a merge operation of an entity -type MergeStatus int +type MergeStatus = bootstrap.MergeStatus const ( - _ MergeStatus = iota - MergeStatusNew // a new Entity was created locally - MergeStatusInvalid // the remote data is invalid - MergeStatusUpdated // a local Entity has been updated - MergeStatusNothing // no changes were made to a local Entity (already up to date) - MergeStatusError // a terminal error happened + MergeStatusNew = bootstrap.MergeStatusNew // a new Entity was created locally + MergeStatusInvalid = bootstrap.MergeStatusInvalid // the remote data is invalid + MergeStatusUpdated = bootstrap.MergeStatusUpdated // a local Entity has been updated + MergeStatusNothing = bootstrap.MergeStatusNothing // no changes were made to a local Entity (already up to date) + MergeStatusError = bootstrap.MergeStatusError // a terminal error happened ) // MergeResult hold the result of a merge operation on an Entity. -type MergeResult struct { - // Err is set when a terminal error occur in the process - Err error +type MergeResult = bootstrap.MergeResult - Id Id - Status MergeStatus +var NewMergeNewStatus = bootstrap.NewMergeNewStatus - // Only set for Invalid status - Reason string +var NewMergeInvalidStatus = bootstrap.NewMergeInvalidStatus - // Only set for New or Updated status - Entity Interface -} +var NewMergeUpdatedStatus = bootstrap.NewMergeUpdatedStatus -func (mr MergeResult) String() string { - switch mr.Status { - case MergeStatusNew: - return "new" - case MergeStatusInvalid: - return fmt.Sprintf("invalid data: %s", mr.Reason) - case MergeStatusUpdated: - return "updated" - case MergeStatusNothing: - return "nothing to do" - case MergeStatusError: - if mr.Id != "" { - return fmt.Sprintf("merge error on %s: %s", mr.Id, mr.Err.Error()) - } - return fmt.Sprintf("merge error: %s", mr.Err.Error()) - default: - panic("unknown merge status") - } -} +var NewMergeNothingStatus = bootstrap.NewMergeNothingStatus -func NewMergeNewStatus(id Id, entity Interface) MergeResult { - return MergeResult{ - Id: id, - Status: MergeStatusNew, - Entity: entity, - } -} - -func NewMergeInvalidStatus(id Id, reason string) MergeResult { - return MergeResult{ - Id: id, - Status: MergeStatusInvalid, - Reason: reason, - } -} - -func NewMergeUpdatedStatus(id Id, entity Interface) MergeResult { - return MergeResult{ - Id: id, - Status: MergeStatusUpdated, - Entity: entity, - } -} - -func NewMergeNothingStatus(id Id) MergeResult { - return MergeResult{ - Id: id, - Status: MergeStatusNothing, - } -} - -func NewMergeError(err error, id Id) MergeResult { - return MergeResult{ - Id: id, - Status: MergeStatusError, - Err: err, - } -} +var NewMergeError = bootstrap.NewMergeError diff --git a/entity/operations.go b/entity/operations.go new file mode 100644 index 0000000000000000000000000000000000000000..fd88f033cac7ec895c3e0f3db47e4e53ba3abf3c --- /dev/null +++ b/entity/operations.go @@ -0,0 +1,76 @@ +package entity + +import ( + "time" + + "github.com/MichaelMure/git-bug/entities/identity" + "github.com/MichaelMure/git-bug/repository" +) + +// OperationType is an operation type identifier +type OperationType int + +// Operation is a piece of data defining a change to reflect on the state of an Entity. +// What this Operation or Entity's state looks like is not of the resort of this package as it only deals with the +// data structure and storage. +type Operation interface { + // Id return the Operation identifier + // + // Some care need to be taken to define a correct Id derivation and enough entropy in the data used to avoid + // collisions. Notably: + // - the Id of the first Operation will be used as the Id of the Entity. Collision need to be avoided across entities + // of the same type (example: no collision within the "bug" namespace). + // - collisions can also happen within the set of Operations of an Entity. Simple Operation might not have enough + // entropy to yield unique Ids (example: two "close" operation within the same second, same author). + // If this is a concern, it is recommended to include a piece of random data in the operation's data, to guarantee + // a minimal amount of entropy and avoid collision. + // + // Author's note: I tried to find a clever way around that inelegance (stuffing random useless data into the stored + // structure is not exactly elegant), but I failed to find a proper way. Essentially, anything that would reuse some + // other data (parent operation's Id, lamport clock) or the graph structure (depth) impose that the Id would only + // make sense in the context of the graph and yield some deep coupling between Entity and Operation. This in turn + // make the whole thing even less elegant. + // + // A common way to derive an Id will be to use the entity.DeriveId() function on the serialized operation data. + Id() Id + // Type return the type of the operation + Type() OperationType + // Validate check if the Operation data is valid + Validate() error + // Author returns the author of this operation + Author() identity.Interface + // Time return the time when the operation was added + Time() time.Time + + // SetMetadata store arbitrary metadata about the operation + SetMetadata(key string, value string) + // GetMetadata retrieve arbitrary metadata about the operation + GetMetadata(key string) (string, bool) + // AllMetadata return all metadata for this operation + AllMetadata() map[string]string +} + +type OperationWithApply[SnapT Snapshot] interface { + Operation + + // Apply the operation to a Snapshot to create the final state + Apply(snapshot SnapT) +} + +// OperationWithFiles is an optional extension for an Operation that has files dependency, stored in git. +type OperationWithFiles interface { + Operation + + // GetFiles return the files needed by this operation + // This implies that the Operation maintain and store internally the references to those files. This is how + // this information is read later, when loading from storage. + // For example, an operation that has a text value referencing some files would maintain a mapping (text ref --> + // hash). + GetFiles() []repository.Hash +} + +// OperationDoesntChangeSnapshot is an interface signaling that the Operation implementing it doesn't change the +// snapshot, for example a metadata operation that act on other operations. +type OperationDoesntChangeSnapshot interface { + DoesntChangeSnapshot() +} diff --git a/entity/refs.go b/entity/refs.go index 6ba505b96a5d3f3df9a56a7cb5a68c554228f3ce..5b06cb25bded9fdaaa057a8366c647b42f8c8863 100644 --- a/entity/refs.go +++ b/entity/refs.go @@ -1,20 +1,11 @@ package entity -import "strings" +import ( + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" +) // RefsToIds parse a slice of git references and return the corresponding Entity's Id. -func RefsToIds(refs []string) []Id { - ids := make([]Id, len(refs)) - - for i, ref := range refs { - ids[i] = RefToId(ref) - } - - return ids -} +var RefsToIds = bootstrap.RefsToIds // RefToId parse a git reference and return the corresponding Entity's Id. -func RefToId(ref string) Id { - split := strings.Split(ref, "/") - return Id(split[len(split)-1]) -} +var RefToId = bootstrap.RefToId diff --git a/entity/resolver.go b/entity/resolver.go index bd16b901cfc8d8d5519e6244173db87b18ff5e67..ce851af6dad3b2dec48946e945462079240af444 100644 --- a/entity/resolver.go +++ b/entity/resolver.go @@ -3,19 +3,16 @@ package entity import ( "fmt" "sync" + + bootstrap "github.com/MichaelMure/git-bug/entity/boostrap" ) // Resolved is a minimal interface on which Resolver operates on. // Notably, this operates on Entity and Excerpt in the cache. -type Resolved interface { - // Id returns the object identifier. - Id() Id -} +type Resolved = bootstrap.Resolved // Resolver is an interface to find an Entity from its Id -type Resolver interface { - Resolve(id Id) (Resolved, error) -} +type Resolver = bootstrap.Resolver // Resolvers is a collection of Resolver, for different type of Entity type Resolvers map[Resolved]Resolver diff --git a/entity/snapshot.go b/entity/snapshot.go new file mode 100644 index 0000000000000000000000000000000000000000..c6ea43fc7f0adfd0f22bab2f86da66f48641cd72 --- /dev/null +++ b/entity/snapshot.go @@ -0,0 +1,14 @@ +package entity + +// Snapshot is the minimal interface that a snapshot need to implement +type Snapshot interface { + // AllOperations returns all the operations that have been applied to that snapshot, in order + AllOperations() []Operation + // AppendOperation add an operation in the list + AppendOperation(op Operation) +} + +type CompileToSnapshot[SnapT Snapshot] interface { + // Compile an Entity in an easily usable snapshot + Compile() SnapT +} diff --git a/termui/label_select.go b/termui/label_select.go index 6721165e07efd25734c14fe6cd1ac319cddea483..f71fadbdf0713850c252ac1d9d03669397a17981 100644 --- a/termui/label_select.go +++ b/termui/label_select.go @@ -40,7 +40,7 @@ func (ls *labelSelect) SetBug(cache *cache.RepoCache, bug *cache.BugCache) { ls.labels = cache.Bugs().ValidLabels() // Find which labels are currently applied to the bug - bugLabels := bug.Snapshot().Labels + bugLabels := bug.Compile().Labels labelSelect := make([]bool, len(ls.labels)) for i, label := range ls.labels { for _, bugLabel := range bugLabels { @@ -271,7 +271,7 @@ func (ls *labelSelect) abort(g *gocui.Gui, v *gocui.View) error { } func (ls *labelSelect) saveAndReturn(g *gocui.Gui, v *gocui.View) error { - bugLabels := ls.bug.Snapshot().Labels + bugLabels := ls.bug.Compile().Labels var selectedLabels []bug.Label for i, label := range ls.labels { if ls.labelSelect[i] { diff --git a/termui/show_bug.go b/termui/show_bug.go index 8bcae84221d9c8ad6bd872ff4b5564d81ae38f46..701697beb2bfdc07b95c9dc75b72813612f90901 100644 --- a/termui/show_bug.go +++ b/termui/show_bug.go @@ -214,7 +214,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error { y0 -= sb.scroll - snap := sb.bug.Snapshot() + snap := sb.bug.Compile() sb.mainSelectableView = nil @@ -425,7 +425,7 @@ func (sb *showBug) renderSidebar(g *gocui.Gui, sideView *gocui.View) error { x0, y0, _, _, _ := g.ViewPosition(sideView.Name()) maxX += x0 - snap := sb.bug.Snapshot() + snap := sb.bug.Compile() sb.sideSelectableView = nil @@ -624,7 +624,7 @@ func (sb *showBug) setTitle(g *gocui.Gui, v *gocui.View) error { } func (sb *showBug) toggleOpenClose(g *gocui.Gui, v *gocui.View) error { - switch sb.bug.Snapshot().Status { + switch sb.bug.Compile().Status { case common.OpenStatus: _, err := sb.bug.Close() return err @@ -637,7 +637,7 @@ func (sb *showBug) toggleOpenClose(g *gocui.Gui, v *gocui.View) error { } func (sb *showBug) edit(g *gocui.Gui, v *gocui.View) error { - snap := sb.bug.Snapshot() + snap := sb.bug.Compile() if sb.isOnSide { return sb.editLabels(g, snap) diff --git a/termui/termui.go b/termui/termui.go index d640ed1c6641f0d357b0728624970144099d66ca..789876219e4636ae31ca816abd8fc32770fe8854 100644 --- a/termui/termui.go +++ b/termui/termui.go @@ -296,7 +296,7 @@ func setTitleWithEditor(bug *cache.BugCache) error { ui.g.Close() ui.g = nil - snap := bug.Snapshot() + snap := bug.Compile() title, err := buginput.BugTitleEditorInput(ui.cache, snap.Title)