select.go

  1package _select
  2
  3import (
  4	"fmt"
  5	"io"
  6	"io/ioutil"
  7	"os"
  8	"path/filepath"
  9
 10	"github.com/pkg/errors"
 11
 12	"github.com/MichaelMure/git-bug/cache"
 13	"github.com/MichaelMure/git-bug/entity"
 14)
 15
 16type ErrNoValidId struct {
 17	typename string
 18}
 19
 20func NewErrNoValidId(typename string) *ErrNoValidId {
 21	return &ErrNoValidId{typename: typename}
 22}
 23
 24func (e ErrNoValidId) Error() string {
 25	return fmt.Sprintf("you must provide a %s id or use the \"select\" command first", e.typename)
 26}
 27
 28func IsErrNoValidId(err error) bool {
 29	_, ok := err.(*ErrNoValidId)
 30	return ok
 31}
 32
 33type Resolver[CacheT cache.CacheEntity] interface {
 34	Resolve(id entity.Id) (CacheT, error)
 35	ResolvePrefix(prefix string) (CacheT, error)
 36}
 37
 38// Resolve first try to resolve an entity using the first argument of the command
 39// line. If it fails, it falls back to the select mechanism.
 40//
 41// Returns:
 42//   - the entity if any
 43//   - the new list of command line arguments with the entity prefix removed if it
 44//     has been used
 45//   - an error if the process failed
 46func Resolve[CacheT cache.CacheEntity](repo *cache.RepoCache,
 47	typename string, namespace string, resolver Resolver[CacheT],
 48	args []string) (CacheT, []string, error) {
 49	// At first, try to use the first argument as an entity prefix
 50	if len(args) > 0 {
 51		cached, err := resolver.ResolvePrefix(args[0])
 52
 53		if err == nil {
 54			return cached, args[1:], nil
 55		}
 56
 57		if !entity.IsErrNotFound(err) {
 58			return *new(CacheT), nil, err
 59		}
 60	}
 61
 62	// first arg is not a valid entity prefix, we can safely use the preselected entity if any
 63
 64	cached, err := selected(repo, resolver, namespace)
 65
 66	// selected entity is invalid
 67	if entity.IsErrNotFound(err) {
 68		// we clear the selected bug
 69		err = Clear(repo, namespace)
 70		if err != nil {
 71			return *new(CacheT), nil, err
 72		}
 73		return *new(CacheT), nil, NewErrNoValidId(typename)
 74	}
 75
 76	// another error when reading the entity
 77	if err != nil {
 78		return *new(CacheT), nil, err
 79	}
 80
 81	// entity is successfully retrieved
 82	if cached != nil {
 83		return *cached, args, nil
 84	}
 85
 86	// no selected bug and no valid first argument
 87	return *new(CacheT), nil, NewErrNoValidId(typename)
 88}
 89
 90func selectFileName(namespace string) string {
 91	return filepath.Join("select", namespace)
 92}
 93
 94// Select will select a bug for future use
 95func Select(repo *cache.RepoCache, namespace string, id entity.Id) error {
 96	filename := selectFileName(namespace)
 97	f, err := repo.LocalStorage().OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
 98	if err != nil {
 99		return err
100	}
101
102	_, err = f.Write([]byte(id.String()))
103	if err != nil {
104		return err
105	}
106
107	return f.Close()
108}
109
110// Clear will clear the selected entity, if any
111func Clear(repo *cache.RepoCache, namespace string) error {
112	filename := selectFileName(namespace)
113	return repo.LocalStorage().Remove(filename)
114}
115
116func selected[CacheT cache.CacheEntity](repo *cache.RepoCache, resolver Resolver[CacheT], namespace string) (*CacheT, error) {
117	filename := selectFileName(namespace)
118	f, err := repo.LocalStorage().Open(filename)
119	if err != nil {
120		if os.IsNotExist(err) {
121			return nil, nil
122		} else {
123			return nil, err
124		}
125	}
126
127	buf, err := ioutil.ReadAll(io.LimitReader(f, 100))
128	if err != nil {
129		return nil, err
130	}
131	if len(buf) == 100 {
132		return nil, fmt.Errorf("the select file should be < 100 bytes")
133	}
134
135	id := entity.Id(buf)
136	if err := id.Validate(); err != nil {
137		err = repo.LocalStorage().Remove(filename)
138		if err != nil {
139			return nil, errors.Wrap(err, "error while removing invalid select file")
140		}
141
142		return nil, fmt.Errorf("select file in invalid, removing it")
143	}
144
145	cached, err := resolver.Resolve(id)
146	if err != nil {
147		return nil, err
148	}
149
150	err = f.Close()
151	if err != nil {
152		return nil, err
153	}
154
155	return &cached, nil
156}