diff options
65 files changed, 615 insertions, 556 deletions
diff --git a/api/auth/context.go b/api/auth/context.go index 17171261..2547aaca 100644 --- a/api/auth/context.go +++ b/api/auth/context.go @@ -24,5 +24,5 @@ func UserFromCtx(ctx context.Context, r *cache.RepoCache) (*cache.IdentityCache, if !ok { return nil, ErrNotAuthenticated } - return r.ResolveIdentity(id) + return r.Identities().Resolve(id) } diff --git a/api/graphql/graphql_test.go b/api/graphql/graphql_test.go index 2ddfb314..e28ce8ab 100644 --- a/api/graphql/graphql_test.go +++ b/api/graphql/graphql_test.go @@ -19,7 +19,7 @@ func TestQueries(t *testing.T) { random_bugs.FillRepoWithSeed(repo, 10, 42) mrc := cache.NewMultiRepoCache() - _, err := mrc.RegisterDefaultRepository(repo) + _, _, err := mrc.RegisterDefaultRepository(repo) require.NoError(t, err) handler := NewHandler(mrc, nil) diff --git a/api/graphql/models/lazy_bug.go b/api/graphql/models/lazy_bug.go index 318fdc99..4b0b598e 100644 --- a/api/graphql/models/lazy_bug.go +++ b/api/graphql/models/lazy_bug.go @@ -58,7 +58,7 @@ func (lb *lazyBug) load() error { return nil } - b, err := lb.cache.ResolveBug(lb.excerpt.Id) + b, err := lb.cache.Bugs().Resolve(lb.excerpt.Id()) if err != nil { return err } @@ -68,7 +68,7 @@ func (lb *lazyBug) load() error { } func (lb *lazyBug) identity(id entity.Id) (IdentityWrapper, error) { - i, err := lb.cache.ResolveIdentityExcerpt(id) + i, err := lb.cache.Identities().ResolveExcerpt(id) if err != nil { return nil, err } @@ -79,7 +79,7 @@ func (lb *lazyBug) identity(id entity.Id) (IdentityWrapper, error) { func (lb *lazyBug) IsAuthored() {} func (lb *lazyBug) Id() entity.Id { - return lb.excerpt.Id + return lb.excerpt.Id() } func (lb *lazyBug) LastEdit() time.Time { diff --git a/api/graphql/models/lazy_identity.go b/api/graphql/models/lazy_identity.go index 27bc9619..c19d077b 100644 --- a/api/graphql/models/lazy_identity.go +++ b/api/graphql/models/lazy_identity.go @@ -48,16 +48,16 @@ func (li *lazyIdentity) load() (*cache.IdentityCache, error) { return li.id, nil } - id, err := li.cache.ResolveIdentity(li.excerpt.Id) + id, err := li.cache.Identities().Resolve(li.excerpt.Id()) if err != nil { - return nil, fmt.Errorf("cache: missing identity %v", li.excerpt.Id) + return nil, fmt.Errorf("cache: missing identity %v", li.excerpt.Id()) } li.id = id return id, nil } func (li *lazyIdentity) Id() entity.Id { - return li.excerpt.Id + return li.excerpt.Id() } func (li *lazyIdentity) Name() string { diff --git a/api/graphql/resolvers/mutation.go b/api/graphql/resolvers/mutation.go index 3f9f7fe1..32e1fa7c 100644 --- a/api/graphql/resolvers/mutation.go +++ b/api/graphql/resolvers/mutation.go @@ -32,7 +32,7 @@ func (r mutationResolver) getBug(repoRef *string, bugPrefix string) (*cache.Repo return nil, nil, err } - b, err := repo.ResolveBugPrefix(bugPrefix) + b, err := repo.Bugs().ResolvePrefix(bugPrefix) if err != nil { return nil, nil, err } @@ -50,7 +50,7 @@ func (r mutationResolver) NewBug(ctx context.Context, input models.NewBugInput) return nil, err } - b, op, err := repo.NewBugRaw(author, + b, op, err := repo.Bugs().NewRaw(author, time.Now().Unix(), text.CleanupOneLine(input.Title), text.Cleanup(input.Message), @@ -181,7 +181,7 @@ func (r mutationResolver) EditComment(ctx context.Context, input models.EditComm return nil, err } - b, target, err := repo.ResolveComment(input.TargetPrefix) + b, target, err := repo.Bugs().ResolveComment(input.TargetPrefix) if err != nil { return nil, err } diff --git a/api/graphql/resolvers/repo.go b/api/graphql/resolvers/repo.go index 3fcaada1..67b03628 100644 --- a/api/graphql/resolvers/repo.go +++ b/api/graphql/resolvers/repo.go @@ -41,7 +41,7 @@ func (repoResolver) AllBugs(_ context.Context, obj *models.Repository, after *st } // Simply pass a []string with the ids to the pagination algorithm - source, err := obj.Repo.QueryBugs(q) + source, err := obj.Repo.Bugs().Query(q) if err != nil { return nil, err } @@ -60,7 +60,7 @@ func (repoResolver) AllBugs(_ context.Context, obj *models.Repository, after *st nodes := make([]models.BugWrapper, len(lazyBugEdges)) for i, lazyBugEdge := range lazyBugEdges { - excerpt, err := obj.Repo.ResolveBugExcerpt(lazyBugEdge.Id) + excerpt, err := obj.Repo.Bugs().ResolveExcerpt(lazyBugEdge.Id) if err != nil { return nil, err } @@ -86,7 +86,7 @@ func (repoResolver) AllBugs(_ context.Context, obj *models.Repository, after *st } func (repoResolver) Bug(_ context.Context, obj *models.Repository, prefix string) (models.BugWrapper, error) { - excerpt, err := obj.Repo.ResolveBugExcerptPrefix(prefix) + excerpt, err := obj.Repo.Bugs().ResolveExcerptPrefix(prefix) if err != nil { return nil, err } @@ -103,7 +103,7 @@ func (repoResolver) AllIdentities(_ context.Context, obj *models.Repository, aft } // Simply pass a []string with the ids to the pagination algorithm - source := obj.Repo.AllIdentityIds() + source := obj.Repo.Identities().AllIds() // The edger create a custom edge holding just the id edger := func(id entity.Id, offset int) connections.Edge { @@ -119,7 +119,7 @@ func (repoResolver) AllIdentities(_ context.Context, obj *models.Repository, aft nodes := make([]models.IdentityWrapper, len(lazyIdentityEdges)) for k, lazyIdentityEdge := range lazyIdentityEdges { - excerpt, err := obj.Repo.ResolveIdentityExcerpt(lazyIdentityEdge.Id) + excerpt, err := obj.Repo.Identities().ResolveExcerpt(lazyIdentityEdge.Id) if err != nil { return nil, err } @@ -145,7 +145,7 @@ func (repoResolver) AllIdentities(_ context.Context, obj *models.Repository, aft } func (repoResolver) Identity(_ context.Context, obj *models.Repository, prefix string) (models.IdentityWrapper, error) { - excerpt, err := obj.Repo.ResolveIdentityExcerptPrefix(prefix) + excerpt, err := obj.Repo.Identities().ResolveExcerptPrefix(prefix) if err != nil { return nil, err } @@ -187,5 +187,5 @@ func (repoResolver) ValidLabels(_ context.Context, obj *models.Repository, after }, nil } - return connections.LabelCon(obj.Repo.ValidLabels(), edger, conMaker, input) + return connections.LabelCon(obj.Repo.Bugs().ValidLabels(), edger, conMaker, input) } diff --git a/api/http/git_file_handlers_test.go b/api/http/git_file_handlers_test.go index 736bf75e..b2371abf 100644 --- a/api/http/git_file_handlers_test.go +++ b/api/http/git_file_handlers_test.go @@ -22,10 +22,10 @@ func TestGitFileHandlers(t *testing.T) { repo := repository.CreateGoGitTestRepo(t, false) mrc := cache.NewMultiRepoCache() - repoCache, err := mrc.RegisterDefaultRepository(repo) + repoCache, _, err := mrc.RegisterDefaultRepository(repo) require.NoError(t, err) - author, err := repoCache.NewIdentity("test identity", "test@test.org") + author, err := repoCache.Identities().New("test identity", "test@test.org") require.NoError(t, err) err = repoCache.SetUserIdentity(author) diff --git a/bridge/core/config.go b/bridge/core/config.go index 8ad7b965..ed079eb8 100644 --- a/bridge/core/config.go +++ b/bridge/core/config.go @@ -10,7 +10,7 @@ import ( func FinishConfig(repo *cache.RepoCache, metaKey string, login string) error { // if no user exist with the given login metadata - _, err := repo.ResolveIdentityImmutableMetadata(metaKey, login) + _, err := repo.Identities().ResolveIdentityImmutableMetadata(metaKey, login) if err != nil && !entity.IsErrNotFound(err) { // real error return err @@ -34,7 +34,7 @@ func FinishConfig(repo *cache.RepoCache, metaKey string, login string) error { } // otherwise create a user with that metadata - i, err := repo.NewIdentityFromGitUserRaw(map[string]string{ + i, err := repo.Identities().NewFromGitUserRaw(map[string]string{ metaKey: login, }) if err != nil { diff --git a/bridge/github/config.go b/bridge/github/config.go index 6b847394..2f5d1f3b 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io/ioutil" - "math/rand" "net/http" "net/url" "regexp" @@ -319,17 +318,6 @@ func pollGithubForAuthorization(deviceCode string, intervalSec int64) (string, e } } -func randomFingerprint() string { - // Doesn't have to be crypto secure, it's just to avoid token collision - rand.Seed(time.Now().UnixNano()) - var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - b := make([]rune, 32) - for i := range b { - b[i] = letterRunes[rand.Intn(len(letterRunes))] - } - return string(b) -} - func promptTokenOptions(repo repository.RepoKeyring, login, owner, project string) (auth.Credential, error) { creds, err := auth.List(repo, auth.WithTarget(target), diff --git a/bridge/github/export.go b/bridge/github/export.go index 5d1d8661..0d340b49 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -20,7 +20,6 @@ import ( "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entities/bug" "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" ) @@ -89,7 +88,7 @@ func (ge *githubExporter) cacheAllClient(repo *cache.RepoCache) error { continue } - user, err := repo.ResolveIdentityImmutableMetadata(metaKeyGithubLogin, login) + user, err := repo.Identities().ResolveIdentityImmutableMetadata(metaKeyGithubLogin, login) if entity.IsErrNotFound(err) { continue } @@ -160,10 +159,10 @@ func (ge *githubExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, allIdentitiesIds = append(allIdentitiesIds, id) } - allBugsIds := repo.AllBugsIds() + allBugsIds := repo.Bugs().AllIds() for _, id := range allBugsIds { - b, err := repo.ResolveBug(id) + b, err := repo.Bugs().Resolve(id) if err != nil { out <- core.NewExportError(errors.Wrap(err, "can't load bug"), id) return diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index 2ebe9622..a2dbf7de 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -34,18 +34,18 @@ type testCase struct { func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { // simple bug - simpleBug, _, err := repo.NewBug("simple bug", "new bug") + simpleBug, _, err := repo.Bugs().New("simple bug", "new bug") require.NoError(t, err) // bug with comments - bugWithComments, _, err := repo.NewBug("bug with comments", "new bug") + bugWithComments, _, err := repo.Bugs().New("bug with comments", "new bug") require.NoError(t, err) _, _, err = bugWithComments.AddComment("new comment") require.NoError(t, err) // bug with label changes - bugLabelChange, _, err := repo.NewBug("bug label change", "new bug") + bugLabelChange, _, err := repo.Bugs().New("bug label change", "new bug") require.NoError(t, err) _, _, err = bugLabelChange.ChangeLabels([]string{"bug"}, nil) @@ -64,7 +64,7 @@ func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { require.NoError(t, err) // bug with comments editions - bugWithCommentEditions, createOp, err := repo.NewBug("bug with comments editions", "new bug") + bugWithCommentEditions, createOp, err := repo.Bugs().New("bug with comments editions", "new bug") require.NoError(t, err) _, err = bugWithCommentEditions.EditComment( @@ -78,7 +78,7 @@ func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { require.NoError(t, err) // bug status changed - bugStatusChanged, _, err := repo.NewBug("bug status changed", "new bug") + bugStatusChanged, _, err := repo.Bugs().New("bug status changed", "new bug") require.NoError(t, err) _, err = bugStatusChanged.Close() @@ -88,7 +88,7 @@ func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { require.NoError(t, err) // bug title changed - bugTitleEdited, _, err := repo.NewBug("bug title edited", "new bug") + bugTitleEdited, _, err := repo.Bugs().New("bug title edited", "new bug") require.NoError(t, err) _, err = bugTitleEdited.SetTitle("bug title edited again") @@ -141,12 +141,15 @@ func TestGithubPushPull(t *testing.T) { // create repo backend repo := repository.CreateGoGitTestRepo(t, false) - backend, err := cache.NewRepoCache(repo) + backend, events, err := cache.NewRepoCache(repo) require.NoError(t, err) + for event := range events { + require.NoError(t, event.Err) + } // set author identity login := "identity-test" - author, err := backend.NewIdentity("test identity", "test@test.org") + author, err := backend.Identities().New("test identity", "test@test.org") require.NoError(t, err) author.SetMetadata(metaKeyGithubLogin, login) err = author.Commit() @@ -217,8 +220,11 @@ func TestGithubPushPull(t *testing.T) { repoTwo := repository.CreateGoGitTestRepo(t, false) // create a second backend - backendTwo, err := cache.NewRepoCache(repoTwo) + backendTwo, events, err := cache.NewRepoCache(repoTwo) require.NoError(t, err) + for event := range events { + require.NoError(t, event.Err) + } importer := &githubImporter{} err = importer.Init(ctx, backend, core.Configuration{ @@ -236,7 +242,7 @@ func TestGithubPushPull(t *testing.T) { require.NoError(t, result.Err) } - require.Len(t, backendTwo.AllBugsIds(), len(tests)) + require.Len(t, backendTwo.Bugs().AllIds(), len(tests)) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -261,7 +267,7 @@ func TestGithubPushPull(t *testing.T) { require.True(t, ok) // retrieve bug from backendTwo - importedBug, err := backendTwo.ResolveBugCreateMetadata(metaKeyGithubId, bugGithubID) + importedBug, err := backendTwo.Bugs().ResolveBugCreateMetadata(metaKeyGithubId, bugGithubID) require.NoError(t, err) // verify bug have same number of original operations diff --git a/bridge/github/import.go b/bridge/github/import.go index a64b7b27..4a51d117 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -10,7 +10,6 @@ import ( "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/util/text" ) @@ -183,7 +182,7 @@ func (gi *githubImporter) ensureIssue(ctx context.Context, repo *cache.RepoCache } // resolve bug - b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool { + b, err := repo.Bugs().ResolveMatcher(func(excerpt *cache.BugExcerpt) bool { return excerpt.CreateMetadata[metaKeyGithubUrl] == issue.Url.String() && excerpt.CreateMetadata[metaKeyGithubId] == parseId(issue.Id) }) @@ -213,7 +212,7 @@ func (gi *githubImporter) ensureIssue(ctx context.Context, repo *cache.RepoCache } // create bug - b, _, err = repo.NewBugRaw( + b, _, err = repo.Bugs().NewRaw( author, issue.CreatedAt.Unix(), text.CleanupOneLine(title), // TODO: this is the *current* title, not the original one @@ -498,7 +497,7 @@ func (gi *githubImporter) ensurePerson(ctx context.Context, repo *cache.RepoCach } // Look first in the cache - i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGithubLogin, string(actor.Login)) + i, err := repo.Identities().ResolveIdentityImmutableMetadata(metaKeyGithubLogin, string(actor.Login)) if err == nil { return i, nil } @@ -531,7 +530,7 @@ func (gi *githubImporter) ensurePerson(ctx context.Context, repo *cache.RepoCach name = string(actor.Login) } - i, err = repo.NewIdentityRaw( + i, err = repo.Identities().NewRaw( name, email, string(actor.Login), @@ -553,7 +552,7 @@ func (gi *githubImporter) ensurePerson(ctx context.Context, repo *cache.RepoCach func (gi *githubImporter) getGhost(ctx context.Context, repo *cache.RepoCache) (*cache.IdentityCache, error) { loginName := "ghost" // Look first in the cache - i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGithubLogin, loginName) + i, err := repo.Identities().ResolveIdentityImmutableMetadata(metaKeyGithubLogin, loginName) if err == nil { return i, nil } @@ -568,7 +567,7 @@ func (gi *githubImporter) getGhost(ctx context.Context, repo *cache.RepoCache) ( if user.Name != nil { userName = string(*user.Name) } - return repo.NewIdentityRaw( + return repo.Identities().NewRaw( userName, "", string(user.Login), diff --git a/bridge/github/import_integration_test.go b/bridge/github/import_integration_test.go index 50cbd5c8..6a41517c 100644 --- a/bridge/github/import_integration_test.go +++ b/bridge/github/import_integration_test.go @@ -34,8 +34,11 @@ func TestGithubImporterIntegration(t *testing.T) { // arrange repo := repository.CreateGoGitTestRepo(t, false) - backend, err := cache.NewRepoCache(repo) + backend, buildEvents, err := cache.NewRepoCache(repo) require.NoError(t, err) + for event := range buildEvents { + require.NoError(t, event.Err) + } defer backend.Close() interrupt.RegisterCleaner(backend.Close) require.NoError(t, err) @@ -48,17 +51,17 @@ func TestGithubImporterIntegration(t *testing.T) { for e := range events { require.NoError(t, e.Err) } - require.Len(t, backend.AllBugsIds(), 5) - require.Len(t, backend.AllIdentityIds(), 2) + require.Len(t, backend.Bugs().AllIds(), 5) + require.Len(t, backend.Identities().AllIds(), 2) - b1, err := backend.ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/1") + b1, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/1") require.NoError(t, err) ops1 := b1.Snapshot().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.ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/3") + b3, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/3") require.NoError(t, err) ops3 := b3.Snapshot().Operations require.Equal(t, "issue 3 comment 1", ops3[1].(*bug.AddCommentOperation).Message) @@ -66,7 +69,7 @@ func TestGithubImporterIntegration(t *testing.T) { require.Equal(t, []bug.Label{"bug"}, ops3[3].(*bug.LabelChangeOperation).Added) require.Equal(t, "title 3, edit 1", ops3[4].(*bug.SetTitleOperation).Title) - b4, err := backend.ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/4") + b4, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGithubUrl, "https://github.com/marcus/to-himself/issues/4") require.NoError(t, err) ops4 := b4.Snapshot().Operations require.Equal(t, "edited", ops4[1].(*bug.EditCommentOperation).Message) diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index 5575de98..6539a52f 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -28,8 +28,11 @@ func TestGithubImporter(t *testing.T) { repo := repository.CreateGoGitTestRepo(t, false) - backend, err := cache.NewRepoCache(repo) + backend, buildEvents, err := cache.NewRepoCache(repo) require.NoError(t, err) + for event := range buildEvents { + require.NoError(t, event.Err) + } defer backend.Close() interrupt.RegisterCleaner(backend.Close) @@ -171,11 +174,11 @@ func TestGithubImporter(t *testing.T) { fmt.Printf("test repository imported in %f seconds\n", time.Since(start).Seconds()) - require.Len(t, backend.AllBugsIds(), len(tests)) + require.Len(t, backend.Bugs().AllIds(), len(tests)) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b, err := backend.ResolveBugCreateMetadata(metaKeyGithubUrl, tt.url) + b, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGithubUrl, tt.url) require.NoError(t, err) ops := b.Snapshot().Operations diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go index 19b8d496..b3a02447 100644 --- a/bridge/gitlab/export.go +++ b/bridge/gitlab/export.go @@ -15,7 +15,6 @@ import ( "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entities/bug" "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" ) @@ -74,7 +73,7 @@ func (ge *gitlabExporter) cacheAllClient(repo *cache.RepoCache, baseURL string) continue } - user, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabLogin, login) + user, err := repo.Identities().ResolveIdentityImmutableMetadata(metaKeyGitlabLogin, login) if entity.IsErrNotFound(err) { continue } @@ -116,14 +115,14 @@ func (ge *gitlabExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, allIdentitiesIds = append(allIdentitiesIds, id) } - allBugsIds := repo.AllBugsIds() + allBugsIds := repo.Bugs().AllIds() for _, id := range allBugsIds { select { case <-ctx.Done(): return default: - b, err := repo.ResolveBug(id) + b, err := repo.Bugs().Resolve(id) if err != nil { out <- core.NewExportError(err, id) return diff --git a/bridge/gitlab/export_test.go b/bridge/gitlab/export_test.go index 47d5a9b1..64bc43f4 100644 --- a/bridge/gitlab/export_test.go +++ b/bridge/gitlab/export_test.go @@ -37,18 +37,18 @@ type testCase struct { func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { // simple bug - simpleBug, _, err := repo.NewBug("simple bug", "new bug") + simpleBug, _, err := repo.Bugs().New("simple bug", "new bug") require.NoError(t, err) // bug with comments - bugWithComments, _, err := repo.NewBug("bug with comments", "new bug") + bugWithComments, _, err := repo.Bugs().New("bug with comments", "new bug") require.NoError(t, err) _, _, err = bugWithComments.AddComment("new comment") require.NoError(t, err) // bug with label changes - bugLabelChange, _, err := repo.NewBug("bug label change", "new bug") + bugLabelChange, _, err := repo.Bugs().New("bug label change", "new bug") require.NoError(t, err) _, _, err = bugLabelChange.ChangeLabels([]string{"bug"}, nil) @@ -61,7 +61,7 @@ func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { require.NoError(t, err) // bug with comments editions - bugWithCommentEditions, createOp, err := repo.NewBug("bug with comments editions", "new bug") + bugWithCommentEditions, createOp, err := repo.Bugs().New("bug with comments editions", "new bug") require.NoError(t, err) _, err = bugWithCommentEditions.EditComment( @@ -75,7 +75,7 @@ func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { require.NoError(t, err) // bug status changed - bugStatusChanged, _, err := repo.NewBug("bug status changed", "new bug") + bugStatusChanged, _, err := repo.Bugs().New("bug status changed", "new bug") require.NoError(t, err) _, err = bugStatusChanged.Close() @@ -85,7 +85,7 @@ func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { require.NoError(t, err) // bug title changed - bugTitleEdited, _, err := repo.NewBug("bug title edited", "new bug") + bugTitleEdited, _, err := repo.Bugs().New("bug title edited", "new bug") require.NoError(t, err) _, err = bugTitleEdited.SetTitle("bug title edited again") @@ -147,12 +147,15 @@ func TestGitlabPushPull(t *testing.T) { // create repo backend repo := repository.CreateGoGitTestRepo(t, false) - backend, err := cache.NewRepoCache(repo) + backend, events, err := cache.NewRepoCache(repo) require.NoError(t, err) + for event := range events { + require.NoError(t, event.Err) + } // set author identity login := "test-identity" - author, err := backend.NewIdentity("test identity", "test@test.org") + author, err := backend.Identities().New("test identity", "test@test.org") require.NoError(t, err) author.SetMetadata(metaKeyGitlabLogin, login) err = author.Commit() @@ -220,8 +223,11 @@ func TestGitlabPushPull(t *testing.T) { repoTwo := repository.CreateGoGitTestRepo(t, false) // create a second backend - backendTwo, err := cache.NewRepoCache(repoTwo) + backendTwo, events, err := cache.NewRepoCache(repoTwo) require.NoError(t, err) + for event := range events { + require.NoError(t, event.Err) + } importer := &gitlabImporter{} err = importer.Init(ctx, backend, core.Configuration{ @@ -239,7 +245,7 @@ func TestGitlabPushPull(t *testing.T) { require.NoError(t, result.Err) } - require.Len(t, backendTwo.AllBugsIds(), len(tests)) + require.Len(t, backendTwo.Bugs().AllIds(), len(tests)) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -264,7 +270,7 @@ func TestGitlabPushPull(t *testing.T) { require.True(t, ok) // retrieve bug from backendTwo - importedBug, err := backendTwo.ResolveBugCreateMetadata(metaKeyGitlabId, bugGitlabID) + importedBug, err := backendTwo.Bugs().ResolveBugCreateMetadata(metaKeyGitlabId, bugGitlabID) require.NoError(t, err) // verify bug have same number of original operations diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go index 85999e90..5947fb60 100644 --- a/bridge/gitlab/import.go +++ b/bridge/gitlab/import.go @@ -11,7 +11,6 @@ import ( "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/util/text" ) @@ -109,7 +108,7 @@ func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue } // resolve bug - b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool { + b, err := repo.Bugs().ResolveMatcher(func(excerpt *cache.BugExcerpt) bool { return excerpt.CreateMetadata[core.MetaKeyOrigin] == target && excerpt.CreateMetadata[metaKeyGitlabId] == fmt.Sprintf("%d", issue.IID) && excerpt.CreateMetadata[metaKeyGitlabBaseUrl] == gi.conf[confKeyGitlabBaseUrl] && @@ -123,7 +122,7 @@ func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue } // if bug was never imported, create bug - b, _, err = repo.NewBugRaw( + b, _, err = repo.Bugs().NewRaw( author, issue.CreatedAt.Unix(), text.CleanupOneLine(issue.Title), @@ -338,7 +337,7 @@ func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCa func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) { // Look first in the cache - i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id)) + i, err := repo.Identities().ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id)) if err == nil { return i, nil } @@ -351,7 +350,7 @@ func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.Id return nil, err } - i, err = repo.NewIdentityRaw( + i, err = repo.Identities().NewRaw( user.Name, user.PublicEmail, user.Username, diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go index d98da4ef..ac91610d 100644 --- a/bridge/gitlab/import_test.go +++ b/bridge/gitlab/import_test.go @@ -33,8 +33,11 @@ func TestGitlabImport(t *testing.T) { repo := repository.CreateGoGitTestRepo(t, false) - backend, err := cache.NewRepoCache(repo) + backend, buildEvents, err := cache.NewRepoCache(repo) require.NoError(t, err) + for event := range buildEvents { + require.NoError(t, event.Err) + } defer backend.Close() interrupt.RegisterCleaner(backend.Close) @@ -126,11 +129,11 @@ func TestGitlabImport(t *testing.T) { fmt.Printf("test repository imported in %f seconds\n", time.Since(start).Seconds()) - require.Len(t, backend.AllBugsIds(), len(tests)) + require.Len(t, backend.Bugs().AllIds(), len(tests)) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b, err := backend.ResolveBugCreateMetadata(metaKeyGitlabUrl, tt.url) + b, err := backend.Bugs().ResolveBugCreateMetadata(metaKeyGitlabUrl, tt.url) require.NoError(t, err) ops := b.Snapshot().Operations diff --git a/bridge/jira/export.go b/bridge/jira/export.go index 10b6823d..95f9e28c 100644 --- a/bridge/jira/export.go +++ b/bridge/jira/export.go @@ -14,7 +14,6 @@ import ( "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/cache" "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" ) @@ -102,7 +101,7 @@ func (je *jiraExporter) cacheAllClient(ctx context.Context, repo *cache.RepoCach continue } - user, err := repo.ResolveIdentityImmutableMetadata(metaKeyJiraLogin, login) + user, err := repo.Identities().ResolveIdentityImmutableMetadata(metaKeyJiraLogin, login) if entity.IsErrNotFound(err) { continue } @@ -146,10 +145,10 @@ func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, si allIdentitiesIds = append(allIdentitiesIds, id) } - allBugsIds := repo.AllBugsIds() + allBugsIds := repo.Bugs().AllIds() for _, id := range allBugsIds { - b, err := repo.ResolveBug(id) + b, err := repo.Bugs().Resolve(id) if err != nil { out <- core.NewExportError(errors.Wrap(err, "can't load bug"), id) return diff --git a/bridge/jira/import.go b/bridge/jira/import.go index 4cec1133..d8a5f8dd 100644 --- a/bridge/jira/import.go +++ b/bridge/jira/import.go @@ -184,7 +184,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si // Create a bug.Person from a JIRA user func (ji *jiraImporter) ensurePerson(repo *cache.RepoCache, user User) (*cache.IdentityCache, error) { // Look first in the cache - i, err := repo.ResolveIdentityImmutableMetadata( + i, err := repo.Identities().ResolveIdentityImmutableMetadata( metaKeyJiraUser, string(user.Key)) if err == nil { return i, nil @@ -193,7 +193,7 @@ func (ji *jiraImporter) ensurePerson(repo *cache.RepoCache, user User) (*cache.I return nil, err } - i, err = repo.NewIdentityRaw( + i, err = repo.Identities().NewRaw( user.DisplayName, user.EmailAddress, user.Key, @@ -219,7 +219,7 @@ func (ji *jiraImporter) ensureIssue(repo *cache.RepoCache, issue Issue) (*cache. return nil, err } - b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool { + b, err := repo.Bugs().ResolveMatcher(func(excerpt *cache.BugExcerpt) bool { if _, ok := excerpt.CreateMetadata[metaKeyJiraBaseUrl]; ok && excerpt.CreateMetadata[metaKeyJiraBaseUrl] != ji.conf[confKeyBaseUrl] { return false @@ -234,7 +234,7 @@ func (ji *jiraImporter) ensureIssue(repo *cache.RepoCache, issue Issue) (*cache. } if entity.IsErrNotFound(err) { - b, _, err = repo.NewBugRaw( + b, _, err = repo.Bugs().NewRaw( author, issue.Fields.Created.Unix(), text.CleanupOneLine(issue.Fields.Summary), diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go index 0f8ecf9f..6a20217c 100644 --- a/bridge/launchpad/import.go +++ b/bridge/launchpad/import.go @@ -7,7 +7,6 @@ import ( "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/util/text" ) @@ -23,7 +22,7 @@ func (li *launchpadImporter) Init(_ context.Context, repo *cache.RepoCache, conf func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson) (*cache.IdentityCache, error) { // Look first in the cache - i, err := repo.ResolveIdentityImmutableMetadata(metaKeyLaunchpadLogin, owner.Login) + i, err := repo.Identities().ResolveIdentityImmutableMetadata(metaKeyLaunchpadLogin, owner.Login) if err == nil { return i, nil } @@ -31,7 +30,7 @@ func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson) return nil, err } - return repo.NewIdentityRaw( + return repo.Identities().NewRaw( owner.Name, "", owner.Login, @@ -64,7 +63,7 @@ func (li *launchpadImporter) ImportAll(ctx context.Context, repo *cache.RepoCach return default: lpBugID := fmt.Sprintf("%d", lpBug.ID) - b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool { + b, err := repo.Bugs().ResolveMatcher(func(excerpt *cache.BugExcerpt) bool { return excerpt.CreateMetadata[core.MetaKeyOrigin] == target && excerpt.CreateMetadata[metaKeyLaunchpadID] == lpBugID }) @@ -81,7 +80,7 @@ func (li *launchpadImporter) ImportAll(ctx context.Context, repo *cache.RepoCach if entity.IsErrNotFound(err) { createdAt, _ := time.Parse(time.RFC3339, lpBug.CreatedAt) - b, _, err = repo.NewBugRaw( + b, _, err = repo.Bugs().NewRaw( owner, createdAt.Unix(), text.CleanupOneLine(lpBug.Title), diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index b4748fd2..26b7ec74 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -15,6 +15,8 @@ func init() { gob.Register(BugExcerpt{}) } +var _ Excerpt = &BugExcerpt{} + // BugExcerpt hold a subset of the bug values to be able to sort and filter bugs // efficiently without having to read and compile each raw bugs. type BugExcerpt struct { @@ -36,7 +38,8 @@ type BugExcerpt struct { CreateMetadata map[string]string } -func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { +func NewBugExcerpt(b *BugCache) *BugExcerpt { + snap := b.Snapshot() participantsIds := make([]entity.Id, 0, len(snap.Participants)) for _, participant := range snap.Participants { participantsIds = append(participantsIds, participant.Id()) @@ -66,6 +69,10 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { return e } +func (b *BugExcerpt) setId(id entity.Id) { + b.id = id +} + func (b *BugExcerpt) Id() entity.Id { return b.id } diff --git a/cache/bug_subcache.go b/cache/bug_subcache.go index f0a8889b..14b56cdc 100644 --- a/cache/bug_subcache.go +++ b/cache/bug_subcache.go @@ -24,11 +24,7 @@ func NewRepoCacheBug(repo repository.ClockedRepo, return NewBugCache(b, repo, getUserIdentity, entityUpdated) } - makeExcerpt := func(b *bug.Bug) *BugExcerpt { - return NewBugExcerpt(b, b.Compile()) - } - - makeIndex := func(b *BugCache) []string { + makeIndexData := func(b *BugCache) []string { snap := b.Snapshot() var res []string for _, comment := range snap.Comments { @@ -38,9 +34,16 @@ func NewRepoCacheBug(repo repository.ClockedRepo, return res } + actions := Actions[*bug.Bug]{ + ReadWithResolver: bug.ReadWithResolver, + ReadAllWithResolver: bug.ReadAllWithResolver, + Remove: bug.Remove, + MergeAll: bug.MergeAll, + } + sc := NewSubCache[*bug.Bug, *BugExcerpt, *BugCache]( repo, resolvers, getUserIdentity, - makeCached, makeExcerpt, makeIndex, + makeCached, NewBugExcerpt, makeIndexData, actions, "bug", "bugs", formatVersion, defaultMaxLoadedBugs, ) @@ -104,8 +107,8 @@ func (c *RepoCacheBug) ResolveComment(prefix string) (*BugCache, entity.Combined return matchingBug, matchingCommentId, nil } -// QueryBugs return the id of all Bug matching the given Query -func (c *RepoCacheBug) QueryBugs(q *query.Query) ([]entity.Id, error) { +// Query return the id of all Bug matching the given Query +func (c *RepoCacheBug) Query(q *query.Query) ([]entity.Id, error) { c.mu.RLock() defer c.mu.RUnlock() @@ -140,7 +143,7 @@ func (c *RepoCacheBug) QueryBugs(q *query.Query) ([]entity.Id, error) { } for _, excerpt := range foundBySearch { - if matcher.Match(excerpt, c) { + if matcher.Match(excerpt, c.resolvers()) { filtered = append(filtered, excerpt) } } diff --git a/cache/cached.go b/cache/cached.go index 74757356..5a31ee59 100644 --- a/cache/cached.go +++ b/cache/cached.go @@ -3,10 +3,10 @@ package cache import ( "sync" - "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/lamport" ) // type withSnapshot[SnapT dag.Snapshot, OpT dag.OperationWithApply[SnapT]] struct { @@ -97,7 +97,7 @@ func (e *CachedEntityBase[SnapT, OpT]) ResolveOperationWithMetadata(key string, } if len(matching) > 1 { - return "", bug.NewErrMultipleMatchOp(matching) + return "", entity.NewErrMultipleMatch("operation", matching) } return matching[0], nil @@ -136,3 +136,15 @@ func (e *CachedEntityBase[SnapT, OpT]) NeedCommit() bool { defer e.mu.RUnlock() return e.entity.NeedCommit() } + +func (e *CachedEntityBase[SnapT, OpT]) CreateLamportTime() lamport.Time { + return e.entity.CreateLamportTime() +} + +func (e *CachedEntityBase[SnapT, OpT]) EditLamportTime() lamport.Time { + return e.entity.EditLamportTime() +} + +func (e *CachedEntityBase[SnapT, OpT]) FirstOp() OpT { + return e.entity.FirstOp() +} diff --git a/cache/filter.go b/cache/filter.go index 01f635c5..5a15e402 100644 --- a/cache/filter.go +++ b/cache/filter.go @@ -8,28 +8,22 @@ import ( "github.com/MichaelMure/git-bug/query" ) -// resolver has the resolving functions needed by filters. -// This exists mainly to go through the functions of the cache with proper locking. -type resolver interface { - ResolveIdentityExcerpt(id entity.Id) (*IdentityExcerpt, error) -} - // Filter is a predicate that match a subset of bugs -type Filter func(excerpt *BugExcerpt, resolver resolver) bool +type Filter func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool // StatusFilter return a Filter that match a bug status func StatusFilter(status common.Status) Filter { - return func(excerpt *BugExcerpt, resolver resolver) bool { + return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool { return excerpt.Status == status } } // AuthorFilter return a Filter that match a bug author func AuthorFilter(query string) Filter { - return func(excerpt *BugExcerpt, resolver resolver) bool { + return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool { query = strings.ToLower(query) - author, err := resolver.ResolveIdentityExcerpt(excerpt.AuthorId) + author, err := entity.Resolve[*IdentityExcerpt](resolvers, excerpt.AuthorId) if err != nil { panic(err) } @@ -40,7 +34,7 @@ func AuthorFilter(query string) Filter { // MetadataFilter return a Filter that match a bug metadata at creation time func MetadataFilter(pair query.StringPair) Filter { - return func(excerpt *BugExcerpt, resolver resolver) bool { + return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool { if value, ok := excerpt.CreateMetadata[pair.Key]; ok { return value == pair.Value } @@ -50,7 +44,7 @@ func MetadataFilter(pair query.StringPair) Filter { // LabelFilter return a Filter that match a label func LabelFilter(label string) Filter { - return func(excerpt *BugExcerpt, resolver resolver) bool { + return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool { for _, l := range excerpt.Labels { if string(l) == label { return true @@ -62,11 +56,11 @@ func LabelFilter(label string) Filter { // ActorFilter return a Filter that match a bug actor func ActorFilter(query string) Filter { - return func(excerpt *BugExcerpt, resolver resolver) bool { + return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool { query = strings.ToLower(query) for _, id := range excerpt.Actors { - identityExcerpt, err := resolver.ResolveIdentityExcerpt(id) + identityExcerpt, err := entity.Resolve[*IdentityExcerpt](resolvers, id) if err != nil { panic(err) } @@ -81,11 +75,11 @@ func ActorFilter(query string) Filter { // ParticipantFilter return a Filter that match a bug participant func ParticipantFilter(query string) Filter { - return func(excerpt *BugExcerpt, resolver resolver) bool { + return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool { query = strings.ToLower(query) for _, id := range excerpt.Participants { - identityExcerpt, err := resolver.ResolveIdentityExcerpt(id) + identityExcerpt, err := entity.Resolve[*IdentityExcerpt](resolvers, id) if err != nil { panic(err) } @@ -100,7 +94,7 @@ func ParticipantFilter(query string) Filter { // TitleFilter return a Filter that match if the title contains the given query func TitleFilter(query string) Filter { - return func(excerpt *BugExcerpt, resolver resolver) bool { + return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool { return strings.Contains( strings.ToLower(excerpt.Title), strings.ToLower(query), @@ -110,7 +104,7 @@ func TitleFilter(query string) Filter { // NoLabelFilter return a Filter that match the absence of labels func NoLabelFilter() Filter { - return func(excerpt *BugExcerpt, resolver resolver) bool { + return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool { return len(excerpt.Labels) == 0 } } @@ -161,36 +155,36 @@ func compileMatcher(filters query.Filters) *Matcher { } // Match check if a bug match the set of filters -func (f *Matcher) Match(excerpt *BugExcerpt, resolver resolver) bool { - if match := f.orMatch(f.Status, excerpt, resolver); !match { +func (f *Matcher) Match(excerpt *BugExcerpt, resolvers entity.Resolvers) bool { + if match := f.orMatch(f.Status, excerpt, resolvers); !match { return false } - if match := f.orMatch(f.Author, excerpt, resolver); !match { + if match := f.orMatch(f.Author, excerpt, resolvers); !match { return false } - if match := f.orMatch(f.Metadata, excerpt, resolver); !match { + if match := f.orMatch(f.Metadata, excerpt, resolvers); !match { return false } - if match := f.orMatch(f.Participant, excerpt, resolver); !match { + if match := f.orMatch(f.Participant, excerpt, resolvers); !match { return false } - if match := f.orMatch(f.Actor, excerpt, resolver); !match { + if match := f.orMatch(f.Actor, excerpt, resolvers); !match { return false } - if match := f.andMatch(f.Label, excerpt, resolver); !match { + if match := f.andMatch(f.Label, excerpt, resolvers); !match { return false } - if match := f.andMatch(f.NoFilters, excerpt, resolver); !match { + if match := f.andMatch(f.NoFilters, excerpt, resolvers); !match { return false } - if match := f.andMatch(f.Title, excerpt, resolver); !match { + if match := f.andMatch(f.Title, excerpt, resolvers); !match { return false } @@ -198,28 +192,28 @@ func (f *Matcher) Match(excerpt *BugExcerpt, resolver resolver) bool { } // Check if any of the filters provided match the bug -func (*Matcher) orMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool { +func (*Matcher) orMatch(filters []Filter, excerpt *BugExcerpt, resolvers entity.Resolvers) bool { if len(filters) == 0 { return true } match := false for _, f := range filters { - match = match || f(excerpt, resolver) + match = match || f(excerpt, resolvers) } return match } // Check if all the filters provided match the bug -func (*Matcher) andMatch(filters []Filter, excerpt *BugExcerpt, resolver resolver) bool { +func (*Matcher) andMatch(filters []Filter, excerpt *BugExcerpt, resolvers entity.Resolvers) bool { if len(filters) == 0 { return true } match := true for _, f := range filters { - match = match && f(excerpt, resolver) + match = match && f(excerpt, resolvers) } return match diff --git a/cache/identity_excerpt.go b/cache/identity_excerpt.go index 126c712b..79d88537 100644 --- a/cache/identity_excerpt.go +++ b/cache/identity_excerpt.go @@ -5,7 +5,6 @@ import ( "fmt" "strings" - "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" ) @@ -14,6 +13,8 @@ func init() { gob.Register(IdentityExcerpt{}) } +var _ Excerpt = &IdentityExcerpt{} + // IdentityExcerpt hold a subset of the identity values to be able to sort and // filter identities efficiently without having to read and compile each raw // identity. @@ -25,7 +26,7 @@ type IdentityExcerpt struct { ImmutableMetadata map[string]string } -func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt { +func NewIdentityExcerpt(i *IdentityCache) *IdentityExcerpt { return &IdentityExcerpt{ id: i.Id(), Name: i.Name(), @@ -34,6 +35,10 @@ func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt { } } +func (i *IdentityExcerpt) setId(id entity.Id) { + i.id = id +} + func (i *IdentityExcerpt) Id() entity.Id { return i.id } diff --git a/cache/identity_subcache.go b/cache/identity_subcache.go index b175d731..a623d0e1 100644 --- a/cache/identity_subcache.go +++ b/cache/identity_subcache.go @@ -20,18 +20,34 @@ func NewRepoCacheIdentity(repo repository.ClockedRepo, return NewIdentityCache(i, repo, entityUpdated) } - makeExcerpt := func(i *identity.Identity) *IdentityExcerpt { - return NewIdentityExcerpt(i) - } - makeIndex := func(i *IdentityCache) []string { // no indexing return nil } + // TODO: this is terribly ugly, but we are currently stuck with the fact that identities are NOT using the fancy dag framework. + // This lead to various complication here and there to handle entities generically, and avoid large code duplication. + // TL;DR: something has to give, and this is the less ugly solution I found. This "normalize" identities as just another "dag framework" + // entity. Ideally identities would be converted to the dag framework, but right now that could lead to potential attack: if an old + // private key is leaked, it would be possible to craft a legal identity update that take over the most recent version. While this is + // meaningless in the case of a normal entity, it's really an issues for identities. + + actions := Actions[*identity.Identity]{ + ReadWithResolver: func(repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (*identity.Identity, error) { + return identity.ReadLocal(repo, id) + }, + ReadAllWithResolver: func(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan entity.StreamedEntity[*identity.Identity] { + return identity.ReadAllLocal(repo) + }, + Remove: identity.RemoveIdentity, + MergeAll: func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult { + return identity.MergeAll(repo, remote) + }, + } + sc := NewSubCache[*identity.Identity, *IdentityExcerpt, *IdentityCache]( repo, resolvers, getUserIdentity, - makeCached, makeExcerpt, makeIndex, + makeCached, NewIdentityExcerpt, makeIndex, actions, "identity", "identities", formatVersion, defaultMaxLoadedBugs, ) @@ -47,32 +63,32 @@ func (c *RepoCacheIdentity) ResolveIdentityImmutableMetadata(key string, value s }) } -func (c *RepoCacheIdentity) NewIdentityFromGitUser() (*IdentityCache, error) { - return c.NewIdentityFromGitUserRaw(nil) +// New create a new identity +// The new identity is written in the repository (commit) +func (c *RepoCacheIdentity) New(name string, email string) (*IdentityCache, error) { + return c.NewRaw(name, email, "", "", nil, nil) } -func (c *RepoCacheIdentity) NewIdentityFromGitUserRaw(metadata map[string]string) (*IdentityCache, error) { - i, err := identity.NewFromGitUser(c.repo) +// NewFull create a new identity +// The new identity is written in the repository (commit) +func (c *RepoCacheIdentity) NewFull(name string, email string, login string, avatarUrl string, keys []*identity.Key) (*IdentityCache, error) { + return c.NewRaw(name, email, login, avatarUrl, keys, nil) +} + +func (c *RepoCacheIdentity) NewRaw(name string, email string, login string, avatarUrl string, keys []*identity.Key, metadata map[string]string) (*IdentityCache, error) { + i, err := identity.NewIdentityFull(c.repo, name, email, login, avatarUrl, keys) if err != nil { return nil, err } return c.finishIdentity(i, metadata) } -// NewIdentity create a new identity -// The new identity is written in the repository (commit) -func (c *RepoCacheIdentity) NewIdentity(name string, email string) (*IdentityCache, error) { - return c.NewIdentityRaw(name, email, "", "", nil, nil) -} - -// NewIdentityFull create a new identity -// The new identity is written in the repository (commit) -func (c *RepoCacheIdentity) NewIdentityFull(name string, email string, login string, avatarUrl string, keys []*identity.Key) (*IdentityCache, error) { - return c.NewIdentityRaw(name, email, login, avatarUrl, keys, nil) +func (c *RepoCacheIdentity) NewFromGitUser() (*IdentityCache, error) { + return c.NewFromGitUserRaw(nil) } -func (c *RepoCacheIdentity) NewIdentityRaw(name string, email string, login string, avatarUrl string, keys []*identity.Key, metadata map[string]string) (*IdentityCache, error) { - i, err := identity.NewIdentityFull(c.repo, name, email, login, avatarUrl, keys) +func (c *RepoCacheIdentity) NewFromGitUserRaw(metadata map[string]string) (*IdentityCache, error) { + i, err := identity.NewFromGitUser(c.repo) if err != nil { return nil, err } diff --git a/cache/multi_repo_cache.go b/cache/multi_repo_cache.go index 98f868e9..12d26e26 100644 --- a/cache/multi_repo_cache.go +++ b/cache/multi_repo_cache.go @@ -21,25 +21,25 @@ func NewMultiRepoCache() *MultiRepoCache { } // RegisterRepository register a named repository. Use this for multi-repo setup -func (c *MultiRepoCache) RegisterRepository(ref string, repo repository.ClockedRepo) (*RepoCache, error) { - r, err := NewRepoCache(repo) +func (c *MultiRepoCache) RegisterRepository(ref string, repo repository.ClockedRepo) (*RepoCache, chan BuildEvent, error) { + r, events, err := NewRepoCache(repo) if err != nil { - return nil, err + return nil, nil, err } c.repos[ref] = r - return r, nil + return r, events, nil } // RegisterDefaultRepository register an unnamed repository. Use this for mono-repo setup -func (c *MultiRepoCache) RegisterDefaultRepository(repo repository.ClockedRepo) (*RepoCache, error) { - r, err := NewRepoCache(repo) +func (c *MultiRepoCache) RegisterDefaultRepository(repo repository.ClockedRepo) (*RepoCache, chan BuildEvent, error) { + r, events, err := NewRepoCache(repo) if err != nil { - return nil, err + return nil, nil, err } c.repos[defaultRepoName] = r - return r, nil + return r, events, nil } // DefaultRepo retrieve the default repository diff --git a/cache/repo_cache.go b/cache/repo_cache.go index b2facac3..2cac711b 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -30,8 +30,10 @@ var _ repository.RepoKeyring = &RepoCache{} type cacheMgmt interface { Typename() string Load() error - Write() error Build() error + SetCacheSize(size int) + MergeAll(remote string) <-chan entity.MergeResult + GetNamespace() string Close() error } @@ -86,18 +88,24 @@ func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, chan c.subcaches = append(c.subcaches, c.bugs) c.resolvers = entity.Resolvers{ - &IdentityCache{}: entity.ResolverFunc[*IdentityCache](c.identities.Resolve), - &BugCache{}: entity.ResolverFunc[*BugCache](c.bugs.Resolve), + &IdentityCache{}: entity.ResolverFunc[*IdentityCache](c.identities.Resolve), + &IdentityExcerpt{}: entity.ResolverFunc[*IdentityExcerpt](c.identities.ResolveExcerpt), + &BugCache{}: entity.ResolverFunc[*BugCache](c.bugs.Resolve), + &BugExcerpt{}: entity.ResolverFunc[*BugExcerpt](c.bugs.ResolveExcerpt), } err := c.lock() if err != nil { - return &RepoCache{}, nil, err + closed := make(chan BuildEvent) + close(closed) + return &RepoCache{}, closed, err } err = c.load() if err == nil { - return c, nil, nil + closed := make(chan BuildEvent) + close(closed) + return c, closed, nil } // Cache is either missing, broken or outdated. Rebuilding. @@ -122,8 +130,9 @@ func (c *RepoCache) getResolvers() entity.Resolvers { // setCacheSize change the maximum number of loaded bugs func (c *RepoCache) setCacheSize(size int) { - c.maxLoadedBugs = size - c.evictIfNeeded() + for _, subcache := range c.subcaches { + subcache.SetCacheSize(size) + } } // load will try to read from the disk all the cache files @@ -135,15 +144,6 @@ func (c *RepoCache) load() error { return errWait.Wait() } -// write will serialize on disk all the cache files -func (c *RepoCache) write() error { - var errWait multierr.ErrWaitGroup - for _, mgmt := range c.subcaches { - errWait.Go(mgmt.Write) - } - return errWait.Wait() -} - func (c *RepoCache) lock() error { err := repoIsAvailable(c.repo) if err != nil { @@ -190,10 +190,14 @@ const ( BuildEventFinished ) +// BuildEvent carry an event happening during the cache build process. type BuildEvent struct { + // Err carry an error if the build process failed. If set, no other field matter. + Err error + // Typename is the name of the entity of which the event relate to. Typename string - Event BuildEventType - Err error + // Event is the type of the event. + Event BuildEventType } func (c *RepoCache) buildCache() chan BuildEvent { @@ -221,15 +225,6 @@ func (c *RepoCache) buildCache() chan BuildEvent { return } - err = subcache.Write() - if err != nil { - out <- BuildEvent{ - Typename: subcache.Typename(), - Err: err, - } - return - } - out <- BuildEvent{ Typename: subcache.Typename(), Event: BuildEventFinished, diff --git a/cache/repo_cache_common.go b/cache/repo_cache_common.go index 1aed04b2..f768b8e2 100644 --- a/cache/repo_cache_common.go +++ b/cache/repo_cache_common.go @@ -1,10 +1,11 @@ package cache import ( + "sync" + "github.com/go-git/go-billy/v5" "github.com/pkg/errors" - "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/repository" @@ -72,76 +73,40 @@ func (c *RepoCache) StoreData(data []byte) (repository.Hash, error) { // Fetch retrieve updates from a remote // This does not change the local bugs or identities state func (c *RepoCache) Fetch(remote string) (string, error) { - stdout1, err := identity.Fetch(c.repo, remote) - if err != nil { - return stdout1, err - } - - stdout2, err := bug.Fetch(c.repo, remote) - if err != nil { - return stdout2, err + prefixes := make([]string, len(c.subcaches)) + for i, subcache := range c.subcaches { + prefixes[i] = subcache.GetNamespace() } - return stdout1 + stdout2, nil + // fetch everything at once, to have a single auth step if required. + return c.repo.FetchRefs(remote, prefixes...) } // MergeAll will merge all the available remote bug and identities func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { out := make(chan entity.MergeResult) - // Intercept merge results to update the cache properly + dependency := [][]cacheMgmt{ + {c.identities}, + {c.bugs}, + } + + // run MergeAll according to entities dependencies and merge the results go func() { defer close(out) - author, err := c.GetUserIdentity() - if err != nil { - out <- entity.NewMergeError(err, "") - return - } - - results := identity.MergeAll(c.repo, remote) - for result := range results { - out <- result - - if result.Err != nil { - continue - } - - switch result.Status { - case entity.MergeStatusNew, entity.MergeStatusUpdated: - i := result.Entity.(*identity.Identity) - c.muIdentity.Lock() - c.identitiesExcerpts[result.Id] = NewIdentityExcerpt(i) - c.muIdentity.Unlock() + for _, subcaches := range dependency { + var wg sync.WaitGroup + for _, subcache := range subcaches { + wg.Add(1) + go func(subcache cacheMgmt) { + for res := range subcache.MergeAll(remote) { + out <- res + } + wg.Done() + }(subcache) } - } - - results = bug.MergeAll(c.repo, c.resolvers, remote, author) - for result := range results { - out <- result - - if result.Err != nil { - continue - } - - // TODO: have subcache do the merging? - switch result.Status { - case entity.MergeStatusNew: - b := result.Entity.(*bug.Bug) - _, err := c.bugs.add(b) - case entity.MergeStatusUpdated: - _, err := c.bugs.entityUpdated(b) - snap := b.Compile() - c.muBug.Lock() - c.bugExcerpts[result.Id] = NewBugExcerpt(b, snap) - c.muBug.Unlock() - } - } - - err = c.write() - if err != nil { - out <- entity.NewMergeError(err, "") - return + wg.Wait() } }() @@ -150,17 +115,13 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { // Push update a remote with the local changes func (c *RepoCache) Push(remote string) (string, error) { - stdout1, err := identity.Push(c.repo, remote) - if err != nil { - return stdout1, err - } - - stdout2, err := bug.Push(c.repo, remote) - if err != nil { - return stdout2, err + prefixes := make([]string, len(c.subcaches)) + for i, subcache := range c.subcaches { + prefixes[i] = subcache.GetNamespace() } - return stdout1 + stdout2, nil + // push everything at once, to have a single auth step if required + return c.repo.PushRefs(remote, prefixes...) } // Pull will do a Fetch + MergeAll diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go index 58ade144..395872f7 100644 --- a/cache/repo_cache_test.go +++ b/cache/repo_cache_test.go @@ -8,20 +8,27 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/query" "github.com/MichaelMure/git-bug/repository" ) +func noBuildEventErrors(t *testing.T, c chan BuildEvent) { + t.Helper() + for event := range c { + require.NoError(t, event.Err) + } +} + func TestCache(t *testing.T) { repo := repository.CreateGoGitTestRepo(t, false) - cache, err := NewRepoCache(repo) + cache, events, err := NewRepoCache(repo) + noBuildEventErrors(t, events) require.NoError(t, err) // Create, set and get user identity - iden1, err := cache.NewIdentity("René Descartes", "rene@descartes.fr") + iden1, err := cache.Identities().New("René Descartes", "rene@descartes.fr") require.NoError(t, err) err = cache.SetUserIdentity(iden1) require.NoError(t, err) @@ -30,102 +37,105 @@ func TestCache(t *testing.T) { require.Equal(t, iden1.Id(), userIden.Id()) // it's possible to create two identical identities - iden2, err := cache.NewIdentity("René Descartes", "rene@descartes.fr") + iden2, err := cache.Identities().New("René Descartes", "rene@descartes.fr") require.NoError(t, err) // Two identical identities yield a different id require.NotEqual(t, iden1.Id(), iden2.Id()) // There is now two identities in the cache - require.Len(t, cache.AllIdentityIds(), 2) - require.Len(t, cache.identitiesExcerpts, 2) - require.Len(t, cache.identities, 2) + require.Len(t, cache.Identities().AllIds(), 2) + require.Len(t, cache.identities.excerpts, 2) + require.Len(t, cache.identities.cached, 2) // Create a bug - bug1, _, err := cache.NewBug("title", "message") + bug1, _, err := cache.Bugs().New("title", "message") require.NoError(t, err) // It's possible to create two identical bugs - bug2, _, err := cache.NewBug("title", "message") + bug2, _, err := cache.Bugs().New("title", "message") require.NoError(t, err) // two identical bugs yield a different id require.NotEqual(t, bug1.Id(), bug2.Id()) // There is now two bugs in the cache - require.Len(t, cache.AllBugsIds(), 2) - require.Len(t, cache.bugExcerpts, 2) - require.Len(t, cache.bugs, 2) + require.Len(t, cache.Bugs().AllIds(), 2) + require.Len(t, cache.bugs.excerpts, 2) + require.Len(t, cache.bugs.cached, 2) // Resolving - _, err = cache.ResolveIdentity(iden1.Id()) + _, err = cache.Identities().Resolve(iden1.Id()) require.NoError(t, err) - _, err = cache.ResolveIdentityExcerpt(iden1.Id()) + _, err = cache.Identities().ResolveExcerpt(iden1.Id()) require.NoError(t, err) - _, err = cache.ResolveIdentityPrefix(iden1.Id().String()[:10]) + _, err = cache.Identities().ResolvePrefix(iden1.Id().String()[:10]) require.NoError(t, err) - _, err = cache.ResolveBug(bug1.Id()) + _, err = cache.Bugs().Resolve(bug1.Id()) require.NoError(t, err) - _, err = cache.ResolveBugExcerpt(bug1.Id()) + _, err = cache.Bugs().ResolveExcerpt(bug1.Id()) require.NoError(t, err) - _, err = cache.ResolveBugPrefix(bug1.Id().String()[:10]) + _, err = cache.Bugs().ResolvePrefix(bug1.Id().String()[:10]) require.NoError(t, err) // Querying q, err := query.Parse("status:open author:descartes sort:edit-asc") require.NoError(t, err) - res, err := cache.QueryBugs(q) + res, err := cache.Bugs().Query(q) require.NoError(t, err) require.Len(t, res, 2) // Close require.NoError(t, cache.Close()) - require.Empty(t, cache.bugs) - require.Empty(t, cache.bugExcerpts) - require.Empty(t, cache.identities) - require.Empty(t, cache.identitiesExcerpts) + require.Empty(t, cache.bugs.cached) + require.Empty(t, cache.bugs.excerpts) + require.Empty(t, cache.identities.cached) + require.Empty(t, cache.identities.excerpts) // Reload, only excerpt are loaded, but as we need to load the identities used in the bugs // to check the signatures, we also load the identity used above - cache, err = NewRepoCache(repo) + cache, events, err = NewRepoCache(repo) + noBuildEventErrors(t, events) require.NoError(t, err) - require.Empty(t, cache.bugs) - require.Len(t, cache.identities, 1) - require.Len(t, cache.bugExcerpts, 2) - require.Len(t, cache.identitiesExcerpts, 2) + require.Empty(t, cache.bugs.cached) + require.Len(t, cache.bugs.excerpts, 2) + require.Len(t, cache.identities.cached, 0) + require.Len(t, cache.identities.excerpts, 2) // Resolving load from the disk - _, err = cache.ResolveIdentity(iden1.Id()) + _, err = cache.Identities().Resolve(iden1.Id()) require.NoError(t, err) - _, err = cache.ResolveIdentityExcerpt(iden1.Id()) + _, err = cache.Identities().ResolveExcerpt(iden1.Id()) require.NoError(t, err) - _, err = cache.ResolveIdentityPrefix(iden1.Id().String()[:10]) + _, err = cache.Identities().ResolvePrefix(iden1.Id().String()[:10]) require.NoError(t, err) - _, err = cache.ResolveBug(bug1.Id()) + _, err = cache.Bugs().Resolve(bug1.Id()) require.NoError(t, err) - _, err = cache.ResolveBugExcerpt(bug1.Id()) + _, err = cache.Bugs().ResolveExcerpt(bug1.Id()) require.NoError(t, err) - _, err = cache.ResolveBugPrefix(bug1.Id().String()[:10]) + _, err = cache.Bugs().ResolvePrefix(bug1.Id().String()[:10]) require.NoError(t, err) } func TestCachePushPull(t *testing.T) { repoA, repoB, _ := repository.SetupGoGitReposAndRemote(t) - cacheA, err := NewRepoCache(repoA) + cacheA, events, err := NewRepoCache(repoA) + noBuildEventErrors(t, events) require.NoError(t, err) - cacheB, err := NewRepoCache(repoB) + cacheB, events, err := NewRepoCache(repoB) + noBuildEventErrors(t, events) require.NoError(t, err) // Create, set and get user identity - reneA, err := cacheA.NewIdentity("René Descartes", "rene@descartes.fr") + reneA, err := cacheA.Identities().New("René Descartes", "rene@descartes.fr") require.NoError(t, err) err = cacheA.SetUserIdentity(reneA) require.NoError(t, err) - isaacB, err := cacheB.NewIdentity("Isaac Newton", "isaac@newton.uk") + isaacB, err := cacheB.Identities().New("Isaac Newton", "isaac@newton.uk") require.NoError(t, err) err = cacheB.SetUserIdentity(isaacB) require.NoError(t, err) @@ -137,7 +147,7 @@ func TestCachePushPull(t *testing.T) { require.NoError(t, err) // Create a bug in A - _, _, err = cacheA.NewBug("bug1", "message") + _, _, err = cacheA.Bugs().New("bug1", "message") require.NoError(t, err) // A --> remote --> B @@ -147,17 +157,17 @@ func TestCachePushPull(t *testing.T) { err = cacheB.Pull("origin") require.NoError(t, err) - require.Len(t, cacheB.AllBugsIds(), 1) + require.Len(t, cacheB.Bugs().AllIds(), 1) // retrieve and set identity - reneB, err := cacheB.ResolveIdentity(reneA.Id()) + reneB, err := cacheB.Identities().Resolve(reneA.Id()) require.NoError(t, err) err = cacheB.SetUserIdentity(reneB) require.NoError(t, err) // B --> remote --> A - _, _, err = cacheB.NewBug("bug2", "message") + _, _, err = cacheB.Bugs().New("bug2", "message") require.NoError(t, err) _, err = cacheB.Push("origin") @@ -166,7 +176,7 @@ func TestCachePushPull(t *testing.T) { err = cacheA.Pull("origin") require.NoError(t, err) - require.Len(t, cacheA.AllBugsIds(), 2) + require.Len(t, cacheA.Bugs().AllIds(), 2) } func TestRemove(t *testing.T) { @@ -180,20 +190,21 @@ func TestRemove(t *testing.T) { err = repo.AddRemote("remoteB", remoteB.GetLocalRemote()) require.NoError(t, err) - repoCache, err := NewRepoCache(repo) + repoCache, events, err := NewRepoCache(repo) + noBuildEventErrors(t, events) require.NoError(t, err) - rene, err := repoCache.NewIdentity("René Descartes", "rene@descartes.fr") + rene, err := repoCache.Identities().New("René Descartes", "rene@descartes.fr") require.NoError(t, err) err = repoCache.SetUserIdentity(rene) require.NoError(t, err) - _, _, err = repoCache.NewBug("title", "message") + _, _, err = repoCache.Bugs().New("title", "message") require.NoError(t, err) // and one more for testing - b1, _, err := repoCache.NewBug("title", "message") + b1, _, err := repoCache.Bugs().New("title", "message") require.NoError(t, err) _, err = repoCache.Push("remoteA") @@ -208,72 +219,73 @@ func TestRemove(t *testing.T) { _, err = repoCache.Fetch("remoteB") require.NoError(t, err) - err = repoCache.RemoveBug(b1.Id().String()) + err = repoCache.Bugs().Remove(b1.Id().String()) require.NoError(t, err) - assert.Equal(t, 1, len(repoCache.bugs)) - assert.Equal(t, 1, len(repoCache.bugExcerpts)) + assert.Len(t, repoCache.bugs.cached, 1) + assert.Len(t, repoCache.bugs.excerpts, 1) - _, err = repoCache.ResolveBug(b1.Id()) - assert.ErrorIs(t, entity.ErrNotFound{}, err) + _, err = repoCache.Bugs().Resolve(b1.Id()) + assert.ErrorAs(t, entity.ErrNotFound{}, err) } func TestCacheEviction(t *testing.T) { repo := repository.CreateGoGitTestRepo(t, false) - repoCache, err := NewRepoCache(repo) + repoCache, events, err := NewRepoCache(repo) + noBuildEventErrors(t, events) require.NoError(t, err) repoCache.setCacheSize(2) - require.Equal(t, 2, repoCache.maxLoadedBugs) - require.Equal(t, 0, repoCache.loadedBugs.Len()) - require.Equal(t, 0, len(repoCache.bugs)) + require.Equal(t, 2, repoCache.bugs.maxLoaded) + require.Len(t, repoCache.bugs.cached, 0) + require.Equal(t, repoCache.bugs.lru.Len(), 0) // Generating some bugs - rene, err := repoCache.NewIdentity("René Descartes", "rene@descartes.fr") + rene, err := repoCache.Identities().New("René Descartes", "rene@descartes.fr") require.NoError(t, err) err = repoCache.SetUserIdentity(rene) require.NoError(t, err) - bug1, _, err := repoCache.NewBug("title", "message") + bug1, _, err := repoCache.Bugs().New("title", "message") require.NoError(t, err) checkBugPresence(t, repoCache, bug1, true) - require.Equal(t, 1, repoCache.loadedBugs.Len()) - require.Equal(t, 1, len(repoCache.bugs)) + require.Len(t, repoCache.bugs.cached, 1) + require.Equal(t, 1, repoCache.bugs.lru.Len()) - bug2, _, err := repoCache.NewBug("title", "message") + bug2, _, err := repoCache.Bugs().New("title", "message") require.NoError(t, err) checkBugPresence(t, repoCache, bug1, true) checkBugPresence(t, repoCache, bug2, true) - require.Equal(t, 2, repoCache.loadedBugs.Len()) - require.Equal(t, 2, len(repoCache.bugs)) + require.Len(t, repoCache.bugs.cached, 2) + require.Equal(t, 2, repoCache.bugs.lru.Len()) // Number of bugs should not exceed max size of lruCache, oldest one should be evicted - bug3, _, err := repoCache.NewBug("title", "message") + bug3, _, err := repoCache.Bugs().New("title", "message") require.NoError(t, err) - require.Equal(t, 2, repoCache.loadedBugs.Len()) - require.Equal(t, 2, len(repoCache.bugs)) + require.Len(t, repoCache.bugs.cached, 2) + require.Equal(t, 2, repoCache.bugs.lru.Len()) checkBugPresence(t, repoCache, bug1, false) checkBugPresence(t, repoCache, bug2, true) checkBugPresence(t, repoCache, bug3, true) // Accessing bug should update position in lruCache and therefore it should not be evicted - repoCache.loadedBugs.Get(bug2.Id()) - oldestId, _ := repoCache.loadedBugs.GetOldest() + repoCache.bugs.lru.Get(bug2.Id()) + oldestId, _ := repoCache.bugs.lru.GetOldest() require.Equal(t, bug3.Id(), oldestId) checkBugPresence(t, repoCache, bug1, false) checkBugPresence(t, repoCache, bug2, true) checkBugPresence(t, repoCache, bug3, true) - require.Equal(t, 2, repoCache.loadedBugs.Len()) - require.Equal(t, 2, len(repoCache.bugs)) + require.Len(t, repoCache.bugs.cached, 2) + require.Equal(t, 2, repoCache.bugs.lru.Len()) } func checkBugPresence(t *testing.T, cache *RepoCache, bug *BugCache, presence bool) { id := bug.Id() - require.Equal(t, presence, cache.loadedBugs.Contains(id)) - b, ok := cache.bugs[id] + require.Equal(t, presence, cache.bugs.lru.Contains(id)) + b, ok := cache.bugs.cached[id] require.Equal(t, presence, ok) if ok { require.Equal(t, bug, b) @@ -287,12 +299,13 @@ func TestLongDescription(t *testing.T) { repo := repository.CreateGoGitTestRepo(t, false) - backend, err := NewRepoCache(repo) + backend, events, err := NewRepoCache(repo) + noBuildEventErrors(t, events) require.NoError(t, err) - i, err := backend.NewIdentity("René Descartes", "rene@descartes.fr") + i, err := backend.Identities().New("René Descartes", "rene@descartes.fr") require.NoError(t, err) - _, _, err = backend.NewBugRaw(i, time.Now().Unix(), text, text, nil, nil) + _, _, err = backend.Bugs().NewRaw(i, time.Now().Unix(), text, text, nil, nil) require.NoError(t, err) } diff --git a/cache/subcache.go b/cache/subcache.go index 1737da43..0678988a 100644 --- a/cache/subcache.go +++ b/cache/subcache.go @@ -4,18 +4,18 @@ import ( "bytes" "encoding/gob" "fmt" - "os" "sync" "github.com/pkg/errors" - "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/repository" ) type Excerpt interface { Id() entity.Id + setId(id entity.Id) } type CacheEntity interface { @@ -25,15 +25,27 @@ type CacheEntity interface { type getUserIdentityFunc func() (*IdentityCache, error) +// Actions expose a number of action functions on Entities, to give upper layers (cache) a way to normalize interactions. +// Note: ideally this wouldn't exist, the cache layer would assume that everything is an entity/dag, and directly use the +// functions from this package, but right now identities are not using that framework. +type Actions[EntityT entity.Interface] struct { + ReadWithResolver func(repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (EntityT, error) + ReadAllWithResolver func(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan entity.StreamedEntity[EntityT] + Remove func(repo repository.ClockedRepo, id entity.Id) error + MergeAll func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult +} + +var _ cacheMgmt = &SubCache[entity.Interface, Excerpt, CacheEntity]{} + type SubCache[EntityT entity.Interface, ExcerptT Excerpt, CacheT CacheEntity] struct { repo repository.ClockedRepo resolvers func() entity.Resolvers - getUserIdentity getUserIdentityFunc - readWithResolver func(repository.ClockedRepo, entity.Resolvers, entity.Id) (EntityT, error) - makeCached func(entity EntityT, entityUpdated func(id entity.Id) error) CacheT - makeExcerpt func(EntityT) ExcerptT - makeIndex func(CacheT) []string + getUserIdentity getUserIdentityFunc + makeCached func(entity EntityT, entityUpdated func(id entity.Id) error) CacheT + makeExcerpt func(CacheT) ExcerptT + makeIndexData func(CacheT) []string + actions Actions[EntityT] typename string namespace string @@ -50,14 +62,19 @@ func NewSubCache[EntityT entity.Interface, ExcerptT Excerpt, CacheT CacheEntity] repo repository.ClockedRepo, resolvers func() entity.Resolvers, getUserIdentity getUserIdentityFunc, makeCached func(entity EntityT, entityUpdated func(id entity.Id) error) CacheT, - makeExcerpt func(EntityT) ExcerptT, - makeIndex func(CacheT) []string, + makeExcerpt func(CacheT) ExcerptT, + makeIndexData func(CacheT) []string, + actions Actions[EntityT], typename, namespace string, version uint, maxLoaded int) *SubCache[EntityT, ExcerptT, CacheT] { return &SubCache[EntityT, ExcerptT, CacheT]{ repo: repo, resolvers: resolvers, getUserIdentity: getUserIdentity, + makeCached: makeCached, + makeExcerpt: makeExcerpt, + makeIndexData: makeIndexData, + actions: actions, typename: typename, namespace: namespace, version: version, @@ -98,6 +115,12 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Load() error { return fmt.Errorf("unknown %s cache format version %v", sc.namespace, aux.Version) } + // the id is not serialized in the excerpt itself (non-exported field in go, long story ...), + // so we fix it here, which doubles as enforcing coherency. + for id, excerpt := range aux.Excerpts { + excerpt.setId(id) + } + sc.excerpts = aux.Excerpts index, err := sc.repo.GetIndex(sc.typename) @@ -118,7 +141,7 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Load() error { } // Write will serialize on disk the entity cache file -func (sc *SubCache[EntityT, ExcerptT, CacheT]) Write() error { +func (sc *SubCache[EntityT, ExcerptT, CacheT]) write() error { sc.mu.RLock() defer sc.mu.RUnlock() @@ -155,9 +178,7 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Write() error { func (sc *SubCache[EntityT, ExcerptT, CacheT]) Build() error { sc.excerpts = make(map[entity.Id]ExcerptT) - sc.readWithResolver - - allBugs := bug.ReadAllWithResolver(c.repo, c.resolvers) + allEntities := sc.actions.ReadAllWithResolver(sc.repo, sc.resolvers()) index, err := sc.repo.GetIndex(sc.typename) if err != nil { @@ -172,15 +193,17 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Build() error { indexer, indexEnd := index.IndexBatch() - for b := range allBugs { - if b.Err != nil { - return b.Err + for e := range allEntities { + if e.Err != nil { + return e.Err } - snap := b.Bug.Compile() - c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, snap) + // TODO: doesn't actually record in cache, should we? + cached := sc.makeCached(e.Entity, sc.entityUpdated) + sc.excerpts[e.Entity.Id()] = sc.makeExcerpt(cached) - if err := indexer(snap); err != nil { + indexData := sc.makeIndexData(cached) + if err := indexer(e.Entity.Id().String(), indexData); err != nil { return err } } @@ -190,10 +213,19 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Build() error { return err } - _, _ = fmt.Fprintln(os.Stderr, "Done.") + err = sc.write() + if err != nil { + return err + } + return nil } +func (sc *SubCache[EntityT, ExcerptT, CacheT]) SetCacheSize(size int) { + sc.maxLoaded = size + sc.evictIfNeeded() +} + func (sc *SubCache[EntityT, ExcerptT, CacheT]) Close() error { sc.mu.Lock() defer sc.mu.Unlock() @@ -229,7 +261,7 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Resolve(id entity.Id) (CacheT, er } sc.mu.RUnlock() - e, err := sc.readWithResolver(sc.repo, sc.resolvers(), id) + e, err := sc.actions.ReadWithResolver(sc.repo, sc.resolvers(), id) if err != nil { return *new(CacheT), err } @@ -315,8 +347,6 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) resolveMatcher(f func(ExcerptT) b return matching[0], nil } -var errNotInCache = errors.New("entity missing from cache") - func (sc *SubCache[EntityT, ExcerptT, CacheT]) add(e EntityT) (CacheT, error) { sc.mu.Lock() if _, has := sc.cached[e.Id()]; has { @@ -348,26 +378,74 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Remove(prefix string) error { sc.mu.Lock() - err = bug.Remove(c.repo, b.Id()) + err = sc.actions.Remove(sc.repo, e.Id()) if err != nil { - c.muBug.Unlock() - + sc.mu.Unlock() return err } - delete(c.bugs, b.Id()) - delete(c.bugExcerpts, b.Id()) - c.loadedBugs.Remove(b.Id()) + delete(sc.cached, e.Id()) + delete(sc.excerpts, e.Id()) + sc.lru.Remove(e.Id()) + + sc.mu.Unlock() + + return sc.write() +} + +func (sc *SubCache[EntityT, ExcerptT, CacheT]) MergeAll(remote string) <-chan entity.MergeResult { + out := make(chan entity.MergeResult) - c.muBug.Unlock() + // Intercept merge results to update the cache properly + go func() { + defer close(out) + + author, err := sc.getUserIdentity() + if err != nil { + out <- entity.NewMergeError(err, "") + return + } + + results := sc.actions.MergeAll(sc.repo, sc.resolvers(), remote, author) + for result := range results { + out <- result + + if result.Err != nil { + continue + } + + switch result.Status { + case entity.MergeStatusNew, entity.MergeStatusUpdated: + e := result.Entity.(EntityT) + + // TODO: doesn't actually record in cache, should we? + cached := sc.makeCached(e, sc.entityUpdated) + + sc.mu.Lock() + sc.excerpts[result.Id] = sc.makeExcerpt(cached) + sc.mu.Unlock() + } + } + + err = sc.write() + if err != nil { + out <- entity.NewMergeError(err, "") + return + } + }() + + return out - return c.writeBugCache() +} + +func (sc *SubCache[EntityT, ExcerptT, CacheT]) GetNamespace() string { + return sc.namespace } // entityUpdated is a callback to trigger when the excerpt of an entity changed func (sc *SubCache[EntityT, ExcerptT, CacheT]) entityUpdated(id entity.Id) error { sc.mu.Lock() - b, ok := sc.cached[id] + e, ok := sc.cached[id] if !ok { sc.mu.Unlock() @@ -376,19 +454,24 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) entityUpdated(id entity.Id) error // memory and thus concurrent write. // Failing immediately here is the simple and safe solution to avoid // complicated data loss. - return errNotInCache + return errors.New("entity missing from cache") } sc.lru.Get(id) // sc.excerpts[id] = bug2.NewBugExcerpt(b.bug, b.Snapshot()) - sc.excerpts[id] = bug2.NewBugExcerpt(b.bug, b.Snapshot()) + sc.excerpts[id] = sc.makeExcerpt(e) sc.mu.Unlock() - if err := sc.addBugToSearchIndex(b.Snapshot()); err != nil { + index, err := sc.repo.GetIndex(sc.typename) + if err != nil { + return err + } + + err = index.IndexOne(e.Id().String(), sc.makeIndexData(e)) + if err != nil { return err } - // we only need to write the bug cache - return sc.Write() + return sc.write() } // evictIfNeeded will evict an entity from the cache if needed @@ -405,7 +488,8 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) evictIfNeeded() { continue } - b.Lock() + // TODO + // b.Lock() sc.lru.Remove(id) delete(sc.cached, id) diff --git a/commands/bridge/bridge_auth_addtoken.go b/commands/bridge/bridge_auth_addtoken.go index bcab7fc3..2992fa63 100644 --- a/commands/bridge/bridge_auth_addtoken.go +++ b/commands/bridge/bridge_auth_addtoken.go @@ -94,7 +94,7 @@ func runBridgeAuthAddToken(env *execenv.Env, opts bridgeAuthAddTokenOptions, arg if opts.user == "" { user, err = env.Backend.GetUserIdentity() } else { - user, err = env.Backend.ResolveIdentityPrefix(opts.user) + user, err = env.Backend.Identities().ResolvePrefix(opts.user) } if err != nil { return err diff --git a/commands/bug/bug.go b/commands/bug/bug.go index 04bf8980..bab040d8 100644 --- a/commands/bug/bug.go +++ b/commands/bug/bug.go @@ -142,14 +142,14 @@ func runBug(env *execenv.Env, opts bugOptions, args []string) error { return err } - allIds, err := env.Backend.QueryBugs(q) + allIds, err := env.Backend.Bugs().Query(q) if err != nil { return err } bugExcerpt := make([]*cache.BugExcerpt, len(allIds)) for i, id := range allIds { - b, err := env.Backend.ResolveBugExcerpt(id) + b, err := env.Backend.Bugs().ResolveExcerpt(id) if err != nil { return err } @@ -208,8 +208,8 @@ func bugsJsonFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error jsonBugs := make([]JSONBugExcerpt, len(bugExcerpts)) for i, b := range bugExcerpts { jsonBug := JSONBugExcerpt{ - Id: b.Id.String(), - HumanId: b.Id.Human(), + Id: b.Id().String(), + HumanId: b.Id().Human(), CreateTime: cmdjson.NewTime(b.CreateTime(), b.CreateLamportTime), EditTime: cmdjson.NewTime(b.EditTime(), b.EditLamportTime), Status: b.Status.String(), @@ -219,7 +219,7 @@ func bugsJsonFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error Metadata: b.CreateMetadata, } - author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId) + author, err := env.Backend.Identities().ResolveExcerpt(b.AuthorId) if err != nil { return err } @@ -227,7 +227,7 @@ func bugsJsonFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error jsonBug.Actors = make([]cmdjson.Identity, len(b.Actors)) for i, element := range b.Actors { - actor, err := env.Backend.ResolveIdentityExcerpt(element) + actor, err := env.Backend.Identities().ResolveExcerpt(element) if err != nil { return err } @@ -236,7 +236,7 @@ func bugsJsonFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error jsonBug.Participants = make([]cmdjson.Identity, len(b.Participants)) for i, element := range b.Participants { - participant, err := env.Backend.ResolveIdentityExcerpt(element) + participant, err := env.Backend.Identities().ResolveExcerpt(element) if err != nil { return err } @@ -252,7 +252,7 @@ func bugsJsonFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error func bugsCompactFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { for _, b := range bugExcerpts { - author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId) + author, err := env.Backend.Identities().ResolveExcerpt(b.AuthorId) if err != nil { return err } @@ -266,7 +266,7 @@ func bugsCompactFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err } env.Out.Printf("%s %s %s %s %s\n", - colors.Cyan(b.Id.Human()), + colors.Cyan(b.Id().Human()), colors.Yellow(b.Status), text.LeftPadMaxLine(strings.TrimSpace(b.Title), 40, 0), text.LeftPadMaxLine(labelsTxt.String(), 5, 0), @@ -278,7 +278,7 @@ func bugsCompactFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err func bugsIDFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { for _, b := range bugExcerpts { - env.Out.Println(b.Id.String()) + env.Out.Println(b.Id().String()) } return nil @@ -286,7 +286,7 @@ func bugsIDFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { for _, b := range bugExcerpts { - author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId) + author, err := env.Backend.Identities().ResolveExcerpt(b.AuthorId) if err != nil { return err } @@ -313,7 +313,7 @@ func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err } env.Out.Printf("%s\t%s\t%s\t%s\t%s\n", - colors.Cyan(b.Id.Human()), + colors.Cyan(b.Id().Human()), colors.Yellow(b.Status), titleFmt+labelsFmt, colors.Magenta(authorFmt), @@ -325,7 +325,7 @@ func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err func bugsPlainFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { for _, b := range bugExcerpts { - env.Out.Printf("%s [%s] %s\n", b.Id.Human(), b.Status, strings.TrimSpace(b.Title)) + env.Out.Printf("%s [%s] %s\n", b.Id().Human(), b.Status, strings.TrimSpace(b.Title)) } return nil } @@ -353,7 +353,7 @@ func bugsOrgmodeFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err title = b.Title } - author, err := env.Backend.ResolveIdentityExcerpt(b.AuthorId) + author, err := env.Backend.Identities().ResolveExcerpt(b.AuthorId) if err != nil { return err } @@ -370,7 +370,7 @@ func bugsOrgmodeFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err env.Out.Printf("* %-6s %s %s %s: %s %s\n", status, - b.Id.Human(), + b.Id().Human(), formatTime(b.CreateTime()), author.DisplayName(), title, @@ -381,26 +381,26 @@ func bugsOrgmodeFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err env.Out.Printf("** Actors:\n") for _, element := range b.Actors { - actor, err := env.Backend.ResolveIdentityExcerpt(element) + actor, err := env.Backend.Identities().ResolveExcerpt(element) if err != nil { return err } env.Out.Printf(": %s %s\n", - actor.Id.Human(), + actor.Id().Human(), actor.DisplayName(), ) } env.Out.Printf("** Participants:\n") for _, element := range b.Participants { - participant, err := env.Backend.ResolveIdentityExcerpt(element) + participant, err := env.Backend.Identities().ResolveExcerpt(element) if err != nil { return err } env.Out.Printf(": %s %s\n", - participant.Id.Human(), + participant.Id().Human(), participant.DisplayName(), ) } diff --git a/commands/bug/bug_comment_edit.go b/commands/bug/bug_comment_edit.go index 8be7cb80..2a0289f5 100644 --- a/commands/bug/bug_comment_edit.go +++ b/commands/bug/bug_comment_edit.go @@ -41,7 +41,7 @@ func newBugCommentEditCommand() *cobra.Command { } func runBugCommentEdit(env *execenv.Env, opts bugCommentEditOptions, args []string) error { - b, commentId, err := env.Backend.ResolveComment(args[0]) + b, commentId, err := env.Backend.Bugs().ResolveComment(args[0]) if err != nil { return err } diff --git a/commands/bug/bug_new.go b/commands/bug/bug_new.go index 4f73a09c..fbfb9def 100644 --- a/commands/bug/bug_new.go +++ b/commands/bug/bug_new.go @@ -63,7 +63,7 @@ func runBugNew(env *execenv.Env, opts bugNewOptions) error { } } - b, _, err := env.Backend.NewBug( + b, _, err := env.Backend.Bugs().New( text.CleanupOneLine(opts.title), text.Cleanup(opts.message), ) diff --git a/commands/bug/bug_rm.go b/commands/bug/bug_rm.go index 1d2a7524..04881d54 100644 --- a/commands/bug/bug_rm.go +++ b/commands/bug/bug_rm.go @@ -34,7 +34,7 @@ func runBugRm(env *execenv.Env, args []string) (err error) { return errors.New("you must provide a bug prefix to remove") } - err = env.Backend.RemoveBug(args[0]) + err = env.Backend.Bugs().Remove(args[0]) if err != nil { return diff --git a/commands/bug/bug_select.go b/commands/bug/bug_select.go index 0b1cb15c..2a4d1201 100644 --- a/commands/bug/bug_select.go +++ b/commands/bug/bug_select.go @@ -46,7 +46,7 @@ func runBugSelect(env *execenv.Env, args []string) error { prefix := args[0] - b, err := env.Backend.ResolveBugPrefix(prefix) + b, err := env.Backend.Bugs().ResolvePrefix(prefix) if err != nil { return err } diff --git a/commands/bug/select/select.go b/commands/bug/select/select.go index 42d65bc2..7096dde4 100644 --- a/commands/bug/select/select.go +++ b/commands/bug/select/select.go @@ -9,7 +9,6 @@ import ( "github.com/pkg/errors" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entity" ) @@ -28,7 +27,7 @@ var ErrNoValidId = errors.New("you must provide a bug id or use the \"select\" c func ResolveBug(repo *cache.RepoCache, args []string) (*cache.BugCache, []string, error) { // At first, try to use the first argument as a bug prefix if len(args) > 0 { - b, err := repo.ResolveBugPrefix(args[0]) + b, err := repo.Bugs().ResolvePrefix(args[0]) if err == nil { return b, args[1:], nil @@ -115,7 +114,7 @@ func selected(repo *cache.RepoCache) (*cache.BugCache, error) { return nil, fmt.Errorf("select file in invalid, removing it") } - b, err := repo.ResolveBug(id) + b, err := repo.Bugs().Resolve(id) if err != nil { return nil, err } diff --git a/commands/bug/select/select_test.go b/commands/bug/select/select_test.go index 702700f4..5533ac2b 100644 --- a/commands/bug/select/select_test.go +++ b/commands/bug/select/select_test.go @@ -13,8 +13,11 @@ import ( func TestSelect(t *testing.T) { repo := repository.CreateGoGitTestRepo(t, false) - repoCache, err := cache.NewRepoCache(repo) + repoCache, events, err := cache.NewRepoCache(repo) require.NoError(t, err) + for event := range events { + require.NoError(t, event.Err) + } _, _, err = ResolveBug(repoCache, []string{}) require.Equal(t, ErrNoValidId, err) @@ -28,18 +31,18 @@ func TestSelect(t *testing.T) { // generate a bunch of bugs - rene, err := repoCache.NewIdentity("René Descartes", "rene@descartes.fr") + rene, err := repoCache.Identities().New("René Descartes", "rene@descartes.fr") require.NoError(t, err) for i := 0; i < 10; i++ { - _, _, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil) + _, _, err := repoCache.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil) require.NoError(t, err) } // and two more for testing - b1, _, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil) + b1, _, err := repoCache.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil) require.NoError(t, err) - b2, _, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil) + b2, _, err := repoCache.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil) require.NoError(t, err) err = Select(repoCache, b1.Id()) diff --git a/commands/bug/testenv/testenv.go b/commands/bug/testenv/testenv.go index 10f20950..acd1f389 100644 --- a/commands/bug/testenv/testenv.go +++ b/commands/bug/testenv/testenv.go @@ -19,7 +19,7 @@ func NewTestEnvAndUser(t *testing.T) (*execenv.Env, entity.Id) { testEnv := execenv.NewTestEnv(t) - i, err := testEnv.Backend.NewIdentity(testUserName, testUserEmail) + i, err := testEnv.Backend.Identities().New(testUserName, testUserEmail) require.NoError(t, err) err = testEnv.Backend.SetUserIdentity(i) @@ -38,7 +38,7 @@ func NewTestEnvAndBug(t *testing.T) (*execenv.Env, entity.Id) { testEnv, _ := NewTestEnvAndUser(t) - b, _, err := testEnv.Backend.NewBug(testBugTitle, testBugMessage) + b, _, err := testEnv.Backend.Bugs().New(testBugTitle, testBugMessage) require.NoError(t, err) return testEnv, b.Id() @@ -53,7 +53,7 @@ func NewTestEnvAndBugWithComment(t *testing.T) (*execenv.Env, entity.Id, entity. env, bugID := NewTestEnvAndBug(t) - b, err := env.Backend.ResolveBug(bugID) + b, err := env.Backend.Bugs().Resolve(bugID) require.NoError(t, err) commentId, _, err := b.AddComment(testCommentMessage) diff --git a/commands/cmdjson/json_common.go b/commands/cmdjson/json_common.go index 60e6e751..34077915 100644 --- a/commands/cmdjson/json_common.go +++ b/commands/cmdjson/json_common.go @@ -26,8 +26,8 @@ func NewIdentity(i identity.Interface) Identity { func NewIdentityFromExcerpt(excerpt *cache.IdentityExcerpt) Identity { return Identity{ - Id: excerpt.Id.String(), - HumanId: excerpt.Id.Human(), + Id: excerpt.Id().String(), + HumanId: excerpt.Id().Human(), Name: excerpt.Name, Login: excerpt.Login, } diff --git a/commands/completion/helper_completion.go b/commands/completion/helper_completion.go index 27fbd615..691f0895 100644 --- a/commands/completion/helper_completion.go +++ b/commands/completion/helper_completion.go @@ -88,11 +88,11 @@ func Bug(env *execenv.Env) ValidArgsFunction { } func bugWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) { - allIds := backend.AllBugsIds() + allIds := backend.Bugs().AllIds() bugExcerpt := make([]*cache.BugExcerpt, len(allIds)) for i, id := range allIds { var err error - bugExcerpt[i], err = backend.ResolveBugExcerpt(id) + bugExcerpt[i], err = backend.Bugs().ResolveExcerpt(id) if err != nil { return handleError(err) } @@ -138,7 +138,7 @@ func BugAndLabels(env *execenv.Env, addOrRemove bool) ValidArgsFunction { seenLabels[label] = true } - allLabels := env.Backend.ValidLabels() + allLabels := env.Backend.Bugs().ValidLabels() labels = make([]bug.Label, 0, len(allLabels)) for _, label := range allLabels { if !seenLabels[label] { @@ -200,7 +200,7 @@ func Label(env *execenv.Env) ValidArgsFunction { _ = env.Backend.Close() }() - labels := env.Backend.ValidLabels() + labels := env.Backend.Bugs().ValidLabels() completions = make([]string, len(labels)) for i, label := range labels { if strings.Contains(label.String(), " ") { @@ -243,10 +243,10 @@ func Ls(env *execenv.Env) ValidArgsFunction { if !strings.HasPrefix(toComplete, key) { continue } - ids := env.Backend.AllIdentityIds() + ids := env.Backend.Identities().AllIds() completions = make([]string, len(ids)) for i, id := range ids { - user, err := env.Backend.ResolveIdentityExcerpt(id) + user, err := env.Backend.Identities().ResolveExcerpt(id) if err != nil { return handleError(err) } @@ -266,7 +266,7 @@ func Ls(env *execenv.Env) ValidArgsFunction { if !strings.HasPrefix(toComplete, key) { continue } - labels := env.Backend.ValidLabels() + labels := env.Backend.Bugs().ValidLabels() completions = make([]string, len(labels)) for i, label := range labels { if strings.Contains(label.String(), " ") { @@ -300,14 +300,14 @@ func User(env *execenv.Env) ValidArgsFunction { _ = env.Backend.Close() }() - ids := env.Backend.AllIdentityIds() + ids := env.Backend.Identities().AllIds() completions = make([]string, len(ids)) for i, id := range ids { - user, err := env.Backend.ResolveIdentityExcerpt(id) + user, err := env.Backend.Identities().ResolveExcerpt(id) if err != nil { return handleError(err) } - completions[i] = user.Id.Human() + "\t" + user.DisplayName() + completions[i] = user.Id().Human() + "\t" + user.DisplayName() } return completions, cobra.ShellCompDirectiveNoFileComp } @@ -322,10 +322,10 @@ func UserForQuery(env *execenv.Env) ValidArgsFunction { _ = env.Backend.Close() }() - ids := env.Backend.AllIdentityIds() + ids := env.Backend.Identities().AllIds() completions = make([]string, len(ids)) for i, id := range ids { - user, err := env.Backend.ResolveIdentityExcerpt(id) + user, err := env.Backend.Identities().ResolveExcerpt(id) if err != nil { return handleError(err) } diff --git a/commands/execenv/env_testing.go b/commands/execenv/env_testing.go index 7d9fbd60..ddba735f 100644 --- a/commands/execenv/env_testing.go +++ b/commands/execenv/env_testing.go @@ -5,9 +5,10 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/repository" - "github.com/stretchr/testify/require" ) type TestOut struct { @@ -33,8 +34,12 @@ func NewTestEnv(t *testing.T) *Env { buf := new(bytes.Buffer) - backend, err := cache.NewRepoCache(repo) + backend, events, err := cache.NewRepoCache(repo) require.NoError(t, err) + for event := range events { + require.NoError(t, event.Err) + } + t.Cleanup(func() { backend.Close() }) diff --git a/commands/label.go b/commands/label.go index 70090d26..08b9e31f 100644 --- a/commands/label.go +++ b/commands/label.go @@ -25,7 +25,7 @@ Note: in the future, a proper label policy could be implemented where valid labe } func runLabel(env *execenv.Env) error { - labels := env.Backend.ValidLabels() + labels := env.Backend.Bugs().ValidLabels() for _, l := range labels { env.Out.Println(l) diff --git a/commands/user/user.go b/commands/user/user.go index 191fb828..9a1e477c 100644 --- a/commands/user/user.go +++ b/commands/user/user.go @@ -46,10 +46,10 @@ func NewUserCommand() *cobra.Command { } func runUser(env *execenv.Env, opts userOptions) error { - ids := env.Backend.AllIdentityIds() + ids := env.Backend.Identities().AllIds() var users []*cache.IdentityExcerpt for _, id := range ids { - user, err := env.Backend.ResolveIdentityExcerpt(id) + user, err := env.Backend.Identities().ResolveExcerpt(id) if err != nil { return err } @@ -69,7 +69,7 @@ func runUser(env *execenv.Env, opts userOptions) error { func userDefaultFormatter(env *execenv.Env, users []*cache.IdentityExcerpt) error { for _, user := range users { env.Out.Printf("%s %s\n", - colors.Cyan(user.Id.Human()), + colors.Cyan(user.Id().Human()), user.DisplayName(), ) } diff --git a/commands/user/user_adopt.go b/commands/user/user_adopt.go index f5944053..30fdb442 100644 --- a/commands/user/user_adopt.go +++ b/commands/user/user_adopt.go @@ -27,7 +27,7 @@ func newUserAdoptCommand() *cobra.Command { func runUserAdopt(env *execenv.Env, args []string) error { prefix := args[0] - i, err := env.Backend.ResolveIdentityPrefix(prefix) + i, err := env.Backend.Identities().ResolvePrefix(prefix) if err != nil { return err } diff --git a/commands/user/user_new.go b/commands/user/user_new.go index d7224512..7b287492 100644 --- a/commands/user/user_new.go +++ b/commands/user/user_new.go @@ -69,7 +69,7 @@ func runUserNew(env *execenv.Env, opts userNewOptions) error { } } - id, err := env.Backend.NewIdentityRaw(opts.name, opts.email, "", opts.avatarURL, nil, nil) + id, err := env.Backend.Identities().NewRaw(opts.name, opts.email, "", opts.avatarURL, nil, nil) if err != nil { return err } diff --git a/commands/user/user_show.go b/commands/user/user_show.go index 36c09e8e..225d0ef4 100644 --- a/commands/user/user_show.go +++ b/commands/user/user_show.go @@ -49,7 +49,7 @@ func runUserShow(env *execenv.Env, opts userShowOptions, args []string) error { var id *cache.IdentityCache var err error if len(args) == 1 { - id, err = env.Backend.ResolveIdentityPrefix(args[0]) + id, err = env.Backend.Identities().ResolvePrefix(args[0]) } else { id, err = env.Backend.GetUserIdentity() } diff --git a/commands/webui.go b/commands/webui.go index 5fe66aa7..e1e0fc2b 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -105,7 +105,7 @@ func runWebUI(env *execenv.Env, opts webUIOptions) error { } mrc := cache.NewMultiRepoCache() - _, err := mrc.RegisterDefaultRepository(env.Repo) + _, _, err := mrc.RegisterDefaultRepository(env.Repo) if err != nil { return err } diff --git a/entities/bug/bug.go b/entities/bug/bug.go index 4c4a9a74..271e7dbd 100644 --- a/entities/bug/bug.go +++ b/entities/bug/bug.go @@ -27,15 +27,6 @@ var def = dag.Definition{ FormatVersion: formatVersion, } -var Actions = dag.Actions[*Bug]{ - Wrap: wrapper, - New: NewBug, - Read: Read, - ReadWithResolver: ReadWithResolver, - ReadAll: ReadAll, - ListLocalIds: ListLocalIds, -} - var ClockLoader = dag.ClockLoader(def) type Interface interface { @@ -75,12 +66,12 @@ func ReadWithResolver(repo repository.ClockedRepo, resolvers entity.Resolvers, i } // ReadAll read and parse all local bugs -func ReadAll(repo repository.ClockedRepo) <-chan dag.StreamedEntity[*Bug] { +func ReadAll(repo repository.ClockedRepo) <-chan entity.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 dag.StreamedEntity[*Bug] { +func ReadAllWithResolver(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan entity.StreamedEntity[*Bug] { return dag.ReadAll(def, wrapper, repo, resolvers) } diff --git a/entities/bug/resolver.go b/entities/bug/resolver.go index e7beb0e4..b0a05917 100644 --- a/entities/bug/resolver.go +++ b/entities/bug/resolver.go @@ -16,6 +16,6 @@ func NewSimpleResolver(repo repository.ClockedRepo) *SimpleResolver { return &SimpleResolver{repo: repo} } -func (r *SimpleResolver) Resolve(id entity.Id) (entity.Interface, error) { +func (r *SimpleResolver) Resolve(id entity.Id) (entity.Resolved, error) { return Read(r.repo, id) } diff --git a/entities/identity/common.go b/entities/identity/common.go index ba35792c..88e30e33 100644 --- a/entities/identity/common.go +++ b/entities/identity/common.go @@ -3,14 +3,8 @@ package identity import ( "encoding/json" "fmt" - - "github.com/MichaelMure/git-bug/entity" ) -func NewErrMultipleMatch(matching []entity.Id) *entity.ErrMultipleMatch { - return entity.NewErrMultipleMatch("identity", matching) -} - // Custom unmarshaling function to allow package user to delegate // the decoding of an Identity and distinguish between an Identity // and a Bare. diff --git a/entities/identity/identity.go b/entities/identity/identity.go index 9bc53aed..572d2c14 100644 --- a/entities/identity/identity.go +++ b/entities/identity/identity.go @@ -25,10 +25,6 @@ var ErrNoIdentitySet = errors.New("No identity is set.\n" + "\"git bug user new\" or adopted with \"git bug user adopt\"") var ErrMultipleIdentitiesSet = errors.New("multiple user identities set") -func NewErrMultipleMatchIdentity(matching []entity.Id) *entity.ErrMultipleMatch { - return entity.NewErrMultipleMatch("identity", matching) -} - var _ Interface = &Identity{} var _ entity.Interface = &Identity{} @@ -174,7 +170,7 @@ func RemoveIdentity(repo repository.ClockedRepo, id entity.Id) error { return err } if len(refs) > 1 { - return NewErrMultipleMatchIdentity(entity.RefsToIds(refs)) + return entity.NewErrMultipleMatch("identity", entity.RefsToIds(refs)) } if len(refs) == 1 { // we have the identity locally @@ -193,7 +189,7 @@ func RemoveIdentity(repo repository.ClockedRepo, id entity.Id) error { return err } if len(remoteRefs) > 1 { - return NewErrMultipleMatchIdentity(entity.RefsToIds(refs)) + return entity.NewErrMultipleMatch("identity", entity.RefsToIds(refs)) } if len(remoteRefs) == 1 { // found the identity in a remote @@ -215,44 +211,39 @@ func RemoveIdentity(repo repository.ClockedRepo, id entity.Id) error { return nil } -type StreamedIdentity struct { - Identity *Identity - Err error -} - // ReadAllLocal read and parse all local Identity -func ReadAllLocal(repo repository.ClockedRepo) <-chan StreamedIdentity { +func ReadAllLocal(repo repository.ClockedRepo) <-chan entity.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 StreamedIdentity { +func ReadAllRemote(repo repository.ClockedRepo, remote string) <-chan entity.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 StreamedIdentity { - out := make(chan StreamedIdentity) +func readAll(repo repository.ClockedRepo, refPrefix string) <-chan entity.StreamedEntity[*Identity] { + out := make(chan entity.StreamedEntity[*Identity]) go func() { defer close(out) refs, err := repo.ListRefs(refPrefix) if err != nil { - out <- StreamedIdentity{Err: err} + out <- entity.StreamedEntity[*Identity]{Err: err} return } for _, ref := range refs { - b, err := read(repo, ref) + i, err := read(repo, ref) if err != nil { - out <- StreamedIdentity{Err: err} + out <- entity.StreamedEntity[*Identity]{Err: err} return } - out <- StreamedIdentity{Identity: b} + out <- entity.StreamedEntity[*Identity]{Entity: i} } }() @@ -308,7 +299,7 @@ func (i *Identity) Mutate(repo repository.RepoClock, f func(orig *Mutator)) erro return nil } -// Write the identity into the Repository. In particular, this ensure that +// Commit write the identity into the Repository. In particular, this ensures that // the Id is properly set. func (i *Identity) Commit(repo repository.ClockedRepo) error { if !i.NeedCommit() { diff --git a/entities/identity/identity_actions_test.go b/entities/identity/identity_actions_test.go index 351fb7a4..e9626cb9 100644 --- a/entities/identity/identity_actions_test.go +++ b/entities/identity/identity_actions_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" ) @@ -145,13 +146,13 @@ func TestIdentityPushPull(t *testing.T) { } } -func allIdentities(t testing.TB, identities <-chan StreamedIdentity) []*Identity { +func allIdentities(t testing.TB, identities <-chan entity.StreamedEntity[*Identity]) []*Identity { var result []*Identity for streamed := range identities { if streamed.Err != nil { t.Fatal(streamed.Err) } - result = append(result, streamed.Identity) + result = append(result, streamed.Entity) } return result } diff --git a/entities/identity/resolver.go b/entities/identity/resolver.go index 5468a8f8..a4b676f3 100644 --- a/entities/identity/resolver.go +++ b/entities/identity/resolver.go @@ -16,19 +16,6 @@ func NewSimpleResolver(repo repository.Repo) *SimpleResolver { return &SimpleResolver{repo: repo} } -func (r *SimpleResolver) Resolve(id entity.Id) (entity.Interface, error) { +func (r *SimpleResolver) Resolve(id entity.Id) (entity.Resolved, error) { return ReadLocal(r.repo, id) } - -var _ entity.Resolver = &StubResolver{} - -// StubResolver is a Resolver that doesn't load anything, only returning IdentityStub instances -type StubResolver struct{} - -func NewStubResolver() *StubResolver { - return &StubResolver{} -} - -func (s *StubResolver) Resolve(id entity.Id) (entity.Interface, error) { - return &IdentityStub{id: id}, nil -} diff --git a/entity/dag/entity.go b/entity/dag/entity.go index 03b97aa0..2028e1b4 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -33,19 +33,6 @@ type Definition struct { FormatVersion uint } -type Actions[EntityT entity.Interface] struct { - Wrap func(e *Entity) EntityT - New func() EntityT - Read func(repo repository.ClockedRepo, id entity.Id) (EntityT, error) - ReadWithResolver func(repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (EntityT, error) - ReadAll func(repo repository.ClockedRepo) <-chan StreamedEntity[EntityT] - ListLocalIds func(repo repository.Repo) ([]entity.Id, error) - Fetch func(repo repository.Repo, remote string) (string, error) - Push func(repo repository.Repo, remote string) (string, error) - Pull func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) error - MergeAll func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult -} - // Entity is a data structure stored in a chain of git objects, supporting actions like Push, Pull and Merge. type Entity struct { // A Lamport clock is a logical clock that allow to order event @@ -96,6 +83,9 @@ 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) { rootHash, err := repo.ResolveRef(ref) + if err == repository.ErrNotFound { + return *new(EntityT), entity.NewErrNotFound(def.Typename) + } if err != nil { return *new(EntityT), err } @@ -260,6 +250,9 @@ func read[EntityT entity.Interface](def Definition, wrapper func(e *Entity) Enti // operation blobs can be implemented instead. func readClockNoCheck(def Definition, repo repository.ClockedRepo, ref string) error { rootHash, err := repo.ResolveRef(ref) + if err == repository.ErrNotFound { + return entity.NewErrNotFound(def.Typename) + } if err != nil { return err } @@ -306,14 +299,9 @@ func readClockNoCheck(def Definition, repo repository.ClockedRepo, ref string) e return nil } -type StreamedEntity[EntityT entity.Interface] struct { - Entity EntityT - Err error -} - // 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 StreamedEntity[EntityT] { - out := make(chan StreamedEntity[EntityT]) +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]) go func() { defer close(out) @@ -322,7 +310,7 @@ func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) E refs, err := repo.ListRefs(refPrefix) if err != nil { - out <- StreamedEntity[EntityT]{Err: err} + out <- entity.StreamedEntity[EntityT]{Err: err} return } @@ -330,11 +318,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 <- StreamedEntity[EntityT]{Err: err} + out <- entity.StreamedEntity[EntityT]{Err: err} return } - out <- StreamedEntity[EntityT]{Entity: e} + out <- entity.StreamedEntity[EntityT]{Entity: e} } }() diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go index d93059de..fd219644 100644 --- a/entity/dag/entity_actions_test.go +++ b/entity/dag/entity_actions_test.go @@ -11,7 +11,7 @@ import ( "github.com/MichaelMure/git-bug/repository" ) -func allEntities(t testing.TB, bugs <-chan StreamedEntity[*Foo]) []*Foo { +func allEntities(t testing.TB, bugs <-chan entity.StreamedEntity[*Foo]) []*Foo { t.Helper() var result []*Foo diff --git a/entity/err.go b/entity/err.go index 9d7c266e..4453d36e 100644 --- a/entity/err.go +++ b/entity/err.go @@ -5,6 +5,8 @@ import ( "strings" ) +// ErrNotFound is to be returned when an entity, item, element is +// not found. type ErrNotFound struct { typename string } @@ -22,13 +24,15 @@ func IsErrNotFound(err error) bool { return ok } +// ErrMultipleMatch is to be returned when more than one entity, item, element +// is found, where only one was expected. type ErrMultipleMatch struct { - entityType string - Matching []Id + typename string + Matching []Id } -func NewErrMultipleMatch(entityType string, matching []Id) *ErrMultipleMatch { - return &ErrMultipleMatch{entityType: entityType, Matching: matching} +func NewErrMultipleMatch(typename string, matching []Id) *ErrMultipleMatch { + return &ErrMultipleMatch{typename: typename, Matching: matching} } func (e ErrMultipleMatch) Error() string { @@ -39,7 +43,7 @@ func (e ErrMultipleMatch) Error() string { } return fmt.Sprintf("Multiple matching %s found:\n%s", - e.entityType, + e.typename, strings.Join(matching, "\n")) } @@ -48,6 +52,8 @@ func IsErrMultipleMatch(err error) bool { 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 diff --git a/entity/interface.go b/entity/interface.go index 572ba602..3035ac88 100644 --- a/entity/interface.go +++ b/entity/interface.go @@ -12,15 +12,3 @@ type Interface interface { // Validate check if the Entity data is valid Validate() error } - -// type Commitable interface { -// Interface -// NeedCommit() bool -// CommitAsNeeded(repo repository.ClockedRepo) error -// Commit(repo repository.ClockedRepo) error -// } - -// -// type Operation interface { -// -// } diff --git a/entity/resolver.go b/entity/resolver.go index 9cacbf00..bd16b901 100644 --- a/entity/resolver.go +++ b/entity/resolver.go @@ -5,16 +5,23 @@ import ( "sync" ) +// 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) (Interface, error) + Resolve(id Id) (Resolved, error) } // Resolvers is a collection of Resolver, for different type of Entity -type Resolvers map[Interface]Resolver +type Resolvers map[Resolved]Resolver // Resolve use the appropriate sub-resolver for the given type and find the Entity matching the Id. -func Resolve[T Interface](rs Resolvers, id Id) (T, error) { +func Resolve[T Resolved](rs Resolvers, id Id) (T, error) { var zero T for t, resolver := range rs { switch t.(type) { @@ -35,17 +42,17 @@ var _ Resolver = &CachedResolver{} type CachedResolver struct { resolver Resolver mu sync.RWMutex - entities map[Id]Interface + entities map[Id]Resolved } func NewCachedResolver(resolver Resolver) *CachedResolver { return &CachedResolver{ resolver: resolver, - entities: make(map[Id]Interface), + entities: make(map[Id]Resolved), } } -func (c *CachedResolver) Resolve(id Id) (Interface, error) { +func (c *CachedResolver) Resolve(id Id) (Resolved, error) { c.mu.RLock() if i, ok := c.entities[id]; ok { c.mu.RUnlock() @@ -64,18 +71,18 @@ func (c *CachedResolver) Resolve(id Id) (Interface, error) { return i, nil } -var _ Resolver = ResolverFunc[Interface](nil) +var _ Resolver = ResolverFunc[Resolved](nil) // ResolverFunc is a helper to morph a function resolver into a Resolver -type ResolverFunc[T Interface] func(id Id) (T, error) +type ResolverFunc[EntityT Resolved] func(id Id) (EntityT, error) -func (fn ResolverFunc[T]) Resolve(id Id) (Interface, error) { +func (fn ResolverFunc[EntityT]) Resolve(id Id) (Resolved, error) { return fn(id) } // MakeResolver create a resolver able to return the given entities. -func MakeResolver(entities ...Interface) Resolver { - return ResolverFunc[Interface](func(id Id) (Interface, error) { +func MakeResolver(entities ...Resolved) Resolver { + return ResolverFunc[Resolved](func(id Id) (Resolved, error) { for _, entity := range entities { if entity.Id() == id { return entity, nil diff --git a/entity/streamed.go b/entity/streamed.go new file mode 100644 index 00000000..789224a3 --- /dev/null +++ b/entity/streamed.go @@ -0,0 +1,6 @@ +package entity + +type StreamedEntity[EntityT Interface] struct { + Entity EntityT + Err error +} diff --git a/termui/bug_table.go b/termui/bug_table.go index f3f8b2f3..9db13ada 100644 --- a/termui/bug_table.go +++ b/termui/bug_table.go @@ -239,7 +239,7 @@ func (bt *bugTable) disable(g *gocui.Gui) error { func (bt *bugTable) paginate(max int) error { var err error - bt.allIds, err = bt.repo.QueryBugs(bt.query) + bt.allIds, err = bt.repo.Bugs().Query(bt.query) if err != nil { return err } @@ -265,7 +265,7 @@ func (bt *bugTable) doPaginate(max int) error { bt.excerpts = make([]*cache.BugExcerpt, len(ids)) for i, id := range ids { - excerpt, err := bt.repo.ResolveBugExcerpt(id) + excerpt, err := bt.repo.Bugs().ResolveExcerpt(id) if err != nil { return err } @@ -319,12 +319,12 @@ func (bt *bugTable) render(v *gocui.View, maxX int) { labelsTxt.WriteString(lc256.Unescape()) } - author, err := bt.repo.ResolveIdentityExcerpt(excerpt.AuthorId) + author, err := bt.repo.Identities().ResolveExcerpt(excerpt.AuthorId) if err != nil { panic(err) } - id := text.LeftPadMaxLine(excerpt.Id.Human(), columnWidths["id"], 0) + id := text.LeftPadMaxLine(excerpt.Id().Human(), columnWidths["id"], 0) status := text.LeftPadMaxLine(excerpt.Status.String(), columnWidths["status"], 0) labels := text.TruncateMax(labelsTxt.String(), minInt(columnWidths["title"]-2, 10)) title := text.LeftPadMaxLine(strings.TrimSpace(excerpt.Title), columnWidths["title"]-text.Len(labels), 0) @@ -451,8 +451,8 @@ func (bt *bugTable) openBug(g *gocui.Gui, v *gocui.View) error { // There are no open bugs, just do nothing return nil } - id := bt.excerpts[bt.selectCursor].Id - b, err := bt.repo.ResolveBug(id) + id := bt.excerpts[bt.selectCursor].Id() + b, err := bt.repo.Bugs().Resolve(id) if err != nil { return err } diff --git a/termui/label_select.go b/termui/label_select.go index 2282583d..6721165e 100644 --- a/termui/label_select.go +++ b/termui/label_select.go @@ -37,7 +37,7 @@ func newLabelSelect() *labelSelect { func (ls *labelSelect) SetBug(cache *cache.RepoCache, bug *cache.BugCache) { ls.cache = cache ls.bug = bug - ls.labels = cache.ValidLabels() + ls.labels = cache.Bugs().ValidLabels() // Find which labels are currently applied to the bug bugLabels := bug.Snapshot().Labels diff --git a/termui/termui.go b/termui/termui.go index 4dd6e27d..79577ba9 100644 --- a/termui/termui.go +++ b/termui/termui.go @@ -200,7 +200,7 @@ func newBugWithEditor(repo *cache.RepoCache) error { return errTerminateMainloop } else { - b, _, err = repo.NewBug( + b, _, err = repo.Bugs().New( text.CleanupOneLine(title), text.Cleanup(message), ) |