env.go

  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}