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
}
Michael Muré created
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/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/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(-)
@@ -63,7 +63,7 @@ func (lb *lazyBug) load() error {
return err
}
- lb.snap = b.Snapshot()
+ lb.snap = b.Compile()
return nil
}
@@ -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
}
@@ -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
@@ -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)
@@ -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)
}
@@ -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)
@@ -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
@@ -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)
@@ -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
}
@@ -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 {
@@ -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
@@ -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,
@@ -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()
@@ -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())
@@ -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
@@ -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)
@@ -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.
@@ -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)
},
}
@@ -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),
}
@@ -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()
@@ -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 {
@@ -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)
@@ -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
}
@@ -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")
@@ -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)
@@ -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)
@@ -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 {
@@ -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 {
@@ -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(),
@@ -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)
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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 {
@@ -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)
}
@@ -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())
}
@@ -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 {
@@ -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 {
@@ -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
}
@@ -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)
@@ -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
@@ -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.
@@ -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)
}
@@ -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
}
@@ -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",
@@ -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
}
@@ -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)
+}
@@ -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
+}
@@ -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,
+ }
+}
@@ -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])
+}
@@ -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)
+}
@@ -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
@@ -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 {
@@ -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,
@@ -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 {
@@ -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
@@ -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 {
@@ -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),
}
@@ -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,
@@ -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)
}
@@ -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)
@@ -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
}
@@ -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,
})
@@ -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
+}
@@ -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
@@ -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
@@ -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])
@@ -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
+// }
@@ -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
@@ -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()
+}
@@ -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
@@ -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
@@ -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
+}
@@ -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] {
@@ -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)
@@ -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)