aboutsummaryrefslogtreecommitdiffstats
path: root/commands/execenv/env.go
blob: 990bd726ebd93f512f127416787ee413717f92c6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package execenv

import (
	"encoding/json"
	"fmt"
	"io"
	"os"

	"github.com/spf13/cobra"
	"github.com/vbauerster/mpb/v8"
	"github.com/vbauerster/mpb/v8/decor"

	"github.com/MichaelMure/git-bug/cache"
	"github.com/MichaelMure/git-bug/entities/identity"
	"github.com/MichaelMure/git-bug/repository"
	"github.com/MichaelMure/git-bug/util/interrupt"
)

const RootCommandName = "git-bug"

const gitBugNamespace = "git-bug"

type IOMode int

const (
	UnknownIOMode IOMode = iota
	TerminalIOMode
	PipedOrRedirectedIOMode
)

func getIOMode(io *os.File) IOMode {
	info, err := io.Stat()
	if err != nil {
		panic("only os.StdIn or os.Stdout should be passed to this method")
	}

	if (info.Mode() & os.ModeCharDevice) == os.ModeCharDevice {
		return TerminalIOMode
	}

	return PipedOrRedirectedIOMode
}

// Env is the environment of a command
type Env struct {
	Repo    repository.ClockedRepo
	Backend *cache.RepoCache
	In      io.Reader
	InMode  IOMode
	Out     Out
	OutMode IOMode
	Err     Out
}

func NewEnv() *Env {
	return &Env{
		Repo:    nil,
		In:      os.Stdin,
		InMode:  getIOMode(os.Stdin),
		Out:     out{Writer: os.Stdout},
		OutMode: getIOMode(os.Stdout),
		Err:     out{Writer: os.Stderr},
	}
}

type Out interface {
	io.Writer
	Printf(format string, a ...interface{})
	Print(a ...interface{})
	Println(a ...interface{})
	PrintJSON(v interface{}) error

	// String returns what have been written in the output before, as a string.
	// This only works in test scenario.
	String() string
	// Bytes returns what have been written in the output before, as []byte.
	// This only works in test scenario.
	Bytes() []byte
	// Reset clear what has been recorded as written in the output before.
	// This only works in test scenario.
	Reset()

	// Raw return the underlying io.Writer, or itself if not.
	// This is useful if something need to access the raw file descriptor.
	Raw() io.Writer
}

type out struct {
	io.Writer
}

func (o out) Printf(format string, a ...interface{}) {
	_, _ = fmt.Fprintf(o, format, a...)
}

func (o out) Print(a ...interface{}) {
	_, _ = fmt.Fprint(o, a...)
}

func (o out) Println(a ...interface{}) {
	_, _ = fmt.Fprintln(o, a...)
}

func (o out) PrintJSON(v interface{}) error {
	raw, err := json.MarshalIndent(v, "", "    ")
	if err != nil {
		return err
	}
	o.Println(string(raw))
	return nil
}

func (o out) String() string {
	panic("only work with a test env")
}

func (o out) Bytes() []byte {
	panic("only work with a test env")
}

func (o out) Reset() {
	panic("only work with a test env")
}

func (o out) Raw() io.Writer {
	return o.Writer
}

// LoadRepo is a pre-run function that load the repository for use in a command
func LoadRepo(env *Env) func(*cobra.Command, []string) error {
	return func(cmd *cobra.Command, args []string) error {
		cwd, err := os.Getwd()
		if err != nil {
			return fmt.Errorf("unable to get the current working directory: %q", err)
		}

		// Note: we are not loading clocks here because we assume that LoadRepo is only used
		//  when we don't manipulate entities, or as a child call of LoadBackend which will
		//  read all clocks anyway.
		env.Repo, err = repository.OpenGoGitRepo(cwd, gitBugNamespace, nil)
		if err == repository.ErrNotARepo {
			return fmt.Errorf("%s must be run from within a git Repo", RootCommandName)
		}
		if err != nil {
			return err
		}

		return nil
	}
}

// LoadRepoEnsureUser is the same as LoadRepo, but also ensure that the user has configured
// an identity. Use this pre-run function when an error after using the configured user won't
// do.
func LoadRepoEnsureUser(env *Env) func(*cobra.Command, []string) error {
	return func(cmd *cobra.Command, args []string) error {
		err := LoadRepo(env)(cmd, args)
		if err != nil {
			return err
		}

		_, err = identity.GetUserIdentity(env.Repo)
		if err != nil {
			return err
		}

		return nil
	}
}

// LoadBackend is a pre-run function that load the repository and the Backend for use in a command
// When using this function you also need to use CloseBackend as a post-run
func LoadBackend(env *Env) func(*cobra.Command, []string) error {
	return func(cmd *cobra.Command, args []string) error {
		err := LoadRepo(env)(cmd, args)
		if err != nil {
			return err
		}

		var events chan cache.BuildEvent
		env.Backend, events = cache.NewRepoCache(env.Repo)

		err = CacheBuildProgressBar(env, events)
		if err != nil {
			return err
		}

		cleaner := func(env *Env) interrupt.CleanerFunc {
			return func() error {
				if env.Backend != nil {
					err := env.Backend.Close()
					env.Backend = nil
					return err
				}
				return nil
			}
		}

		// Cleanup properly on interrupt
		interrupt.RegisterCleaner(cleaner(env))
		return nil
	}
}

// LoadBackendEnsureUser is the same as LoadBackend, but also ensure that the user has configured
// an identity. Use this pre-run function when an error after using the configured user won't
// do.
func LoadBackendEnsureUser(env *Env) func(*cobra.Command, []string) error {
	return func(cmd *cobra.Command, args []string) error {
		err := LoadBackend(env)(cmd, args)
		if err != nil {
			return err
		}

		_, err = identity.GetUserIdentity(env.Repo)
		if err != nil {
			return err
		}

		return nil
	}
}

// CloseBackend is a wrapper for a RunE function that will close the Backend properly
// if it has been opened.
// This wrapper style is necessary because a Cobra PostE function does not run if RunE return an error.
func CloseBackend(env *Env, runE func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) error {
	return func(cmd *cobra.Command, args []string) error {
		errRun := runE(cmd, args)

		if env.Backend == nil {
			return nil
		}
		err := env.Backend.Close()
		env.Backend = nil

		// prioritize the RunE error
		if errRun != nil {
			return errRun
		}
		return err
	}
}

func CacheBuildProgressBar(env *Env, events chan cache.BuildEvent) error {
	var progress *mpb.Progress
	var bars = make(map[string]*mpb.Bar)

	for event := range events {
		if event.Err != nil {
			return event.Err
		}

		if progress == nil {
			progress = mpb.New(mpb.WithOutput(env.Err.Raw()))
		}

		switch event.Event {
		case cache.BuildEventCacheIsBuilt:
			env.Err.Println("Building cache... ")
		case cache.BuildEventStarted:
			bars[event.Typename] = progress.AddBar(-1,
				mpb.BarRemoveOnComplete(),
				mpb.PrependDecorators(
					decor.Name(event.Typename, decor.WCSyncSpace),
					decor.CountersNoUnit("%d / %d", decor.WCSyncSpace),
				),
				mpb.AppendDecorators(decor.Percentage(decor.WCSyncSpace)),
			)
		case cache.BuildEventProgress:
			bars[event.Typename].SetTotal(event.Total, false)
			bars[event.Typename].SetCurrent(event.Progress)
		}
	}

	if progress != nil {
		progress.Shutdown()
	}

	return nil
}