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