1package execenv
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "os"
8
9 "github.com/spf13/cobra"
10
11 "github.com/MichaelMure/git-bug/cache"
12 "github.com/MichaelMure/git-bug/entities/identity"
13 "github.com/MichaelMure/git-bug/repository"
14 "github.com/MichaelMure/git-bug/util/interrupt"
15)
16
17const RootCommandName = "git-bug"
18
19const gitBugNamespace = "git-bug"
20
21// Env is the environment of a command
22type Env struct {
23 Repo repository.ClockedRepo
24 Backend *cache.RepoCache
25 Out Out
26 Err Out
27}
28
29func NewEnv() *Env {
30 return &Env{
31 Repo: nil,
32 Out: out{Writer: os.Stdout},
33 Err: out{Writer: os.Stderr},
34 }
35}
36
37type Out interface {
38 io.Writer
39 Printf(format string, a ...interface{})
40 Print(a ...interface{})
41 Println(a ...interface{})
42 PrintJSON(v interface{}) error
43
44 // String returns what have been written in the output before, as a string.
45 // This only works in test scenario.
46 String() string
47 // Bytes returns what have been written in the output before, as []byte.
48 // This only works in test scenario.
49 Bytes() []byte
50 // Reset clear what has been recorded as written in the output before.
51 // This only works in test scenario.
52 Reset()
53}
54
55type out struct {
56 io.Writer
57}
58
59func (o out) Printf(format string, a ...interface{}) {
60 _, _ = fmt.Fprintf(o, format, a...)
61}
62
63func (o out) Print(a ...interface{}) {
64 _, _ = fmt.Fprint(o, a...)
65}
66
67func (o out) Println(a ...interface{}) {
68 _, _ = fmt.Fprintln(o, a...)
69}
70
71func (o out) PrintJSON(v interface{}) error {
72 raw, err := json.MarshalIndent(v, "", " ")
73 if err != nil {
74 return err
75 }
76 o.Println(string(raw))
77 return nil
78}
79
80func (o out) String() string {
81 panic("only work with a test env")
82}
83
84func (o out) Bytes() []byte {
85 panic("only work with a test env")
86}
87
88func (o out) Reset() {
89 panic("only work with a test env")
90}
91
92// LoadRepo is a pre-run function that load the repository for use in a command
93func LoadRepo(env *Env) func(*cobra.Command, []string) error {
94 return func(cmd *cobra.Command, args []string) error {
95 cwd, err := os.Getwd()
96 if err != nil {
97 return fmt.Errorf("unable to get the current working directory: %q", err)
98 }
99
100 // Note: we are not loading clocks here because we assume that LoadRepo is only used
101 // when we don't manipulate entities, or as a child call of LoadBackend which will
102 // read all clocks anyway.
103 env.Repo, err = repository.OpenGoGitRepo(cwd, gitBugNamespace, nil)
104 if err == repository.ErrNotARepo {
105 return fmt.Errorf("%s must be run from within a git Repo", RootCommandName)
106 }
107 if err != nil {
108 return err
109 }
110
111 return nil
112 }
113}
114
115// LoadRepoEnsureUser is the same as LoadRepo, but also ensure that the user has configured
116// an identity. Use this pre-run function when an error after using the configured user won't
117// do.
118func LoadRepoEnsureUser(env *Env) func(*cobra.Command, []string) error {
119 return func(cmd *cobra.Command, args []string) error {
120 err := LoadRepo(env)(cmd, args)
121 if err != nil {
122 return err
123 }
124
125 _, err = identity.GetUserIdentity(env.Repo)
126 if err != nil {
127 return err
128 }
129
130 return nil
131 }
132}
133
134// LoadBackend is a pre-run function that load the repository and the Backend for use in a command
135// When using this function you also need to use CloseBackend as a post-run
136func LoadBackend(env *Env) func(*cobra.Command, []string) error {
137 return func(cmd *cobra.Command, args []string) error {
138 err := LoadRepo(env)(cmd, args)
139 if err != nil {
140 return err
141 }
142
143 var events chan cache.BuildEvent
144 env.Backend, events = cache.NewRepoCache(env.Repo)
145
146 for event := range events {
147 if event.Err != nil {
148 return event.Err
149 }
150 switch event.Event {
151 case cache.BuildEventCacheIsBuilt:
152 env.Err.Println("Building cache... ")
153 case cache.BuildEventStarted:
154 env.Err.Printf("[%s] started\n", event.Typename)
155 case cache.BuildEventFinished:
156 env.Err.Printf("[%s] done\n", event.Typename)
157 }
158 }
159
160 cleaner := func(env *Env) interrupt.CleanerFunc {
161 return func() error {
162 if env.Backend != nil {
163 err := env.Backend.Close()
164 env.Backend = nil
165 return err
166 }
167 return nil
168 }
169 }
170
171 // Cleanup properly on interrupt
172 interrupt.RegisterCleaner(cleaner(env))
173 return nil
174 }
175}
176
177// LoadBackendEnsureUser is the same as LoadBackend, but also ensure that the user has configured
178// an identity. Use this pre-run function when an error after using the configured user won't
179// do.
180func LoadBackendEnsureUser(env *Env) func(*cobra.Command, []string) error {
181 return func(cmd *cobra.Command, args []string) error {
182 err := LoadBackend(env)(cmd, args)
183 if err != nil {
184 return err
185 }
186
187 _, err = identity.GetUserIdentity(env.Repo)
188 if err != nil {
189 return err
190 }
191
192 return nil
193 }
194}
195
196// CloseBackend is a wrapper for a RunE function that will close the Backend properly
197// if it has been opened.
198// This wrapper style is necessary because a Cobra PostE function does not run if RunE return an error.
199func CloseBackend(env *Env, runE func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) error {
200 return func(cmd *cobra.Command, args []string) error {
201 errRun := runE(cmd, args)
202
203 if env.Backend == nil {
204 return nil
205 }
206 err := env.Backend.Close()
207 env.Backend = nil
208
209 // prioritize the RunE error
210 if errRun != nil {
211 return errRun
212 }
213 return err
214 }
215}