env.go

  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}