1package _select
2
3import (
4 "fmt"
5 "io"
6 "os"
7 "path/filepath"
8
9 "github.com/pkg/errors"
10
11 "github.com/MichaelMure/git-bug/cache"
12 "github.com/MichaelMure/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// - the entity if any
42// - the new list of command line arguments with the entity prefix removed if it
43// has been used
44// - an error if the process failed
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), nil, 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), nil, err
71 }
72 return *new(CacheT), nil, NewErrNoValidId(typename)
73 }
74
75 // another error when reading the entity
76 if err != nil {
77 return *new(CacheT), nil, 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), nil, 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}