aboutsummaryrefslogtreecommitdiffstats
path: root/commands/select
diff options
context:
space:
mode:
Diffstat (limited to 'commands/select')
-rw-r--r--commands/select/select.go156
-rw-r--r--commands/select/select_test.go86
2 files changed, 242 insertions, 0 deletions
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)
+}