aboutsummaryrefslogtreecommitdiffstats
path: root/commands/select/select.go
blob: 694d636a4399c5a93c106392f7428f9179fdeee2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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
}