diff options
78 files changed, 1041 insertions, 510 deletions
diff --git a/bridge/gitlab/event.go b/bridge/gitlab/event.go index 483af921..0653269d 100644 --- a/bridge/gitlab/event.go +++ b/bridge/gitlab/event.go @@ -40,6 +40,7 @@ const ( EventRemoveLabel EventMentionedInIssue EventMentionedInMergeRequest + EventMentionedInCommit ) var _ Event = &NoteEvent{} @@ -97,6 +98,9 @@ func (n NoteEvent) Kind() EventKind { case strings.HasPrefix(n.Body, "mentioned in merge request"): return EventMentionedInMergeRequest + case strings.HasPrefix(n.Body, "mentioned in commit"): + return EventMentionedInCommit + default: return EventUnknown } diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go index 5947fb60..e4330b4c 100644 --- a/bridge/gitlab/import.go +++ b/bridge/gitlab/import.go @@ -324,7 +324,8 @@ func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCa EventLocked, EventUnlocked, EventMentionedInIssue, - EventMentionedInMergeRequest: + EventMentionedInMergeRequest, + EventMentionedInCommit: return nil diff --git a/cache/bug_subcache.go b/cache/bug_subcache.go index 920fe1dc..21c9a6d2 100644 --- a/cache/bug_subcache.go +++ b/cache/bug_subcache.go @@ -38,6 +38,7 @@ func NewRepoCacheBug(repo repository.ClockedRepo, ReadWithResolver: bug.ReadWithResolver, ReadAllWithResolver: bug.ReadAllWithResolver, Remove: bug.Remove, + RemoveAll: bug.RemoveAll, MergeAll: bug.MergeAll, } diff --git a/cache/identity_subcache.go b/cache/identity_subcache.go index f862ca8b..05a91358 100644 --- a/cache/identity_subcache.go +++ b/cache/identity_subcache.go @@ -39,7 +39,8 @@ func NewRepoCacheIdentity(repo repository.ClockedRepo, ReadAllWithResolver: func(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan entity.StreamedEntity[*identity.Identity] { return identity.ReadAllLocal(repo) }, - Remove: identity.RemoveIdentity, + Remove: identity.Remove, + RemoveAll: identity.RemoveAll, MergeAll: func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult { return identity.MergeAll(repo, remote) }, diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 99e9abbd..9e45d1f1 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -30,8 +30,9 @@ var _ repository.RepoKeyring = &RepoCache{} type cacheMgmt interface { Typename() string Load() error - Build() error + Build() <-chan BuildEvent SetCacheSize(size int) + RemoveAll() error MergeAll(remote string) <-chan entity.MergeResult GetNamespace() string Close() error @@ -212,6 +213,7 @@ const ( BuildEventCacheIsBuilt BuildEventRemoveLock BuildEventStarted + BuildEventProgress BuildEventFinished ) @@ -223,6 +225,10 @@ type BuildEvent struct { Typename string // Event is the type of the event. Event BuildEventType + // Total is the total number of element being built. Set if Event is BuildEventStarted. + Total int64 + // Progress is the current count of processed element. Set if Event is BuildEventProgress. + Progress int64 } func (c *RepoCache) buildCache(events chan BuildEvent) { @@ -233,23 +239,13 @@ func (c *RepoCache) buildCache(events chan BuildEvent) { wg.Add(1) go func(subcache cacheMgmt) { defer wg.Done() - events <- BuildEvent{ - Typename: subcache.Typename(), - Event: BuildEventStarted, - } - err := subcache.Build() - if err != nil { - events <- BuildEvent{ - Typename: subcache.Typename(), - Err: err, + buildEvents := subcache.Build() + for buildEvent := range buildEvents { + events <- buildEvent + if buildEvent.Err != nil { + return } - return - } - - events <- BuildEvent{ - Typename: subcache.Typename(), - Event: BuildEventFinished, } }(subcache) } diff --git a/cache/repo_cache_common.go b/cache/repo_cache_common.go index f768b8e2..759536bd 100644 --- a/cache/repo_cache_common.go +++ b/cache/repo_cache_common.go @@ -3,12 +3,12 @@ package cache import ( "sync" - "github.com/go-git/go-billy/v5" "github.com/pkg/errors" "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/multierr" ) func (c *RepoCache) Name() string { @@ -56,7 +56,7 @@ func (c *RepoCache) GetRemotes() (map[string]string, error) { } // LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug -func (c *RepoCache) LocalStorage() billy.Filesystem { +func (c *RepoCache) LocalStorage() repository.LocalStorage { return c.repo.LocalStorage() } @@ -82,6 +82,15 @@ func (c *RepoCache) Fetch(remote string) (string, error) { return c.repo.FetchRefs(remote, prefixes...) } +// RemoveAll deletes all entities from the cache and the disk. +func (c *RepoCache) RemoveAll() error { + var errWait multierr.ErrWaitGroup + for _, mgmt := range c.subcaches { + errWait.Go(mgmt.RemoveAll) + } + return errWait.Wait() +} + // MergeAll will merge all the available remote bug and identities func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { out := make(chan entity.MergeResult) @@ -163,6 +172,19 @@ func (c *RepoCache) SetUserIdentity(i *IdentityCache) error { return nil } +func (c *RepoCache) ClearUserIdentity() error { + c.muUserIdentity.Lock() + defer c.muUserIdentity.Unlock() + + err := identity.ClearUserIdentity(c.repo) + if err != nil { + return err + } + + c.userIdentityId = "" + return nil +} + func (c *RepoCache) GetUserIdentity() (*IdentityCache, error) { c.muUserIdentity.RLock() if c.userIdentityId != "" { diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go index 07a3fee8..3c11220d 100644 --- a/cache/repo_cache_test.go +++ b/cache/repo_cache_test.go @@ -135,6 +135,39 @@ func TestCache(t *testing.T) { _, err = cache.Bugs().ResolvePrefix(bug1.Id().String()[:10]) require.NoError(t, err) + require.Len(t, cache.bugs.cached, 1) + require.Len(t, cache.bugs.excerpts, 2) + require.Len(t, cache.identities.cached, 1) + require.Len(t, cache.identities.excerpts, 2) + require.Equal(t, uint64(2), indexCount(t, identity.Namespace)) + require.Equal(t, uint64(2), indexCount(t, bug.Namespace)) + + // Remove + RemoveAll + err = cache.Identities().Remove(iden1.Id().String()[:10]) + require.NoError(t, err) + err = cache.Bugs().Remove(bug1.Id().String()[:10]) + require.NoError(t, err) + require.Len(t, cache.bugs.cached, 0) + require.Len(t, cache.bugs.excerpts, 1) + require.Len(t, cache.identities.cached, 0) + require.Len(t, cache.identities.excerpts, 1) + require.Equal(t, uint64(1), indexCount(t, identity.Namespace)) + require.Equal(t, uint64(1), indexCount(t, bug.Namespace)) + + _, err = cache.Identities().New("René Descartes", "rene@descartes.fr") + require.NoError(t, err) + _, _, err = cache.Bugs().NewRaw(iden2, time.Now().Unix(), "title", "message", nil, nil) + require.NoError(t, err) + + err = cache.RemoveAll() + require.NoError(t, err) + require.Len(t, cache.bugs.cached, 0) + require.Len(t, cache.bugs.excerpts, 0) + require.Len(t, cache.identities.cached, 0) + require.Len(t, cache.identities.excerpts, 0) + require.Equal(t, uint64(0), indexCount(t, identity.Namespace)) + require.Equal(t, uint64(0), indexCount(t, bug.Namespace)) + // Close require.NoError(t, cache.Close()) require.Empty(t, cache.bugs.cached) diff --git a/cache/subcache.go b/cache/subcache.go index 7b599b10..09e53c23 100644 --- a/cache/subcache.go +++ b/cache/subcache.go @@ -34,6 +34,7 @@ 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 + RemoveAll func(repo repository.ClockedRepo) error MergeAll func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult } @@ -185,51 +186,98 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) write() error { return f.Close() } -func (sc *SubCache[EntityT, ExcerptT, CacheT]) Build() error { - sc.excerpts = make(map[entity.Id]ExcerptT) +func (sc *SubCache[EntityT, ExcerptT, CacheT]) Build() <-chan BuildEvent { + out := make(chan BuildEvent) - allEntities := sc.actions.ReadAllWithResolver(sc.repo, sc.resolvers()) + go func() { + defer close(out) - index, err := sc.repo.GetIndex(sc.namespace) - if err != nil { - return err - } + out <- BuildEvent{ + Typename: sc.typename, + Event: BuildEventStarted, + } - // wipe the index just to be sure - err = index.Clear() - if err != nil { - return err - } + sc.excerpts = make(map[entity.Id]ExcerptT) - indexer, indexEnd := index.IndexBatch() + allEntities := sc.actions.ReadAllWithResolver(sc.repo, sc.resolvers()) - for e := range allEntities { - if e.Err != nil { - return e.Err + index, err := sc.repo.GetIndex(sc.namespace) + if err != nil { + out <- BuildEvent{ + Typename: sc.typename, + Err: err, + } + return + } + + // wipe the index just to be sure + err = index.Clear() + if err != nil { + out <- BuildEvent{ + Typename: sc.typename, + Err: err, + } + return } - cached := sc.makeCached(e.Entity, sc.entityUpdated) - sc.excerpts[e.Entity.Id()] = sc.makeExcerpt(cached) - // might as well keep them in memory - sc.cached[e.Entity.Id()] = cached + indexer, indexEnd := index.IndexBatch() + + for e := range allEntities { + if e.Err != nil { + out <- BuildEvent{ + Typename: sc.typename, + Err: e.Err, + } + return + } - indexData := sc.makeIndexData(cached) - if err := indexer(e.Entity.Id().String(), indexData); err != nil { - return err + cached := sc.makeCached(e.Entity, sc.entityUpdated) + sc.excerpts[e.Entity.Id()] = sc.makeExcerpt(cached) + // might as well keep them in memory + sc.cached[e.Entity.Id()] = cached + + indexData := sc.makeIndexData(cached) + if err := indexer(e.Entity.Id().String(), indexData); err != nil { + out <- BuildEvent{ + Typename: sc.typename, + Err: err, + } + return + } + + out <- BuildEvent{ + Typename: sc.typename, + Event: BuildEventProgress, + Progress: e.CurrentEntity, + Total: e.TotalEntities, + } } - } - err = indexEnd() - if err != nil { - return err - } + err = indexEnd() + if err != nil { + out <- BuildEvent{ + Typename: sc.typename, + Err: err, + } + return + } - err = sc.write() - if err != nil { - return err - } + err = sc.write() + if err != nil { + out <- BuildEvent{ + Typename: sc.typename, + Err: err, + } + return + } - return nil + out <- BuildEvent{ + Typename: sc.typename, + Event: BuildEventFinished, + } + }() + + return out } func (sc *SubCache[EntityT, ExcerptT, CacheT]) SetCacheSize(size int) { @@ -399,7 +447,49 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Remove(prefix string) error { delete(sc.excerpts, e.Id()) sc.lru.Remove(e.Id()) + index, err := sc.repo.GetIndex(sc.namespace) + if err != nil { + sc.mu.Unlock() + return err + } + + err = index.Remove(e.Id().String()) + sc.mu.Unlock() + if err != nil { + return err + } + + return sc.write() +} + +func (sc *SubCache[EntityT, ExcerptT, CacheT]) RemoveAll() error { + sc.mu.Lock() + + err := sc.actions.RemoveAll(sc.repo) + if err != nil { + sc.mu.Unlock() + return err + } + + for id, _ := range sc.cached { + delete(sc.cached, id) + sc.lru.Remove(id) + } + for id, _ := range sc.excerpts { + delete(sc.excerpts, id) + } + + index, err := sc.repo.GetIndex(sc.namespace) + if err != nil { + sc.mu.Unlock() + return err + } + + err = index.Clear() sc.mu.Unlock() + if err != nil { + return err + } return sc.write() } diff --git a/commands/bridge/bridge.go b/commands/bridge/bridge.go index 980a38e2..9f7c5e50 100644 --- a/commands/bridge/bridge.go +++ b/commands/bridge/bridge.go @@ -7,9 +7,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func NewBridgeCommand() *cobra.Command { - env := execenv.NewEnv() - +func NewBridgeCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "bridge", Short: "List bridges to other bug trackers", @@ -20,11 +18,11 @@ func NewBridgeCommand() *cobra.Command { Args: cobra.NoArgs, } - cmd.AddCommand(newBridgeAuthCommand()) - cmd.AddCommand(newBridgeNewCommand()) - cmd.AddCommand(newBridgePullCommand()) - cmd.AddCommand(newBridgePushCommand()) - cmd.AddCommand(newBridgeRm()) + cmd.AddCommand(newBridgeAuthCommand(env)) + cmd.AddCommand(newBridgeNewCommand(env)) + cmd.AddCommand(newBridgePullCommand(env)) + cmd.AddCommand(newBridgePushCommand(env)) + cmd.AddCommand(newBridgeRm(env)) return cmd } diff --git a/commands/bridge/bridge_auth.go b/commands/bridge/bridge_auth.go index 52e063e6..f27004e0 100644 --- a/commands/bridge/bridge_auth.go +++ b/commands/bridge/bridge_auth.go @@ -12,9 +12,7 @@ import ( "github.com/MichaelMure/git-bug/util/colors" ) -func newBridgeAuthCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBridgeAuthCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "auth", Short: "List all known bridge authentication credentials", @@ -25,9 +23,9 @@ func newBridgeAuthCommand() *cobra.Command { Args: cobra.NoArgs, } - cmd.AddCommand(newBridgeAuthAddTokenCommand()) - cmd.AddCommand(newBridgeAuthRm()) - cmd.AddCommand(newBridgeAuthShow()) + cmd.AddCommand(newBridgeAuthAddTokenCommand(env)) + cmd.AddCommand(newBridgeAuthRm(env)) + cmd.AddCommand(newBridgeAuthShow(env)) return cmd } diff --git a/commands/bridge/bridge_auth_addtoken.go b/commands/bridge/bridge_auth_addtoken.go index 2992fa63..5af27728 100644 --- a/commands/bridge/bridge_auth_addtoken.go +++ b/commands/bridge/bridge_auth_addtoken.go @@ -24,8 +24,7 @@ type bridgeAuthAddTokenOptions struct { user string } -func newBridgeAuthAddTokenCommand() *cobra.Command { - env := execenv.NewEnv() +func newBridgeAuthAddTokenCommand(env *execenv.Env) *cobra.Command { options := bridgeAuthAddTokenOptions{} cmd := &cobra.Command{ diff --git a/commands/bridge/bridge_auth_rm.go b/commands/bridge/bridge_auth_rm.go index d58ca63e..33253e26 100644 --- a/commands/bridge/bridge_auth_rm.go +++ b/commands/bridge/bridge_auth_rm.go @@ -8,9 +8,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newBridgeAuthRm() *cobra.Command { - env := execenv.NewEnv() - +func newBridgeAuthRm(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "rm BRIDGE_ID", Short: "Remove a credential", diff --git a/commands/bridge/bridge_auth_show.go b/commands/bridge/bridge_auth_show.go index d373273d..25c517d3 100644 --- a/commands/bridge/bridge_auth_show.go +++ b/commands/bridge/bridge_auth_show.go @@ -13,9 +13,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newBridgeAuthShow() *cobra.Command { - env := execenv.NewEnv() - +func newBridgeAuthShow(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "show", Short: "Display an authentication credential", diff --git a/commands/bridge/bridge_new.go b/commands/bridge/bridge_new.go index 2c51d9ef..07a555da 100644 --- a/commands/bridge/bridge_new.go +++ b/commands/bridge/bridge_new.go @@ -26,8 +26,7 @@ type bridgeNewOptions struct { nonInteractive bool } -func newBridgeNewCommand() *cobra.Command { - env := execenv.NewEnv() +func newBridgeNewCommand(env *execenv.Env) *cobra.Command { options := bridgeNewOptions{} cmd := &cobra.Command{ diff --git a/commands/bridge/bridge_pull.go b/commands/bridge/bridge_pull.go index d1fc279a..03f4929a 100644 --- a/commands/bridge/bridge_pull.go +++ b/commands/bridge/bridge_pull.go @@ -23,8 +23,7 @@ type bridgePullOptions struct { noResume bool } -func newBridgePullCommand() *cobra.Command { - env := execenv.NewEnv() +func newBridgePullCommand(env *execenv.Env) *cobra.Command { options := bridgePullOptions{} cmd := &cobra.Command{ diff --git a/commands/bridge/bridge_push.go b/commands/bridge/bridge_push.go index 51baed4d..08f9b872 100644 --- a/commands/bridge/bridge_push.go +++ b/commands/bridge/bridge_push.go @@ -15,9 +15,7 @@ import ( "github.com/MichaelMure/git-bug/util/interrupt" ) -func newBridgePushCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBridgePushCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "push [NAME]", Short: "Push updates to remote bug tracker", diff --git a/commands/bridge/bridge_rm.go b/commands/bridge/bridge_rm.go index 5d8d23c5..f6279ade 100644 --- a/commands/bridge/bridge_rm.go +++ b/commands/bridge/bridge_rm.go @@ -8,9 +8,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newBridgeRm() *cobra.Command { - env := execenv.NewEnv() - +func newBridgeRm(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "rm NAME", Short: "Delete a configured bridge", diff --git a/commands/bug/bug.go b/commands/bug/bug.go index a5ce11ed..55a1fa59 100644 --- a/commands/bug/bug.go +++ b/commands/bug/bug.go @@ -15,26 +15,27 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entities/common" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/query" "github.com/MichaelMure/git-bug/util/colors" ) type bugOptions struct { - statusQuery []string - authorQuery []string - metadataQuery []string - participantQuery []string - actorQuery []string - labelQuery []string - titleQuery []string - noQuery []string - sortBy string - sortDirection string - outputFormat string + statusQuery []string + authorQuery []string + metadataQuery []string + participantQuery []string + actorQuery []string + labelQuery []string + titleQuery []string + noQuery []string + sortBy string + sortDirection string + outputFormat string + outputFormatChanged bool } -func NewBugCommand() *cobra.Command { - env := execenv.NewEnv() +func NewBugCommand(env *execenv.Env) *cobra.Command { options := bugOptions{} cmd := &cobra.Command{ @@ -57,6 +58,7 @@ git bug status:open --by creation "foo bar" baz `, PreRunE: execenv.LoadBackend(env), RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error { + options.outputFormatChanged = cmd.Flags().Changed("format") return runBug(env, options, args) }), ValidArgsFunction: completion.Ls(env), @@ -94,9 +96,9 @@ git bug status:open --by creation "foo bar" baz "Select the sorting direction. Valid values are [asc,desc]") cmd.RegisterFlagCompletionFunc("direction", completion.From([]string{"asc", "desc"})) flags.StringVarP(&options.outputFormat, "format", "f", "default", - "Select the output formatting style. Valid values are [default,plain,compact,id,json,org-mode]") + "Select the output formatting style. Valid values are [default,plain,id,json,org-mode]") cmd.RegisterFlagCompletionFunc("format", - completion.From([]string{"default", "plain", "compact", "id", "json", "org-mode"})) + completion.From([]string{"default", "plain", "id", "json", "org-mode"})) const selectGroup = "select" cmd.AddGroup(&cobra.Group{ID: selectGroup, Title: "Implicit selection"}) @@ -106,16 +108,16 @@ git bug status:open --by creation "foo bar" baz child.GroupID = groupID } - addCmdWithGroup(newBugDeselectCommand(), selectGroup) - addCmdWithGroup(newBugSelectCommand(), selectGroup) + addCmdWithGroup(newBugDeselectCommand(env), selectGroup) + addCmdWithGroup(newBugSelectCommand(env), selectGroup) - cmd.AddCommand(newBugCommentCommand()) - cmd.AddCommand(newBugLabelCommand()) - cmd.AddCommand(newBugNewCommand()) - cmd.AddCommand(newBugRmCommand()) - cmd.AddCommand(newBugShowCommand()) - cmd.AddCommand(newBugStatusCommand()) - cmd.AddCommand(newBugTitleCommand()) + cmd.AddCommand(newBugCommentCommand(env)) + cmd.AddCommand(newBugLabelCommand(env)) + cmd.AddCommand(newBugNewCommand(env)) + cmd.AddCommand(newBugRmCommand(env)) + cmd.AddCommand(newBugShowCommand(env)) + cmd.AddCommand(newBugStatusCommand(env)) + cmd.AddCommand(newBugTitleCommand(env)) return cmd } @@ -146,28 +148,33 @@ func runBug(env *execenv.Env, opts bugOptions, args []string) error { return err } - bugExcerpt := make([]*cache.BugExcerpt, len(allIds)) + excerpts := make([]*cache.BugExcerpt, len(allIds)) for i, id := range allIds { b, err := env.Backend.Bugs().ResolveExcerpt(id) if err != nil { return err } - bugExcerpt[i] = b + excerpts[i] = b } switch opts.outputFormat { - case "org-mode": - return bugsOrgmodeFormatter(env, bugExcerpt) + case "default": + if opts.outputFormatChanged { + return bugsDefaultFormatter(env, excerpts) + } + if env.Out.IsTerminal() { + return bugsDefaultFormatter(env, excerpts) + } else { + return bugsPlainFormatter(env, excerpts) + } + case "id": + return bugsIDFormatter(env, excerpts) case "plain": - return bugsPlainFormatter(env, bugExcerpt) + return bugsPlainFormatter(env, excerpts) case "json": - return bugsJsonFormatter(env, bugExcerpt) - case "compact": - return bugsCompactFormatter(env, bugExcerpt) - case "id": - return bugsIDFormatter(env, bugExcerpt) - case "default": - return bugsDefaultFormatter(env, bugExcerpt) + return bugsJsonFormatter(env, excerpts) + case "org-mode": + return bugsOrgmodeFormatter(env, excerpts) default: return fmt.Errorf("unknown format %s", opts.outputFormat) } @@ -186,9 +193,9 @@ func repairQuery(args []string) string { return strings.Join(args, " ") } -func bugsJsonFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { - jsonBugs := make([]cmdjson.BugExcerpt, len(bugExcerpts)) - for i, b := range bugExcerpts { +func bugsJsonFormatter(env *execenv.Env, excerpts []*cache.BugExcerpt) error { + jsonBugs := make([]cmdjson.BugExcerpt, len(excerpts)) + for i, b := range excerpts { jsonBug, err := cmdjson.NewBugExcerpt(env.Backend, b) if err != nil { return err @@ -198,42 +205,34 @@ func bugsJsonFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error return env.Out.PrintJSON(jsonBugs) } -func bugsCompactFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { - for _, b := range bugExcerpts { - author, err := env.Backend.Identities().ResolveExcerpt(b.AuthorId) - if err != nil { - return err - } - - var labelsTxt strings.Builder - for _, l := range b.Labels { - lc256 := l.Color().Term256() - labelsTxt.WriteString(lc256.Escape()) - labelsTxt.WriteString("◼") - labelsTxt.WriteString(lc256.Unescape()) - } - - env.Out.Printf("%s %s %s %s %s\n", - colors.Cyan(b.Id().Human()), - colors.Yellow(b.Status), - text.LeftPadMaxLine(strings.TrimSpace(b.Title), 40, 0), - text.LeftPadMaxLine(labelsTxt.String(), 5, 0), - colors.Magenta(text.TruncateMax(author.DisplayName(), 15)), - ) +func bugsIDFormatter(env *execenv.Env, excerpts []*cache.BugExcerpt) error { + for _, b := range excerpts { + env.Out.Println(b.Id().String()) } + return nil } -func bugsIDFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { - for _, b := range bugExcerpts { - env.Out.Println(b.Id().String()) +func bugsDefaultFormatter(env *execenv.Env, excerpts []*cache.BugExcerpt) error { + width := env.Out.Width() + widthId := entity.HumanIdLength + widthStatus := len("closed") + widthComment := 6 + + widthRemaining := width - + widthId - 1 - + widthStatus - 1 - + widthComment - 1 + + widthTitle := int(float32(widthRemaining-3) * 0.7) + if widthTitle < 0 { + widthTitle = 0 } - return nil -} + widthRemaining = widthRemaining - widthTitle - 3 - 2 + widthAuthor := widthRemaining -func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { - for _, b := range bugExcerpts { + for _, b := range excerpts { author, err := env.Backend.Identities().ResolveExcerpt(b.AuthorId) if err != nil { return err @@ -249,8 +248,8 @@ func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err // truncate + pad if needed labelsFmt := text.TruncateMax(labelsTxt.String(), 10) - titleFmt := text.LeftPadMaxLine(strings.TrimSpace(b.Title), 50-text.Len(labelsFmt), 0) - authorFmt := text.LeftPadMaxLine(author.DisplayName(), 15, 0) + titleFmt := text.LeftPadMaxLine(strings.TrimSpace(b.Title), widthTitle-text.Len(labelsFmt), 0) + authorFmt := text.LeftPadMaxLine(author.DisplayName(), widthAuthor, 0) comments := fmt.Sprintf("%3d 💬", b.LenComments-1) if b.LenComments-1 <= 0 { @@ -260,7 +259,7 @@ func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err comments = " ∞ 💬" } - env.Out.Printf("%s\t%s\t%s\t%s\t%s\n", + env.Out.Printf("%s\t%s\t%s %s %s\n", colors.Cyan(b.Id().Human()), colors.Yellow(b.Status), titleFmt+labelsFmt, @@ -271,14 +270,14 @@ func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err return nil } -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)) +func bugsPlainFormatter(env *execenv.Env, excerpts []*cache.BugExcerpt) error { + for _, b := range excerpts { + env.Out.Printf("%s\t%s\t%s\n", b.Id().Human(), b.Status, strings.TrimSpace(b.Title)) } return nil } -func bugsOrgmodeFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { +func bugsOrgmodeFormatter(env *execenv.Env, excerpts []*cache.BugExcerpt) error { // see https://orgmode.org/manual/Tags.html orgTagRe := regexp.MustCompile("[^[:alpha:]_@]") formatTag := func(l bug.Label) string { @@ -291,7 +290,7 @@ func bugsOrgmodeFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err env.Out.Println("#+TODO: OPEN | CLOSED") - for _, b := range bugExcerpts { + for _, b := range excerpts { status := strings.ToUpper(b.Status.String()) var title string diff --git a/commands/bug/bug_comment.go b/commands/bug/bug_comment.go index 4dc8dc1f..5cb8ff17 100644 --- a/commands/bug/bug_comment.go +++ b/commands/bug/bug_comment.go @@ -8,9 +8,7 @@ import ( "github.com/MichaelMure/git-bug/util/colors" ) -func newBugCommentCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBugCommentCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "comment [BUG_ID]", Short: "List a bug's comments", @@ -21,8 +19,8 @@ func newBugCommentCommand() *cobra.Command { ValidArgsFunction: BugCompletion(env), } - cmd.AddCommand(newBugCommentNewCommand()) - cmd.AddCommand(newBugCommentEditCommand()) + cmd.AddCommand(newBugCommentNewCommand(env)) + cmd.AddCommand(newBugCommentEditCommand(env)) return cmd } diff --git a/commands/bug/bug_comment_add.go b/commands/bug/bug_comment_add.go index ff406b4f..152a1893 100644 --- a/commands/bug/bug_comment_add.go +++ b/commands/bug/bug_comment_add.go @@ -14,8 +14,7 @@ type bugCommentNewOptions struct { nonInteractive bool } -func newBugCommentNewCommand() *cobra.Command { - env := execenv.NewEnv() +func newBugCommentNewCommand(env *execenv.Env) *cobra.Command { options := bugCommentNewOptions{} cmd := &cobra.Command{ diff --git a/commands/bug/bug_comment_edit.go b/commands/bug/bug_comment_edit.go index ded3d82a..e6f8d667 100644 --- a/commands/bug/bug_comment_edit.go +++ b/commands/bug/bug_comment_edit.go @@ -13,8 +13,7 @@ type bugCommentEditOptions struct { nonInteractive bool } -func newBugCommentEditCommand() *cobra.Command { - env := execenv.NewEnv() +func newBugCommentEditCommand(env *execenv.Env) *cobra.Command { options := bugCommentEditOptions{} cmd := &cobra.Command{ diff --git a/commands/bug/bug_comment_test.go b/commands/bug/bug_comment_test.go index c1dc9952..ecc1c5f6 100644 --- a/commands/bug/bug_comment_test.go +++ b/commands/bug/bug_comment_test.go @@ -2,7 +2,7 @@ package bugcmd import ( "fmt" - "io/ioutil" + "os" "strings" "testing" "time" @@ -143,7 +143,7 @@ func requireCommentsEqual(t *testing.T, golden string, env *execenv.Env) { t.Log("Got here") for i, comment := range comments { fileName := fmt.Sprintf(goldenFilePattern, golden, i) - require.NoError(t, ioutil.WriteFile(fileName, []byte(comment.message), 0644)) + require.NoError(t, os.WriteFile(fileName, []byte(comment.message), 0644)) } } @@ -157,7 +157,7 @@ func requireCommentsEqual(t *testing.T, golden string, env *execenv.Env) { require.Equal(t, date.Add(time.Duration(i)*time.Minute), comment.date) fileName := fmt.Sprintf(goldenFilePattern, golden, i) - exp, err := ioutil.ReadFile(fileName) + exp, err := os.ReadFile(fileName) require.NoError(t, err) require.Equal(t, strings.ReplaceAll(string(exp), "\r", ""), strings.ReplaceAll(comment.message, "\r", "")) } diff --git a/commands/bug/bug_deselect.go b/commands/bug/bug_deselect.go index 090a7bf2..5e9acc60 100644 --- a/commands/bug/bug_deselect.go +++ b/commands/bug/bug_deselect.go @@ -8,9 +8,7 @@ import ( "github.com/MichaelMure/git-bug/entities/bug" ) -func newBugDeselectCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBugDeselectCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "deselect", Short: "Clear the implicitly selected bug", diff --git a/commands/bug/bug_label.go b/commands/bug/bug_label.go index e6d0e603..554496e3 100644 --- a/commands/bug/bug_label.go +++ b/commands/bug/bug_label.go @@ -6,9 +6,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newBugLabelCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBugLabelCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "label [BUG_ID]", Short: "Display labels of a bug", @@ -19,8 +17,8 @@ func newBugLabelCommand() *cobra.Command { ValidArgsFunction: BugCompletion(env), } - cmd.AddCommand(newBugLabelNewCommand()) - cmd.AddCommand(newBugLabelRmCommand()) + cmd.AddCommand(newBugLabelNewCommand(env)) + cmd.AddCommand(newBugLabelRmCommand(env)) return cmd } diff --git a/commands/bug/bug_label_new.go b/commands/bug/bug_label_new.go index aa4f9463..1e1f2d4f 100644 --- a/commands/bug/bug_label_new.go +++ b/commands/bug/bug_label_new.go @@ -7,9 +7,7 @@ import ( "github.com/MichaelMure/git-bug/util/text" ) -func newBugLabelNewCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBugLabelNewCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "new [BUG_ID] LABEL...", Short: "Add a label to a bug", diff --git a/commands/bug/bug_label_rm.go b/commands/bug/bug_label_rm.go index 18510bbd..6dda007c 100644 --- a/commands/bug/bug_label_rm.go +++ b/commands/bug/bug_label_rm.go @@ -7,9 +7,7 @@ import ( "github.com/MichaelMure/git-bug/util/text" ) -func newBugLabelRmCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBugLabelRmCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "rm [BUG_ID] LABEL...", Short: "Remove a label from a bug", diff --git a/commands/bug/bug_new.go b/commands/bug/bug_new.go index 9ef288e9..e66967f9 100644 --- a/commands/bug/bug_new.go +++ b/commands/bug/bug_new.go @@ -15,8 +15,7 @@ type bugNewOptions struct { nonInteractive bool } -func newBugNewCommand() *cobra.Command { - env := execenv.NewEnv() +func newBugNewCommand(env *execenv.Env) *cobra.Command { options := bugNewOptions{} cmd := &cobra.Command{ diff --git a/commands/bug/bug_rm.go b/commands/bug/bug_rm.go index 386c57ec..b9d3d525 100644 --- a/commands/bug/bug_rm.go +++ b/commands/bug/bug_rm.go @@ -8,9 +8,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newBugRmCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBugRmCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "rm BUG_ID", Short: "Remove an existing bug", diff --git a/commands/bug/bug_select.go b/commands/bug/bug_select.go index bfad899d..652c61ea 100644 --- a/commands/bug/bug_select.go +++ b/commands/bug/bug_select.go @@ -15,9 +15,7 @@ func ResolveSelected(repo *cache.RepoCache, args []string) (*cache.BugCache, []s return _select.Resolve[*cache.BugCache](repo, bug.Typename, bug.Namespace, repo.Bugs(), args) } -func newBugSelectCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBugSelectCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "select BUG_ID", Short: "Select a bug for implicit use in future commands", diff --git a/commands/bug/bug_show.go b/commands/bug/bug_show.go index 9f80120c..9a03c9a3 100644 --- a/commands/bug/bug_show.go +++ b/commands/bug/bug_show.go @@ -19,8 +19,7 @@ type bugShowOptions struct { format string } -func newBugShowCommand() *cobra.Command { - env := execenv.NewEnv() +func newBugShowCommand(env *execenv.Env) *cobra.Command { options := bugShowOptions{} cmd := &cobra.Command{ diff --git a/commands/bug/bug_status.go b/commands/bug/bug_status.go index 807a9a60..59bef3fd 100644 --- a/commands/bug/bug_status.go +++ b/commands/bug/bug_status.go @@ -6,9 +6,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newBugStatusCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBugStatusCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "status [BUG_ID]", Short: "Display the status of a bug", @@ -19,8 +17,8 @@ func newBugStatusCommand() *cobra.Command { ValidArgsFunction: BugCompletion(env), } - cmd.AddCommand(newBugStatusCloseCommand()) - cmd.AddCommand(newBugStatusOpenCommand()) + cmd.AddCommand(newBugStatusCloseCommand(env)) + cmd.AddCommand(newBugStatusOpenCommand(env)) return cmd } diff --git a/commands/bug/bug_status_close.go b/commands/bug/bug_status_close.go index e52959b2..1d06007b 100644 --- a/commands/bug/bug_status_close.go +++ b/commands/bug/bug_status_close.go @@ -6,9 +6,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newBugStatusCloseCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBugStatusCloseCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "close [BUG_ID]", Short: "Mark a bug as closed", diff --git a/commands/bug/bug_status_open.go b/commands/bug/bug_status_open.go index 74177974..e99d2db0 100644 --- a/commands/bug/bug_status_open.go +++ b/commands/bug/bug_status_open.go @@ -6,9 +6,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newBugStatusOpenCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBugStatusOpenCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "open [BUG_ID]", Short: "Mark a bug as open", diff --git a/commands/bug/bug_test.go b/commands/bug/bug_test.go index cb6a4373..6b0aa4e0 100644 --- a/commands/bug/bug_test.go +++ b/commands/bug/bug_test.go @@ -2,7 +2,6 @@ package bugcmd import ( "encoding/json" - "fmt" "testing" "github.com/stretchr/testify/require" @@ -61,44 +60,34 @@ $` format string exp string }{ - {"default", "^[0-9a-f]{7}\topen\tthis is a bug title \tJohn Doe \t\n$"}, - {"plain", "^[0-9a-f]{7} \\[open\\] this is a bug title\n$"}, - {"compact", "^[0-9a-f]{7} open this is a bug title John Doe\n$"}, + {"default", "^[0-9a-f]{7}\topen\tthis is a bug title John Doe \n$"}, + {"plain", "^[0-9a-f]{7}\topen\tthis is a bug title\n$"}, {"id", "^[0-9a-f]{64}\n$"}, {"org-mode", expOrgMode}, + {"json", ".*"}, } for _, testcase := range cases { - opts := bugOptions{ - sortDirection: "asc", - sortBy: "creation", - outputFormat: testcase.format, - } - - name := fmt.Sprintf("with %s format", testcase.format) - - t.Run(name, func(t *testing.T) { + t.Run(testcase.format, func(t *testing.T) { env, _ := testenv.NewTestEnvAndBug(t) + opts := bugOptions{ + sortDirection: "asc", + sortBy: "creation", + outputFormat: testcase.format, + outputFormatChanged: true, // disable auto-detect + } + require.NoError(t, runBug(env, opts, []string{})) - require.Regexp(t, testcase.exp, env.Out.String()) + + switch testcase.format { + case "json": + var bugs []cmdjson.BugExcerpt + require.NoError(t, json.Unmarshal(env.Out.Bytes(), &bugs)) + require.Len(t, bugs, 1) + default: + require.Regexp(t, testcase.exp, env.Out.String()) + } }) } - - t.Run("with JSON format", func(t *testing.T) { - opts := bugOptions{ - sortDirection: "asc", - sortBy: "creation", - outputFormat: "json", - } - - env, _ := testenv.NewTestEnvAndBug(t) - - require.NoError(t, runBug(env, opts, []string{})) - - var bugs []cmdjson.BugExcerpt - require.NoError(t, json.Unmarshal(env.Out.Bytes(), &bugs)) - - require.Len(t, bugs, 1) - }) } diff --git a/commands/bug/bug_title.go b/commands/bug/bug_title.go index e59a1fdc..47603410 100644 --- a/commands/bug/bug_title.go +++ b/commands/bug/bug_title.go @@ -6,9 +6,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newBugTitleCommand() *cobra.Command { - env := execenv.NewEnv() - +func newBugTitleCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "title [BUG_ID]", Short: "Display the title of a bug", @@ -19,7 +17,7 @@ func newBugTitleCommand() *cobra.Command { ValidArgsFunction: BugCompletion(env), } - cmd.AddCommand(newBugTitleEditCommand()) + cmd.AddCommand(newBugTitleEditCommand(env)) return cmd } diff --git a/commands/bug/bug_title_edit.go b/commands/bug/bug_title_edit.go index 59898530..fc60824f 100644 --- a/commands/bug/bug_title_edit.go +++ b/commands/bug/bug_title_edit.go @@ -13,8 +13,7 @@ type bugTitleEditOptions struct { nonInteractive bool } -func newBugTitleEditCommand() *cobra.Command { - env := execenv.NewEnv() +func newBugTitleEditCommand(env *execenv.Env) *cobra.Command { options := bugTitleEditOptions{} cmd := &cobra.Command{ diff --git a/commands/commands.go b/commands/commands.go index 7d2fc37d..173a0904 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -12,8 +12,7 @@ type commandOptions struct { desc bool } -func newCommandsCommand() *cobra.Command { - env := execenv.NewEnv() +func newCommandsCommand(env *execenv.Env) *cobra.Command { options := commandOptions{} cmd := &cobra.Command{ diff --git a/commands/execenv/env.go b/commands/execenv/env.go index 4be7c247..e693807e 100644 --- a/commands/execenv/env.go +++ b/commands/execenv/env.go @@ -6,12 +6,11 @@ import ( "io" "os" - "github.com/spf13/cobra" + "github.com/mattn/go-isatty" + "golang.org/x/term" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/interrupt" ) const RootCommandName = "git-bug" @@ -22,6 +21,7 @@ const gitBugNamespace = "git-bug" type Env struct { Repo repository.ClockedRepo Backend *cache.RepoCache + In In Out Out Err Out } @@ -29,18 +29,42 @@ type Env struct { func NewEnv() *Env { return &Env{ Repo: nil, + In: in{Reader: os.Stdin}, Out: out{Writer: os.Stdout}, Err: out{Writer: os.Stderr}, } } +type In interface { + io.Reader + + // IsTerminal tells if the input is a user terminal (rather than a buffer, + // a pipe ...), which tells if we can use interactive features. + IsTerminal() bool + + // ForceIsTerminal allow to force the returned value of IsTerminal + // This only works in test scenario. + ForceIsTerminal(value bool) +} + type Out interface { io.Writer + Printf(format string, a ...interface{}) Print(a ...interface{}) Println(a ...interface{}) PrintJSON(v interface{}) error + // IsTerminal tells if the output is a user terminal (rather than a buffer, + // a pipe ...), which tells if we can use colors and other interactive features. + IsTerminal() bool + // Width return the width of the attached terminal, or a good enough value. + Width() int + + // Raw return the underlying io.Writer, or itself if not. + // This is useful if something need to access the raw file descriptor. + Raw() io.Writer + // String returns what have been written in the output before, as a string. // This only works in test scenario. String() string @@ -50,6 +74,24 @@ type Out interface { // Reset clear what has been recorded as written in the output before. // This only works in test scenario. Reset() + // ForceIsTerminal allow to force the returned value of IsTerminal + // This only works in test scenario. + ForceIsTerminal(value bool) +} + +type in struct { + io.Reader +} + +func (i in) IsTerminal() bool { + if f, ok := i.Reader.(*os.File); ok { + return isTerminal(f) + } + return false +} + +func (i in) ForceIsTerminal(_ bool) { + panic("only work with a test env") } type out struct { @@ -77,139 +119,43 @@ func (o out) PrintJSON(v interface{}) error { return nil } -func (o out) String() string { - panic("only work with a test env") -} - -func (o out) Bytes() []byte { - panic("only work with a test env") -} - -func (o out) Reset() { - panic("only work with a test env") -} - -// LoadRepo is a pre-run function that load the repository for use in a command -func LoadRepo(env *Env) func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("unable to get the current working directory: %q", err) - } - - // Note: we are not loading clocks here because we assume that LoadRepo is only used - // when we don't manipulate entities, or as a child call of LoadBackend which will - // read all clocks anyway. - env.Repo, err = repository.OpenGoGitRepo(cwd, gitBugNamespace, nil) - if err == repository.ErrNotARepo { - return fmt.Errorf("%s must be run from within a git Repo", RootCommandName) - } - if err != nil { - return err - } - - return nil +func (o out) IsTerminal() bool { + if f, ok := o.Writer.(*os.File); ok { + return isTerminal(f) } + return false } -// LoadRepoEnsureUser is the same as LoadRepo, but also ensure that the user has configured -// an identity. Use this pre-run function when an error after using the configured user won't -// do. -func LoadRepoEnsureUser(env *Env) func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - err := LoadRepo(env)(cmd, args) - if err != nil { - return err - } - - _, err = identity.GetUserIdentity(env.Repo) - if err != nil { - return err +func (o out) Width() int { + if f, ok := o.Raw().(*os.File); ok { + width, _, err := term.GetSize(int(f.Fd())) + if err == nil { + return width } - - return nil } + return 80 } -// LoadBackend is a pre-run function that load the repository and the Backend for use in a command -// When using this function you also need to use CloseBackend as a post-run -func LoadBackend(env *Env) func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - err := LoadRepo(env)(cmd, args) - if err != nil { - return err - } - - var events chan cache.BuildEvent - env.Backend, events = cache.NewRepoCache(env.Repo) - - for event := range events { - if event.Err != nil { - return event.Err - } - switch event.Event { - case cache.BuildEventCacheIsBuilt: - env.Err.Println("Building cache... ") - case cache.BuildEventStarted: - env.Err.Printf("[%s] started\n", event.Typename) - case cache.BuildEventFinished: - env.Err.Printf("[%s] done\n", event.Typename) - } - } - - cleaner := func(env *Env) interrupt.CleanerFunc { - return func() error { - if env.Backend != nil { - err := env.Backend.Close() - env.Backend = nil - return err - } - return nil - } - } - - // Cleanup properly on interrupt - interrupt.RegisterCleaner(cleaner(env)) - return nil - } +func (o out) Raw() io.Writer { + return o.Writer } -// LoadBackendEnsureUser is the same as LoadBackend, but also ensure that the user has configured -// an identity. Use this pre-run function when an error after using the configured user won't -// do. -func LoadBackendEnsureUser(env *Env) func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - err := LoadBackend(env)(cmd, args) - if err != nil { - return err - } - - _, err = identity.GetUserIdentity(env.Repo) - if err != nil { - return err - } +func (o out) String() string { + panic("only work with a test env") +} - return nil - } +func (o out) Bytes() []byte { + panic("only work with a test env") } -// CloseBackend is a wrapper for a RunE function that will close the Backend properly -// if it has been opened. -// This wrapper style is necessary because a Cobra PostE function does not run if RunE return an error. -func CloseBackend(env *Env, runE func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - errRun := runE(cmd, args) +func (o out) Reset() { + panic("only work with a test env") +} - if env.Backend == nil { - return nil - } - err := env.Backend.Close() - env.Backend = nil +func (o out) ForceIsTerminal(_ bool) { + panic("only work with a test env") +} - // prioritize the RunE error - if errRun != nil { - return errRun - } - return err - } +func isTerminal(file *os.File) bool { + return isatty.IsTerminal(file.Fd()) || isatty.IsCygwinTerminal(file.Fd()) } diff --git a/commands/execenv/env_test.go b/commands/execenv/env_test.go new file mode 100644 index 00000000..3fc6e581 --- /dev/null +++ b/commands/execenv/env_test.go @@ -0,0 +1,21 @@ +package execenv + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsTerminal(t *testing.T) { + // easy way to get a reader and a writer + r, w, err := os.Pipe() + require.NoError(t, err) + + require.False(t, isTerminal(r)) + require.False(t, isTerminal(w)) + + // golang's testing framework replaces os.Stdin and os.Stdout, so the following doesn't work here + // require.True(t, isTerminal(os.Stdin)) + // require.True(t, isTerminal(os.Stdout)) +} diff --git a/commands/execenv/env_testing.go b/commands/execenv/env_testing.go index 34eafc9c..15d7b646 100644 --- a/commands/execenv/env_testing.go +++ b/commands/execenv/env_testing.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "testing" "github.com/stretchr/testify/require" @@ -12,8 +13,26 @@ import ( "github.com/MichaelMure/git-bug/repository" ) +var _ In = &TestIn{} + +type TestIn struct { + *bytes.Buffer + forceIsTerminal bool +} + +func (t *TestIn) IsTerminal() bool { + return t.forceIsTerminal +} + +func (t *TestIn) ForceIsTerminal(value bool) { + t.forceIsTerminal = value +} + +var _ Out = &TestOut{} + type TestOut struct { *bytes.Buffer + forceIsTerminal bool } func (te *TestOut) Printf(format string, a ...interface{}) { @@ -37,12 +56,34 @@ func (te *TestOut) PrintJSON(v interface{}) error { return nil } +func (te *TestOut) IsTerminal() bool { + return te.forceIsTerminal +} + +func (te *TestOut) Width() int { + return 80 +} + +func (te *TestOut) Raw() io.Writer { + return te.Buffer +} + +func (te *TestOut) ForceIsTerminal(value bool) { + te.forceIsTerminal = value +} + func NewTestEnv(t *testing.T) *Env { t.Helper() + return newTestEnv(t, false) +} - repo := repository.CreateGoGitTestRepo(t, false) +func NewTestEnvTerminal(t *testing.T) *Env { + t.Helper() + return newTestEnv(t, true) +} - buf := new(bytes.Buffer) +func newTestEnv(t *testing.T, isTerminal bool) *Env { + repo := repository.CreateGoGitTestRepo(t, false) backend, err := cache.NewRepoCacheNoEvents(repo) require.NoError(t, err) @@ -54,7 +95,8 @@ func NewTestEnv(t *testing.T) *Env { return &Env{ Repo: repo, Backend: backend, - Out: &TestOut{buf}, - Err: &TestOut{buf}, + In: &TestIn{Buffer: &bytes.Buffer{}, forceIsTerminal: isTerminal}, + Out: &TestOut{Buffer: &bytes.Buffer{}, forceIsTerminal: isTerminal}, + Err: &TestOut{Buffer: &bytes.Buffer{}, forceIsTerminal: isTerminal}, } } diff --git a/commands/execenv/loading.go b/commands/execenv/loading.go new file mode 100644 index 00000000..2263f700 --- /dev/null +++ b/commands/execenv/loading.go @@ -0,0 +1,169 @@ +package execenv + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" + + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/entities/identity" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/interrupt" +) + +// LoadRepo is a pre-run function that load the repository for use in a command +func LoadRepo(env *Env) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("unable to get the current working directory: %q", err) + } + + // Note: we are not loading clocks here because we assume that LoadRepo is only used + // when we don't manipulate entities, or as a child call of LoadBackend which will + // read all clocks anyway. + env.Repo, err = repository.OpenGoGitRepo(cwd, gitBugNamespace, nil) + if err == repository.ErrNotARepo { + return fmt.Errorf("%s must be run from within a git Repo", RootCommandName) + } + if err != nil { + return err + } + + return nil + } +} + +// LoadRepoEnsureUser is the same as LoadRepo, but also ensure that the user has configured +// an identity. Use this pre-run function when an error after using the configured user won't +// do. +func LoadRepoEnsureUser(env *Env) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + err := LoadRepo(env)(cmd, args) + if err != nil { + return err + } + + _, err = identity.GetUserIdentity(env.Repo) + if err != nil { + return err + } + + return nil + } +} + +// LoadBackend is a pre-run function that load the repository and the Backend for use in a command +// When using this function you also need to use CloseBackend as a post-run +func LoadBackend(env *Env) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + err := LoadRepo(env)(cmd, args) + if err != nil { + return err + } + + var events chan cache.BuildEvent + env.Backend, events = cache.NewRepoCache(env.Repo) + + err = CacheBuildProgressBar(env, events) + if err != nil { + return err + } + + cleaner := func(env *Env) interrupt.CleanerFunc { + return func() error { + if env.Backend != nil { + err := env.Backend.Close() + env.Backend = nil + return err + } + return nil + } + } + + // Cleanup properly on interrupt + interrupt.RegisterCleaner(cleaner(env)) + return nil + } +} + +// LoadBackendEnsureUser is the same as LoadBackend, but also ensure that the user has configured +// an identity. Use this pre-run function when an error after using the configured user won't +// do. +func LoadBackendEnsureUser(env *Env) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + err := LoadBackend(env)(cmd, args) + if err != nil { + return err + } + + _, err = identity.GetUserIdentity(env.Repo) + if err != nil { + return err + } + + return nil + } +} + +// CloseBackend is a wrapper for a RunE function that will close the Backend properly +// if it has been opened. +// This wrapper style is necessary because a Cobra PostE function does not run if RunE return an error. +func CloseBackend(env *Env, runE func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + errRun := runE(cmd, args) + + if env.Backend == nil { + return nil + } + err := env.Backend.Close() + env.Backend = nil + + // prioritize the RunE error + if errRun != nil { + return errRun + } + return err + } +} + +func CacheBuildProgressBar(env *Env, events chan cache.BuildEvent) error { + var progress *mpb.Progress + var bars = make(map[string]*mpb.Bar) + + for event := range events { + if event.Err != nil { + return event.Err + } + + if progress == nil { + progress = mpb.New(mpb.WithOutput(env.Err.Raw())) + } + + switch event.Event { + case cache.BuildEventCacheIsBuilt: + env.Err.Println("Building cache... ") + case cache.BuildEventStarted: + bars[event.Typename] = progress.AddBar(-1, + mpb.BarRemoveOnComplete(), + mpb.PrependDecorators( + decor.Name(event.Typename, decor.WCSyncSpace), + decor.CountersNoUnit("%d / %d", decor.WCSyncSpace), + ), + mpb.AppendDecorators(decor.Percentage(decor.WCSyncSpace)), + ) + case cache.BuildEventProgress: + bars[event.Typename].SetTotal(event.Total, false) + bars[event.Typename].SetCurrent(event.Progress) + } + } + + if progress != nil { + progress.Shutdown() + } + + return nil +} diff --git a/commands/label.go b/commands/label.go index 08b9e31f..d59479b4 100644 --- a/commands/label.go +++ b/commands/label.go @@ -6,9 +6,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newLabelCommand() *cobra.Command { - env := execenv.NewEnv() - +func newLabelCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "label", Short: "List valid labels", diff --git a/commands/pull.go b/commands/pull.go index 2e2639e1..91eadcf8 100644 --- a/commands/pull.go +++ b/commands/pull.go @@ -10,9 +10,7 @@ import ( "github.com/MichaelMure/git-bug/entity" ) -func newPullCommand() *cobra.Command { - env := execenv.NewEnv() - +func newPullCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "pull [REMOTE]", Short: "Pull updates from a git remote", diff --git a/commands/push.go b/commands/push.go index d45e301a..254bbb27 100644 --- a/commands/push.go +++ b/commands/push.go @@ -9,9 +9,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newPushCommand() *cobra.Command { - env := execenv.NewEnv() - +func newPushCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "push [REMOTE]", Short: "Push updates to a git remote", diff --git a/commands/root.go b/commands/root.go index 1ccc3e56..01c817ee 100644 --- a/commands/root.go +++ b/commands/root.go @@ -62,19 +62,22 @@ the same git remote you are already using to collaborate with other people. child.GroupID = groupID } - addCmdWithGroup(bugcmd.NewBugCommand(), entityGroup) - addCmdWithGroup(usercmd.NewUserCommand(), entityGroup) - addCmdWithGroup(newLabelCommand(), entityGroup) + env := execenv.NewEnv() - addCmdWithGroup(newTermUICommand(), uiGroup) - addCmdWithGroup(newWebUICommand(), uiGroup) + addCmdWithGroup(bugcmd.NewBugCommand(env), entityGroup) + addCmdWithGroup(usercmd.NewUserCommand(env), entityGroup) + addCmdWithGroup(newLabelCommand(env), entityGroup) - addCmdWithGroup(newPullCommand(), remoteGroup) - addCmdWithGroup(newPushCommand(), remoteGroup) - addCmdWithGroup(bridgecmd.NewBridgeCommand(), remoteGroup) + addCmdWithGroup(newTermUICommand(env), uiGroup) + addCmdWithGroup(newWebUICommand(env), uiGroup) - cmd.AddCommand(newCommandsCommand()) - cmd.AddCommand(newVersionCommand()) + addCmdWithGroup(newPullCommand(env), remoteGroup) + addCmdWithGroup(newPushCommand(env), remoteGroup) + addCmdWithGroup(bridgecmd.NewBridgeCommand(env), remoteGroup) + + cmd.AddCommand(newCommandsCommand(env)) + cmd.AddCommand(newVersionCommand(env)) + cmd.AddCommand(newWipeCommand(env)) return cmd } diff --git a/commands/termui.go b/commands/termui.go index 1cfdd8f3..08eba1d6 100644 --- a/commands/termui.go +++ b/commands/termui.go @@ -7,9 +7,7 @@ import ( "github.com/MichaelMure/git-bug/termui" ) -func newTermUICommand() *cobra.Command { - env := execenv.NewEnv() - +func newTermUICommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "termui", Aliases: []string{"tui"}, diff --git a/commands/user/user.go b/commands/user/user.go index a9a45726..de5c1ccd 100644 --- a/commands/user/user.go +++ b/commands/user/user.go @@ -16,8 +16,7 @@ type userOptions struct { format string } -func NewUserCommand() *cobra.Command { - env := execenv.NewEnv() +func NewUserCommand(env *execenv.Env) *cobra.Command { options := userOptions{} cmd := &cobra.Command{ @@ -29,9 +28,9 @@ func NewUserCommand() *cobra.Command { }), } - cmd.AddCommand(newUserNewCommand()) - cmd.AddCommand(newUserShowCommand()) - cmd.AddCommand(newUserAdoptCommand()) + cmd.AddCommand(newUserNewCommand(env)) + cmd.AddCommand(newUserShowCommand(env)) + cmd.AddCommand(newUserAdoptCommand(env)) flags := cmd.Flags() flags.SortFlags = false diff --git a/commands/user/user_adopt.go b/commands/user/user_adopt.go index 30fdb442..47f0f04e 100644 --- a/commands/user/user_adopt.go +++ b/commands/user/user_adopt.go @@ -7,9 +7,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" ) -func newUserAdoptCommand() *cobra.Command { - env := execenv.NewEnv() - +func newUserAdoptCommand(env *execenv.Env) *cobra.Command { cmd := &cobra.Command{ Use: "adopt USER_ID", Short: "Adopt an existing identity as your own", diff --git a/commands/user/user_new.go b/commands/user/user_new.go index 7b287492..ba4198f8 100644 --- a/commands/user/user_new.go +++ b/commands/user/user_new.go @@ -14,9 +14,7 @@ type userNewOptions struct { nonInteractive bool } -func newUserNewCommand() *cobra.Command { - env := execenv.NewEnv() - +func newUserNewCommand(env *execenv.Env) *cobra.Command { options := userNewOptions{} cmd := &cobra.Command{ Use: "new", diff --git a/commands/user/user_show.go b/commands/user/user_show.go index 225d0ef4..049eee93 100644 --- a/commands/user/user_show.go +++ b/commands/user/user_show.go @@ -16,8 +16,7 @@ type userShowOptions struct { fields string } -func newUserShowCommand() *cobra.Command { - env := execenv.NewEnv() +func newUserShowCommand(env *execenv.Env) *cobra.Command { options := userShowOptions{} cmd := &cobra.Command{ diff --git a/commands/version.go b/commands/version.go index 0e54bb92..957cc653 100644 --- a/commands/version.go +++ b/commands/version.go @@ -14,8 +14,7 @@ type versionOptions struct { all bool } -func newVersionCommand() *cobra.Command { - env := execenv.NewEnv() +func newVersionCommand(env *execenv.Env) *cobra.Command { options := versionOptions{} cmd := &cobra.Command{ diff --git a/commands/webui.go b/commands/webui.go index 0b3b24a6..3129a222 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -42,8 +42,7 @@ type webUIOptions struct { query string } -func newWebUICommand() *cobra.Command { - env := execenv.NewEnv() +func newWebUICommand(env *execenv.Env) *cobra.Command { options := webUIOptions{} cmd := &cobra.Command{ @@ -108,19 +107,10 @@ func runWebUI(env *execenv.Env, opts webUIOptions) error { mrc := cache.NewMultiRepoCache() _, events := mrc.RegisterDefaultRepository(env.Repo) - for event := range events { - if event.Err != nil { - env.Err.Printf("Cache building error [%s]: %v\n", event.Typename, event.Err) - continue - } - switch event.Event { - case cache.BuildEventCacheIsBuilt: - env.Err.Println("Building cache... ") - case cache.BuildEventStarted: - env.Err.Printf("[%s] started\n", event.Typename) - case cache.BuildEventFinished: - env.Err.Printf("[%s] done\n", event.Typename) - } + + err := execenv.CacheBuildProgressBar(env, events) + if err != nil { + return err } var errOut io.Writer diff --git a/commands/wipe.go b/commands/wipe.go new file mode 100644 index 00000000..ce9da032 --- /dev/null +++ b/commands/wipe.go @@ -0,0 +1,56 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/commands/execenv" +) + +func newWipeCommand(env *execenv.Env) *cobra.Command { + cmd := &cobra.Command{ + Use: "wipe", + Short: "Wipe git-bug from the git repository", + PreRunE: execenv.LoadBackend(env), + RunE: func(cmd *cobra.Command, args []string) error { + return runWipe(env) + }, + } + + return cmd +} + +func runWipe(env *execenv.Env) error { + env.Out.Println("cleaning entities...") + err := env.Backend.RemoveAll() + if err != nil { + _ = env.Backend.Close() + return err + } + + env.Out.Println("cleaning git config ...") + err = env.Backend.ClearUserIdentity() + if err != nil { + _ = env.Backend.Close() + return err + } + err = env.Backend.LocalConfig().RemoveAll("git-bug") + if err != nil { + _ = env.Backend.Close() + return err + } + + storage := env.Backend.LocalStorage() + + err = env.Backend.Close() + if err != nil { + return err + } + + env.Out.Println("cleaning caches ...") + err = storage.RemoveAll(".") + if err != nil { + return err + } + + return nil +} diff --git a/doc/man/git-bug-bug.1 b/doc/man/git-bug-bug.1 index 6ee62303..db26518e 100644 --- a/doc/man/git-bug-bug.1 +++ b/doc/man/git-bug-bug.1 @@ -62,7 +62,7 @@ You can pass an additional query to filter and order the list. This query can be .PP \fB-f\fP, \fB--format\fP="default" - Select the output formatting style. Valid values are [default,plain,compact,id,json,org-mode] + Select the output formatting style. Valid values are [default,plain,id,json,org-mode] .PP \fB-h\fP, \fB--help\fP[=false] diff --git a/doc/man/git-bug-wipe.1 b/doc/man/git-bug-wipe.1 new file mode 100644 index 00000000..fc3178f6 --- /dev/null +++ b/doc/man/git-bug-wipe.1 @@ -0,0 +1,27 @@ +.nh +.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" "" + +.SH NAME +.PP +git-bug-wipe - Wipe git-bug from the git repository + + +.SH SYNOPSIS +.PP +\fBgit-bug wipe [flags]\fP + + +.SH DESCRIPTION +.PP +Wipe git-bug from the git repository + + +.SH OPTIONS +.PP +\fB-h\fP, \fB--help\fP[=false] + help for wipe + + +.SH SEE ALSO +.PP +\fBgit-bug(1)\fP diff --git a/doc/man/git-bug.1 b/doc/man/git-bug.1 index 8b66d312..9bf59164 100644 --- a/doc/man/git-bug.1 +++ b/doc/man/git-bug.1 @@ -29,4 +29,4 @@ the same git remote you are already using to collaborate with other people. .SH SEE ALSO .PP -\fBgit-bug-bridge(1)\fP, \fBgit-bug-bug(1)\fP, \fBgit-bug-commands(1)\fP, \fBgit-bug-label(1)\fP, \fBgit-bug-pull(1)\fP, \fBgit-bug-push(1)\fP, \fBgit-bug-termui(1)\fP, \fBgit-bug-user(1)\fP, \fBgit-bug-version(1)\fP, \fBgit-bug-webui(1)\fP +\fBgit-bug-bridge(1)\fP, \fBgit-bug-bug(1)\fP, \fBgit-bug-commands(1)\fP, \fBgit-bug-label(1)\fP, \fBgit-bug-pull(1)\fP, \fBgit-bug-push(1)\fP, \fBgit-bug-termui(1)\fP, \fBgit-bug-user(1)\fP, \fBgit-bug-version(1)\fP, \fBgit-bug-webui(1)\fP, \fBgit-bug-wipe(1)\fP diff --git a/doc/md/git-bug.md b/doc/md/git-bug.md index b311aff5..a71d6dfb 100644 --- a/doc/md/git-bug.md +++ b/doc/md/git-bug.md @@ -34,4 +34,5 @@ git-bug [flags] * [git-bug user](git-bug_user.md) - List identities * [git-bug version](git-bug_version.md) - Show git-bug version information * [git-bug webui](git-bug_webui.md) - Launch the web UI +* [git-bug wipe](git-bug_wipe.md) - Wipe git-bug from the git repository diff --git a/doc/md/git-bug_bug.md b/doc/md/git-bug_bug.md index c040cd16..0917f654 100644 --- a/doc/md/git-bug_bug.md +++ b/doc/md/git-bug_bug.md @@ -42,7 +42,7 @@ git bug status:open --by creation "foo bar" baz -n, --no strings Filter by absence of something. Valid values are [label] -b, --by string Sort the results by a characteristic. Valid values are [id,creation,edit] (default "creation") -d, --direction string Select the sorting direction. Valid values are [asc,desc] (default "asc") - -f, --format string Select the output formatting style. Valid values are [default,plain,compact,id,json,org-mode] (default "default") + -f, --format string Select the output formatting style. Valid values are [default,plain,id,json,org-mode] (default "default") -h, --help help for bug ``` diff --git a/doc/md/git-bug_wipe.md b/doc/md/git-bug_wipe.md new file mode 100644 index 00000000..be1a7246 --- /dev/null +++ b/doc/md/git-bug_wipe.md @@ -0,0 +1,18 @@ +## git-bug wipe + +Wipe git-bug from the git repository + +``` +git-bug wipe [flags] +``` + +### Options + +``` + -h, --help help for wipe +``` + +### SEE ALSO + +* [git-bug](git-bug.md) - A bug tracker embedded in Git + diff --git a/entities/bug/bug_actions.go b/entities/bug/bug_actions.go index 198e4ed0..651d24dc 100644 --- a/entities/bug/bug_actions.go +++ b/entities/bug/bug_actions.go @@ -37,3 +37,9 @@ func MergeAll(repo repository.ClockedRepo, resolvers entity.Resolvers, remote st func Remove(repo repository.ClockedRepo, id entity.Id) error { return dag.Remove(def, repo, id) } + +// RemoveAll will remove all local bugs. +// RemoveAll is idempotent. +func RemoveAll(repo repository.ClockedRepo) error { + return dag.RemoveAll(def, repo) +} diff --git a/entities/identity/identity.go b/entities/identity/identity.go index 22ce652c..91564ffa 100644 --- a/entities/identity/identity.go +++ b/entities/identity/identity.go @@ -164,56 +164,6 @@ func ListLocalIds(repo repository.Repo) ([]entity.Id, error) { return entity.RefsToIds(refs), nil } -// RemoveIdentity will remove a local identity from its entity.Id -func RemoveIdentity(repo repository.ClockedRepo, id entity.Id) error { - var fullMatches []string - - refs, err := repo.ListRefs(identityRefPattern + id.String()) - if err != nil { - return err - } - if len(refs) > 1 { - return entity.NewErrMultipleMatch(Typename, entity.RefsToIds(refs)) - } - if len(refs) == 1 { - // we have the identity locally - fullMatches = append(fullMatches, refs[0]) - } - - remotes, err := repo.GetRemotes() - if err != nil { - return err - } - - for remote := range remotes { - remotePrefix := fmt.Sprintf(identityRemoteRefPattern+id.String(), remote) - remoteRefs, err := repo.ListRefs(remotePrefix) - if err != nil { - return err - } - if len(remoteRefs) > 1 { - return entity.NewErrMultipleMatch(Typename, entity.RefsToIds(refs)) - } - if len(remoteRefs) == 1 { - // found the identity in a remote - fullMatches = append(fullMatches, remoteRefs[0]) - } - } - - if len(fullMatches) == 0 { - return entity.NewErrNotFound(Typename) - } - - for _, ref := range fullMatches { - err = repo.RemoveRef(ref) - if err != nil { - return err - } - } - - return nil -} - // ReadAllLocal read and parse all local Identity func ReadAllLocal(repo repository.ClockedRepo) <-chan entity.StreamedEntity[*Identity] { return readAll(repo, identityRefPattern) @@ -238,6 +188,9 @@ func readAll(repo repository.ClockedRepo, refPrefix string) <-chan entity.Stream return } + total := int64(len(refs)) + current := int64(1) + for _, ref := range refs { i, err := read(repo, ref) @@ -246,7 +199,12 @@ func readAll(repo repository.ClockedRepo, refPrefix string) <-chan entity.Stream return } - out <- entity.StreamedEntity[*Identity]{Entity: i} + out <- entity.StreamedEntity[*Identity]{ + Entity: i, + CurrentEntity: current, + TotalEntities: total, + } + current++ } }() diff --git a/entities/identity/identity_actions.go b/entities/identity/identity_actions.go index 13776078..07560dc0 100644 --- a/entities/identity/identity_actions.go +++ b/entities/identity/identity_actions.go @@ -123,3 +123,74 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes return out } + +// Remove will remove a local identity from its entity.Id. +// It is left as a responsibility to the caller to make sure that this identities is not +// linked from another entity, otherwise it would break it. +// Remove is idempotent. +func Remove(repo repository.ClockedRepo, id entity.Id) error { + var fullMatches []string + + refs, err := repo.ListRefs(identityRefPattern + id.String()) + if err != nil { + return err + } + if len(refs) > 1 { + return entity.NewErrMultipleMatch(Typename, entity.RefsToIds(refs)) + } + if len(refs) == 1 { + // we have the identity locally + fullMatches = append(fullMatches, refs[0]) + } + + remotes, err := repo.GetRemotes() + if err != nil { + return err + } + + for remote := range remotes { + remotePrefix := fmt.Sprintf(identityRemoteRefPattern+id.String(), remote) + remoteRefs, err := repo.ListRefs(remotePrefix) + if err != nil { + return err + } + if len(remoteRefs) > 1 { + return entity.NewErrMultipleMatch(Typename, entity.RefsToIds(refs)) + } + if len(remoteRefs) == 1 { + // found the identity in a remote + fullMatches = append(fullMatches, remoteRefs[0]) + } + } + + if len(fullMatches) == 0 { + return entity.NewErrNotFound(Typename) + } + + for _, ref := range fullMatches { + err = repo.RemoveRef(ref) + if err != nil { + return err + } + } + + return nil +} + +// RemoveAll will remove all local identities. +// It is left as a responsibility to the caller to make sure that those identities are not +// linked from another entity, otherwise it would break them. +// RemoveAll is idempotent. +func RemoveAll(repo repository.ClockedRepo) error { + localIds, err := ListLocalIds(repo) + if err != nil { + return err + } + for _, id := range localIds { + err = Remove(repo, id) + if err != nil { + return err + } + } + return nil +} diff --git a/entities/identity/identity_test.go b/entities/identity/identity_test.go index 0ecc8058..85d5385b 100644 --- a/entities/identity/identity_test.go +++ b/entities/identity/identity_test.go @@ -275,7 +275,7 @@ func TestIdentityRemove(t *testing.T) { _, err = Fetch(repo, "remoteB") require.NoError(t, err) - err = RemoveIdentity(repo, rene.Id()) + err = Remove(repo, rene.Id()) require.NoError(t, err) _, err = ReadLocal(repo, rene.Id()) diff --git a/entities/identity/identity_user.go b/entities/identity/identity_user.go index 7eb374d4..f9e39bb2 100644 --- a/entities/identity/identity_user.go +++ b/entities/identity/identity_user.go @@ -15,6 +15,10 @@ func SetUserIdentity(repo repository.RepoConfig, identity *Identity) error { return repo.LocalConfig().StoreString(identityConfigKey, identity.Id().String()) } +func ClearUserIdentity(repo repository.RepoConfig) error { + return repo.LocalConfig().RemoveAll(identityConfigKey) +} + // GetUserIdentity read the current user identity, set with a git config entry func GetUserIdentity(repo repository.Repo) (*Identity, error) { id, err := GetUserIdentityId(repo) diff --git a/entity/dag/entity.go b/entity/dag/entity.go index 2028e1b4..f8dbd53d 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -314,6 +314,9 @@ func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) E return } + total := int64(len(refs)) + current := int64(1) + for _, ref := range refs { e, err := read[EntityT](def, wrapper, repo, resolvers, ref) @@ -322,7 +325,12 @@ func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) E return } - out <- entity.StreamedEntity[EntityT]{Entity: e} + out <- entity.StreamedEntity[EntityT]{ + Entity: e, + CurrentEntity: current, + TotalEntities: total, + } + current++ } }() diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index 97a68c36..5f0abec3 100644 --- a/entity/dag/entity_actions.go +++ b/entity/dag/entity_actions.go @@ -258,3 +258,19 @@ func Remove(def Definition, repo repository.ClockedRepo, id entity.Id) error { return nil } + +// RemoveAll delete all Entity matching the Definition. +// RemoveAll is idempotent. +func RemoveAll(def Definition, repo repository.ClockedRepo) error { + localIds, err := ListLocalIds(def, repo) + if err != nil { + return err + } + for _, id := range localIds { + err = Remove(def, repo, id) + if err != nil { + return err + } + } + return nil +} diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go index fd219644..6181614b 100644 --- a/entity/dag/entity_actions_test.go +++ b/entity/dag/entity_actions_test.go @@ -406,3 +406,34 @@ func TestRemove(t *testing.T) { err = Remove(def, repoA, e.Id()) require.NoError(t, err) } + +func TestRemoveAll(t *testing.T) { + repoA, _, _, id1, _, resolvers, def := makeTestContextRemote(t) + + var ids []entity.Id + + for i := 0; i < 10; i++ { + e := New(def) + e.Append(newOp1(id1, "foo")) + require.NoError(t, e.Commit(repoA)) + ids = append(ids, e.Id()) + } + + _, err := Push(def, repoA, "remote") + require.NoError(t, err) + + err = RemoveAll(def, repoA) + require.NoError(t, err) + + for _, id := range ids { + _, err = Read(def, wrapper, repoA, resolvers, id) + require.Error(t, err) + + _, err = readRemote(def, wrapper, repoA, resolvers, "remote", id) + require.Error(t, err) + } + + // Remove is idempotent + err = RemoveAll(def, repoA) + require.NoError(t, err) +} diff --git a/entity/id.go b/entity/id.go index 49398da8..0949bf92 100644 --- a/entity/id.go +++ b/entity/id.go @@ -11,7 +11,7 @@ import ( // sha-256 const idLength = 64 -const humanIdLength = 7 +const HumanIdLength = 7 const UnsetId = Id("unset") @@ -34,7 +34,7 @@ func (i Id) String() string { // Human return the identifier, shortened for human consumption func (i Id) Human() string { - format := fmt.Sprintf("%%.%ds", humanIdLength) + format := fmt.Sprintf("%%.%ds", HumanIdLength) return fmt.Sprintf(format, i) } diff --git a/entity/id_interleaved.go b/entity/id_interleaved.go index 28c59a42..7ae6d72e 100644 --- a/entity/id_interleaved.go +++ b/entity/id_interleaved.go @@ -22,7 +22,7 @@ func (ci CombinedId) String() string { // Human return the identifier, shortened for human consumption func (ci CombinedId) Human() string { - format := fmt.Sprintf("%%.%ds", humanIdLength) + format := fmt.Sprintf("%%.%ds", HumanIdLength) return fmt.Sprintf(format, ci) } diff --git a/entity/streamed.go b/entity/streamed.go index 789224a3..33124ef0 100644 --- a/entity/streamed.go +++ b/entity/streamed.go @@ -1,6 +1,11 @@ package entity type StreamedEntity[EntityT Interface] struct { - Entity EntityT Err error + Entity EntityT + + // CurrentEntity is the index of the current entity being streamed, to express progress. + CurrentEntity int64 + // TotalEntities is the total count of expected entities, if known. + TotalEntities int64 } @@ -11,7 +11,7 @@ require ( github.com/awesome-gocui/gocui v1.1.0 github.com/blevesearch/bleve v1.0.14 github.com/cheekybits/genny v1.0.0 - github.com/dustin/go-humanize v1.0.0 + github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.13.0 github.com/go-git/go-billy/v5 v5.4.0 github.com/go-git/go-git/v5 v5.5.2 @@ -26,6 +26,7 @@ require ( github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e github.com/spf13/cobra v1.6.1 github.com/stretchr/testify v1.8.1 + github.com/vbauerster/mpb/v8 v8.1.4 github.com/vektah/gqlparser/v2 v2.5.1 github.com/xanzy/go-gitlab v0.77.0 golang.org/x/crypto v0.5.0 @@ -35,7 +36,12 @@ require ( golang.org/x/text v0.6.0 ) +// https://github.com/go-git/go-git/pull/659 +replace github.com/go-git/go-git/v5 => github.com/MichaelMure/go-git/v5 v5.1.1-0.20230114115943-17400561a81c + require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/cloudflare/circl v1.3.1 // indirect github.com/lithammer/dedent v1.1.0 // indirect github.com/owenrumney/go-sarif v1.0.11 // indirect @@ -86,14 +92,14 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.0.3 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-runewidth v0.0.12 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/philhofer/fwd v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/praetorian-inc/gokart v0.5.1 - github.com/rivo/uniseg v0.1.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect @@ -108,7 +114,7 @@ require ( go.etcd.io/bbolt v1.3.5 // indirect golang.org/x/mod v0.7.0 // indirect golang.org/x/net v0.5.0 // indirect - golang.org/x/term v0.4.0 // indirect + golang.org/x/term v0.4.0 golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect golang.org/x/tools v0.4.0 // indirect golang.org/x/vuln v0.0.0-20220908155419-5537ad2271a7 @@ -10,6 +10,8 @@ github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +github.com/MichaelMure/go-git/v5 v5.1.1-0.20230114115943-17400561a81c h1:JFFZbyq4cdKo+QrKNxXemMftPy08aS9gELrPTlPTaZU= +github.com/MichaelMure/go-git/v5 v5.1.1-0.20230114115943-17400561a81c/go.mod h1:BE5hUJ5yaV2YMxhmaP4l6RBQ08kMxKSPD4BlxtH7OjI= github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0= github.com/MichaelMure/go-term-text v0.3.1/go.mod h1:QgVjAEDUnRMlzpS6ky5CGblux7ebeiLnuy9dAaFZu8o= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -19,6 +21,10 @@ github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 h1:ra2OtmuW0A github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8= github.com/RoaringBitmap/roaring v0.4.23 h1:gpyfd12QohbqhFO4NVDUdoPOCXsyahYRQhINmlHxKeo= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= @@ -94,8 +100,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -123,8 +129,6 @@ github.com/go-git/go-billy/v5 v5.4.0 h1:Vaw7LaSTRJOUric7pe4vnzBSgyuf2KrLsu2Y4ZpQ github.com/go-git/go-billy/v5 v5.4.0/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ= github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= -github.com/go-git/go-git/v5 v5.5.2 h1:v8lgZa5k9ylUw+OR/roJHTxR4QItsNFI5nKtAXFuynw= -github.com/go-git/go-git/v5 v5.5.2/go.mod h1:BE5hUJ5yaV2YMxhmaP4l6RBQ08kMxKSPD4BlxtH7OjI= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -209,8 +213,9 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -243,8 +248,9 @@ github.com/praetorian-inc/gokart v0.5.1 h1:GYUM69qskrRibZUAEwKEm/pd/j/SFzlFnQnhx github.com/praetorian-inc/gokart v0.5.1/go.mod h1:GuA97YgdXwqOVsnHY6PCvV1t9t0Jsk3Zcd6sbTXj4uI= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -298,6 +304,8 @@ github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDW github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4= github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY= +github.com/vbauerster/mpb/v8 v8.1.4 h1:MOcLTIbbAA892wVjRiuFHa1nRlNvifQMDVh12Bq/xIs= +github.com/vbauerster/mpb/v8 v8.1.4/go.mod h1:2fRME8lCLU9gwJwghZb1bO9A3Plc8KPeQ/ayGj+Ek4I= github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4= github.com/vektah/gqlparser/v2 v2.5.1/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= diff --git a/repository/gogit.go b/repository/gogit.go index 01c47d41..93806026 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -13,7 +13,6 @@ import ( "time" "github.com/ProtonMail/go-crypto/openpgp" - "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/osfs" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" @@ -48,7 +47,7 @@ type GoGitRepo struct { indexes map[string]Index keyring Keyring - localStorage billy.Filesystem + localStorage LocalStorage } // OpenGoGitRepo opens an already existing repo at the given path and @@ -77,7 +76,7 @@ func OpenGoGitRepo(path, namespace string, clockLoaders []ClockLoader) (*GoGitRe clocks: make(map[string]lamport.Clock), indexes: make(map[string]Index), keyring: k, - localStorage: osfs.New(filepath.Join(path, namespace)), + localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, namespace))}, } loaderToRun := make([]ClockLoader, 0, len(clockLoaders)) @@ -131,7 +130,7 @@ func InitGoGitRepo(path, namespace string) (*GoGitRepo, error) { clocks: make(map[string]lamport.Clock), indexes: make(map[string]Index), keyring: k, - localStorage: osfs.New(filepath.Join(path, ".git", namespace)), + localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, ".git", namespace))}, }, nil } @@ -156,7 +155,7 @@ func InitBareGoGitRepo(path, namespace string) (*GoGitRepo, error) { clocks: make(map[string]lamport.Clock), indexes: make(map[string]Index), keyring: k, - localStorage: osfs.New(filepath.Join(path, namespace)), + localStorage: billyLocalStorage{Filesystem: osfs.New(filepath.Join(path, namespace))}, }, nil } @@ -320,7 +319,7 @@ func (repo *GoGitRepo) GetRemotes() (map[string]string, error) { // LocalStorage returns a billy.Filesystem giving access to // $RepoPath/.git/$Namespace. -func (repo *GoGitRepo) LocalStorage() billy.Filesystem { +func (repo *GoGitRepo) LocalStorage() LocalStorage { return repo.localStorage } diff --git a/repository/index_bleve.go b/repository/index_bleve.go index aae41d5f..40170919 100644 --- a/repository/index_bleve.go +++ b/repository/index_bleve.go @@ -129,6 +129,13 @@ func (b *bleveIndex) DocCount() (uint64, error) { return b.index.DocCount() } +func (b *bleveIndex) Remove(id string) error { + b.mu.Lock() + defer b.mu.Unlock() + + return b.index.Delete(id) +} + func (b *bleveIndex) Clear() error { b.mu.Lock() defer b.mu.Unlock() diff --git a/repository/localstorage_billy.go b/repository/localstorage_billy.go new file mode 100644 index 00000000..ab3909c9 --- /dev/null +++ b/repository/localstorage_billy.go @@ -0,0 +1,16 @@ +package repository + +import ( + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/util" +) + +var _ LocalStorage = &billyLocalStorage{} + +type billyLocalStorage struct { + billy.Filesystem +} + +func (b billyLocalStorage) RemoveAll(path string) error { + return util.RemoveAll(b.Filesystem, path) +} diff --git a/repository/mock_repo.go b/repository/mock_repo.go index c2cef8ef..6ea5c71e 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -9,7 +9,6 @@ import ( "github.com/99designs/keyring" "github.com/ProtonMail/go-crypto/openpgp" - "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" "github.com/MichaelMure/git-bug/util/lamport" @@ -123,14 +122,14 @@ func (r *mockRepoCommon) GetRemotes() (map[string]string, error) { var _ RepoStorage = &mockRepoStorage{} type mockRepoStorage struct { - localFs billy.Filesystem + localFs LocalStorage } func NewMockRepoStorage() *mockRepoStorage { - return &mockRepoStorage{localFs: memfs.New()} + return &mockRepoStorage{localFs: billyLocalStorage{Filesystem: memfs.New()}} } -func (m *mockRepoStorage) LocalStorage() billy.Filesystem { +func (m *mockRepoStorage) LocalStorage() LocalStorage { return m.localFs } @@ -204,6 +203,11 @@ func (m *mockIndex) DocCount() (uint64, error) { return uint64(len(*m)), nil } +func (m *mockIndex) Remove(id string) error { + delete(*m, id) + return nil +} + func (m *mockIndex) Clear() error { for k, _ := range *m { delete(*m, k) diff --git a/repository/repo.go b/repository/repo.go index 66baec65..c39051d5 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -76,10 +76,15 @@ type RepoCommon interface { GetRemotes() (map[string]string, error) } +type LocalStorage interface { + billy.Filesystem + RemoveAll(path string) error +} + // RepoStorage give access to the filesystem type RepoStorage interface { // LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug - LocalStorage() billy.Filesystem + LocalStorage() LocalStorage } // RepoIndex gives access to full-text search indexes @@ -103,6 +108,9 @@ type Index interface { // DocCount returns the number of document in the index. DocCount() (uint64, error) + // Remove delete one document in the index. + Remove(id string) error + // Clear empty the index. Clear() error diff --git a/repository/repo_testing.go b/repository/repo_testing.go index 821eb762..3dd702f4 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -2,6 +2,7 @@ package repository import ( "math/rand" + "os" "testing" "github.com/ProtonMail/go-crypto/openpgp" @@ -10,8 +11,6 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) -// TODO: add tests for RepoStorage - type RepoCreator func(t testing.TB, bare bool) TestedRepo // Test suite for a Repo implementation @@ -32,6 +31,10 @@ func RepoTest(t *testing.T, creator RepoCreator) { RepoConfigTest(t, repo) }) + t.Run("Storage", func(t *testing.T) { + RepoStorageTest(t, repo) + }) + t.Run("Index", func(t *testing.T) { RepoIndexTest(t, repo) }) @@ -48,6 +51,36 @@ func RepoConfigTest(t *testing.T, repo RepoConfig) { testConfig(t, repo.LocalConfig()) } +func RepoStorageTest(t *testing.T, repo RepoStorage) { + storage := repo.LocalStorage() + + err := storage.MkdirAll("foo/bar", 0755) + require.NoError(t, err) + + f, err := storage.Create("foo/bar/foofoo") + require.NoError(t, err) + + _, err = f.Write([]byte("hello")) + require.NoError(t, err) + + err = f.Close() + require.NoError(t, err) + + // remove all + err = storage.RemoveAll(".") + require.NoError(t, err) + + fi, err := storage.ReadDir(".") + // a real FS would remove the root directory with RemoveAll and subsequent call would fail + // a memory FS would still have a virtual root and subsequent call would succeed + // not ideal, but will do for now + if err == nil { + require.Empty(t, fi) + } else { + require.True(t, os.IsNotExist(err)) + } +} + func randomHash() Hash { var letterRunes = "abcdef0123456789" b := make([]byte, idLengthSHA256) |