1package commands
2
3import (
4 "fmt"
5 "io"
6 "os"
7
8 "github.com/spf13/cobra"
9
10 "github.com/MichaelMure/git-bug/bug"
11 "github.com/MichaelMure/git-bug/cache"
12 "github.com/MichaelMure/git-bug/identity"
13 "github.com/MichaelMure/git-bug/repository"
14 "github.com/MichaelMure/git-bug/util/interrupt"
15)
16
17const GitBugNamespace = "git-bug"
18
19// Env is the environment of a command
20type Env struct {
21 repo repository.ClockedRepo
22 backend *cache.RepoCache
23 out out
24 err out
25}
26
27func newEnv() *Env {
28 return &Env{
29 repo: nil,
30 out: out{Writer: os.Stdout},
31 err: out{Writer: os.Stderr},
32 }
33}
34
35type out struct {
36 io.Writer
37}
38
39func (o out) Printf(format string, a ...interface{}) {
40 _, _ = fmt.Fprintf(o, format, a...)
41}
42
43func (o out) Print(a ...interface{}) {
44 _, _ = fmt.Fprint(o, a...)
45}
46
47func (o out) Println(a ...interface{}) {
48 _, _ = fmt.Fprintln(o, a...)
49}
50
51// getCWD returns the current working directory. Normal operation simply
52// returns the working directory reported by the OS (as an OS-compatible
53// filepath.) During tests, temporary repositories are created outside
54// the test execution's CWD. In this case, it's possible to provide an
55// alternate CWD filepath by adding a value to the command's context
56// with the key "cwd".
57func getCWD(cmd *cobra.Command) (string, error) {
58 cwd, ok := cmd.Context().Value("cwd").(string)
59 if cwd != "" && ok {
60 return cwd, nil
61 }
62
63 cwd, err := os.Getwd()
64 if err != nil {
65 return "", fmt.Errorf("unable to get the current working directory: %q", err)
66 }
67
68 return cwd, nil
69}
70
71// loadRepo is a pre-run function that load the repository for use in a command
72func loadRepo(env *Env) func(*cobra.Command, []string) error {
73 return func(cmd *cobra.Command, args []string) error {
74 cwd, err := getCWD(cmd)
75 if err != nil {
76 return err
77 }
78
79 env.repo, err = repository.OpenGoGitRepo(cwd, GitBugNamespace, []repository.ClockLoader{bug.ClockLoader})
80 if err == repository.ErrNotARepo {
81 return fmt.Errorf("%s must be run from within a git repo", rootCommandName)
82 }
83
84 if err != nil {
85 return err
86 }
87
88 return nil
89 }
90}
91
92// loadRepoEnsureUser is the same as loadRepo, but also ensure that the user has configured
93// an identity. Use this pre-run function when an error after using the configured user won't
94// do.
95func loadRepoEnsureUser(env *Env) func(*cobra.Command, []string) error {
96 return func(cmd *cobra.Command, args []string) error {
97 err := loadRepo(env)(cmd, args)
98 if err != nil {
99 return err
100 }
101
102 _, err = identity.GetUserIdentity(env.repo)
103 if err != nil {
104 return err
105 }
106
107 return nil
108 }
109}
110
111// loadBackend is a pre-run function that load the repository and the backend for use in a command
112// When using this function you also need to use closeBackend as a post-run
113func loadBackend(env *Env) func(*cobra.Command, []string) error {
114 return func(cmd *cobra.Command, args []string) error {
115 err := loadRepo(env)(cmd, args)
116 if err != nil {
117 return err
118 }
119
120 env.backend, err = cache.NewRepoCache(env.repo)
121 if err != nil {
122 return err
123 }
124
125 cleaner := func(env *Env) interrupt.CleanerFunc {
126 return func() error {
127 if env.backend != nil {
128 err := env.backend.Close()
129 env.backend = nil
130 return err
131 }
132 return nil
133 }
134 }
135
136 // Cleanup properly on interrupt
137 interrupt.RegisterCleaner(cleaner(env))
138 return nil
139 }
140}
141
142// loadBackendEnsureUser is the same as loadBackend, but also ensure that the user has configured
143// an identity. Use this pre-run function when an error after using the configured user won't
144// do.
145func loadBackendEnsureUser(env *Env) func(*cobra.Command, []string) error {
146 return func(cmd *cobra.Command, args []string) error {
147 err := loadBackend(env)(cmd, args)
148 if err != nil {
149 return err
150 }
151
152 _, err = identity.GetUserIdentity(env.repo)
153 if err != nil {
154 return err
155 }
156
157 return nil
158 }
159}
160
161// closeBackend is a wrapper for a RunE function that will close the backend properly
162// if it has been opened.
163// This wrapper style is necessary because a Cobra PostE function does not run if RunE return an error.
164func closeBackend(env *Env, runE func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) error {
165 return func(cmd *cobra.Command, args []string) error {
166 env.err = out{Writer: cmd.ErrOrStderr()}
167 env.out = out{Writer: cmd.OutOrStdout()}
168
169 errRun := runE(cmd, args)
170
171 if env.backend == nil {
172 return nil
173 }
174 err := env.backend.Close()
175 env.backend = nil
176
177 // prioritize the RunE error
178 if errRun != nil {
179 return errRun
180 }
181 return err
182 }
183}