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