From e920987860dd9392fefc4b222aa4d2446b74f3d8 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Tue, 27 Dec 2022 19:40:40 +0100 Subject: commands: generic "select" code, move bug completion in bugcmd --- commands/select/select.go | 156 +++++++++++++++++++++++++++++++++++++++++ commands/select/select_test.go | 86 +++++++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 commands/select/select.go create mode 100644 commands/select/select_test.go (limited to 'commands/select') diff --git a/commands/select/select.go b/commands/select/select.go new file mode 100644 index 00000000..b821ba59 --- /dev/null +++ b/commands/select/select.go @@ -0,0 +1,156 @@ +package _select + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/pkg/errors" + + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/entity" +) + +type ErrNoValidId struct { + typename string +} + +func NewErrNoValidId(typename string) *ErrNoValidId { + return &ErrNoValidId{typename: typename} +} + +func (e ErrNoValidId) Error() string { + return fmt.Sprintf("you must provide a %s id or use the \"select\" command first", e.typename) +} + +func IsErrNoValidId(err error) bool { + _, ok := err.(*ErrNoValidId) + return ok +} + +type Resolver[CacheT cache.CacheEntity] interface { + Resolve(id entity.Id) (CacheT, error) + ResolvePrefix(prefix string) (CacheT, error) +} + +// Resolve first try to resolve an entity using the first argument of the command +// line. If it fails, it falls back to the select mechanism. +// +// Returns: +// - the entity if any +// - the new list of command line arguments with the entity prefix removed if it +// has been used +// - an error if the process failed +func Resolve[CacheT cache.CacheEntity](repo *cache.RepoCache, + typename string, namespace string, resolver Resolver[CacheT], + args []string) (CacheT, []string, error) { + // At first, try to use the first argument as an entity prefix + if len(args) > 0 { + cached, err := resolver.ResolvePrefix(args[0]) + + if err == nil { + return cached, args[1:], nil + } + + if !entity.IsErrNotFound(err) { + return *new(CacheT), nil, err + } + } + + // first arg is not a valid entity prefix, we can safely use the preselected entity if any + + cached, err := selected(repo, resolver, namespace) + + // selected entity is invalid + if entity.IsErrNotFound(err) { + // we clear the selected bug + err = Clear(repo, namespace) + if err != nil { + return *new(CacheT), nil, err + } + return *new(CacheT), nil, NewErrNoValidId(typename) + } + + // another error when reading the entity + if err != nil { + return *new(CacheT), nil, err + } + + // entity is successfully retrieved + if cached != nil { + return *cached, args, nil + } + + // no selected bug and no valid first argument + return *new(CacheT), nil, NewErrNoValidId(typename) +} + +func selectFileName(namespace string) string { + return filepath.Join("select", namespace) +} + +// Select will select a bug for future use +func Select(repo *cache.RepoCache, namespace string, id entity.Id) error { + filename := selectFileName(namespace) + f, err := repo.LocalStorage().OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return err + } + + _, err = f.Write([]byte(id.String())) + if err != nil { + return err + } + + return f.Close() +} + +// Clear will clear the selected entity, if any +func Clear(repo *cache.RepoCache, namespace string) error { + filename := selectFileName(namespace) + return repo.LocalStorage().Remove(filename) +} + +func selected[CacheT cache.CacheEntity](repo *cache.RepoCache, resolver Resolver[CacheT], namespace string) (*CacheT, error) { + filename := selectFileName(namespace) + f, err := repo.LocalStorage().Open(filename) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } else { + return nil, err + } + } + + buf, err := ioutil.ReadAll(io.LimitReader(f, 100)) + if err != nil { + return nil, err + } + if len(buf) == 100 { + return nil, fmt.Errorf("the select file should be < 100 bytes") + } + + id := entity.Id(buf) + if err := id.Validate(); err != nil { + err = repo.LocalStorage().Remove(filename) + if err != nil { + return nil, errors.Wrap(err, "error while removing invalid select file") + } + + return nil, fmt.Errorf("select file in invalid, removing it") + } + + cached, err := resolver.Resolve(id) + if err != nil { + return nil, err + } + + err = f.Close() + if err != nil { + return nil, err + } + + return &cached, nil +} diff --git a/commands/select/select_test.go b/commands/select/select_test.go new file mode 100644 index 00000000..4425c275 --- /dev/null +++ b/commands/select/select_test.go @@ -0,0 +1,86 @@ +package _select + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/repository" +) + +func TestSelect(t *testing.T) { + repo := repository.CreateGoGitTestRepo(t, false) + + backend, err := cache.NewRepoCacheNoEvents(repo) + require.NoError(t, err) + + const typename = "foo" + const namespace = "foos" + + resolve := func(args []string) (*cache.BugCache, []string, error) { + return Resolve[*cache.BugCache](backend, typename, namespace, backend.Bugs(), args) + } + + _, _, err = resolve([]string{}) + require.True(t, IsErrNoValidId(err)) + + err = Select(backend, namespace, "invalid") + require.NoError(t, err) + + // Resolve without a pattern should fail when no bug is selected + _, _, err = resolve([]string{}) + require.Error(t, err) + + // generate a bunch of bugs + + rene, err := backend.Identities().New("René Descartes", "rene@descartes.fr") + require.NoError(t, err) + + for i := 0; i < 10; i++ { + _, _, err := backend.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil) + require.NoError(t, err) + } + + // and two more for testing + b1, _, err := backend.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil) + require.NoError(t, err) + b2, _, err := backend.Bugs().NewRaw(rene, time.Now().Unix(), "title", "message", nil, nil) + require.NoError(t, err) + + err = Select(backend, namespace, b1.Id()) + require.NoError(t, err) + + // normal select without args + b3, _, err := resolve([]string{}) + require.NoError(t, err) + require.Equal(t, b1.Id(), b3.Id()) + + // override selection with same id + b4, _, err := resolve([]string{b1.Id().String()}) + require.NoError(t, err) + require.Equal(t, b1.Id(), b4.Id()) + + // override selection with a prefix + b5, _, err := resolve([]string{b1.Id().Human()}) + require.NoError(t, err) + require.Equal(t, b1.Id(), b5.Id()) + + // args that shouldn't override + b6, _, err := resolve([]string{"arg"}) + require.NoError(t, err) + require.Equal(t, b1.Id(), b6.Id()) + + // override with a different id + b7, _, err := resolve([]string{b2.Id().String()}) + require.NoError(t, err) + require.Equal(t, b2.Id(), b7.Id()) + + err = Clear(backend, namespace) + require.NoError(t, err) + + // Resolve without a pattern should error again after clearing the selected bug + _, _, err = resolve([]string{}) + require.Error(t, err) +} -- cgit