select.go

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