1package factory 2 3import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "os" 8 "path/filepath" 9 "time" 10 11 "github.com/cli/cli/v2/api" 12 "github.com/cli/cli/v2/context" 13 "github.com/cli/cli/v2/git" 14 "github.com/cli/cli/v2/internal/config" 15 "github.com/cli/cli/v2/internal/ghrepo" 16 "github.com/cli/cli/v2/pkg/cmd/extension" 17 "github.com/cli/cli/v2/pkg/cmdutil" 18 "github.com/cli/cli/v2/pkg/iostreams" 19) 20 21func New(appVersion string) *cmdutil.Factory { 22 var exe string 23 f := &cmdutil.Factory{ 24 Config: configFunc(), // No factory dependencies 25 Branch: branchFunc(), // No factory dependencies 26 Executable: func() string { 27 if exe != "" { 28 return exe 29 } 30 exe = executable("gh") 31 return exe 32 }, 33 } 34 35 f.IOStreams = ioStreams(f) // Depends on Config 36 f.HttpClient = httpClientFunc(f, appVersion) // Depends on Config, IOStreams, and appVersion 37 f.Remotes = remotesFunc(f) // Depends on Config 38 f.BaseRepo = BaseRepoFunc(f) // Depends on Remotes 39 f.Browser = browser(f) // Depends on Config, and IOStreams 40 f.ExtensionManager = extensionManager(f) // Depends on Config, HttpClient, and IOStreams 41 42 return f 43} 44 45func BaseRepoFunc(f *cmdutil.Factory) func() (ghrepo.Interface, error) { 46 return func() (ghrepo.Interface, error) { 47 remotes, err := f.Remotes() 48 if err != nil { 49 return nil, err 50 } 51 return remotes[0], nil 52 } 53} 54 55func SmartBaseRepoFunc(f *cmdutil.Factory) func() (ghrepo.Interface, error) { 56 return func() (ghrepo.Interface, error) { 57 httpClient, err := f.HttpClient() 58 if err != nil { 59 return nil, err 60 } 61 62 apiClient := api.NewClientFromHTTP(httpClient) 63 64 remotes, err := f.Remotes() 65 if err != nil { 66 return nil, err 67 } 68 repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") 69 if err != nil { 70 return nil, err 71 } 72 baseRepo, err := repoContext.BaseRepo(f.IOStreams) 73 if err != nil { 74 return nil, err 75 } 76 77 return baseRepo, nil 78 } 79} 80 81func remotesFunc(f *cmdutil.Factory) func() (context.Remotes, error) { 82 rr := &remoteResolver{ 83 readRemotes: git.Remotes, 84 getConfig: f.Config, 85 } 86 return rr.Resolver() 87} 88 89func httpClientFunc(f *cmdutil.Factory, appVersion string) func() (*http.Client, error) { 90 return func() (*http.Client, error) { 91 io := f.IOStreams 92 cfg, err := f.Config() 93 if err != nil { 94 return nil, err 95 } 96 return NewHTTPClient(io, cfg, appVersion, true) 97 } 98} 99 100func browser(f *cmdutil.Factory) cmdutil.Browser { 101 io := f.IOStreams 102 return cmdutil.NewBrowser(browserLauncher(f), io.Out, io.ErrOut) 103} 104 105// Browser precedence 106// 1. GH_BROWSER 107// 2. browser from config 108// 3. BROWSER 109func browserLauncher(f *cmdutil.Factory) string { 110 if ghBrowser := os.Getenv("GH_BROWSER"); ghBrowser != "" { 111 return ghBrowser 112 } 113 114 cfg, err := f.Config() 115 if err == nil { 116 if cfgBrowser, _ := cfg.Get("", "browser"); cfgBrowser != "" { 117 return cfgBrowser 118 } 119 } 120 121 return os.Getenv("BROWSER") 122} 123 124// Finds the location of the executable for the current process as it's found in PATH, respecting symlinks. 125// If the process couldn't determine its location, return fallbackName. If the executable wasn't found in 126// PATH, return the absolute location to the program. 127// 128// The idea is that the result of this function is callable in the future and refers to the same 129// installation of gh, even across upgrades. This is needed primarily for Homebrew, which installs software 130// under a location such as `/usr/local/Cellar/gh/1.13.1/bin/gh` and symlinks it from `/usr/local/bin/gh`. 131// When the version is upgraded, Homebrew will often delete older versions, but keep the symlink. Because of 132// this, we want to refer to the `gh` binary as `/usr/local/bin/gh` and not as its internal Homebrew 133// location. 134// 135// None of this would be needed if we could just refer to GitHub CLI as `gh`, i.e. without using an absolute 136// path. However, for some reason Homebrew does not include `/usr/local/bin` in PATH when it invokes git 137// commands to update its taps. If `gh` (no path) is being used as git credential helper, as set up by `gh 138// auth login`, running `brew update` will print out authentication errors as git is unable to locate 139// Homebrew-installed `gh`. 140func executable(fallbackName string) string { 141 exe, err := os.Executable() 142 if err != nil { 143 return fallbackName 144 } 145 146 base := filepath.Base(exe) 147 path := os.Getenv("PATH") 148 for _, dir := range filepath.SplitList(path) { 149 p, err := filepath.Abs(filepath.Join(dir, base)) 150 if err != nil { 151 continue 152 } 153 f, err := os.Stat(p) 154 if err != nil { 155 continue 156 } 157 158 if p == exe { 159 return p 160 } else if f.Mode()&os.ModeSymlink != 0 { 161 if t, err := os.Readlink(p); err == nil && t == exe { 162 return p 163 } 164 } 165 } 166 167 return exe 168} 169 170func configFunc() func() (config.Config, error) { 171 var cachedConfig config.Config 172 var configError error 173 return func() (config.Config, error) { 174 if cachedConfig != nil || configError != nil { 175 return cachedConfig, configError 176 } 177 cachedConfig, configError = config.ParseDefaultConfig() 178 if errors.Is(configError, os.ErrNotExist) { 179 cachedConfig = config.NewBlankConfig() 180 configError = nil 181 } 182 cachedConfig = config.InheritEnv(cachedConfig) 183 return cachedConfig, configError 184 } 185} 186 187func branchFunc() func() (string, error) { 188 return func() (string, error) { 189 currentBranch, err := git.CurrentBranch() 190 if err != nil { 191 return "", fmt.Errorf("could not determine current branch: %w", err) 192 } 193 return currentBranch, nil 194 } 195} 196 197func extensionManager(f *cmdutil.Factory) *extension.Manager { 198 em := extension.NewManager(f.IOStreams) 199 200 cfg, err := f.Config() 201 if err != nil { 202 return em 203 } 204 em.SetConfig(cfg) 205 206 client, err := f.HttpClient() 207 if err != nil { 208 return em 209 } 210 211 em.SetClient(api.NewCachedClient(client, time.Second*30)) 212 213 return em 214} 215 216func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams { 217 io := iostreams.System() 218 cfg, err := f.Config() 219 if err != nil { 220 return io 221 } 222 223 if prompt, _ := cfg.Get("", "prompt"); prompt == "disabled" { 224 io.SetNeverPrompt(true) 225 } 226 227 // Pager precedence 228 // 1. GH_PAGER 229 // 2. pager from config 230 // 3. PAGER 231 if ghPager, ghPagerExists := os.LookupEnv("GH_PAGER"); ghPagerExists { 232 io.SetPager(ghPager) 233 } else if pager, _ := cfg.Get("", "pager"); pager != "" { 234 io.SetPager(pager) 235 } 236 237 return io 238} 239