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}