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