Extract root command and factory logic into separate packages
This commit is contained in:
parent
5e24e0d9b9
commit
aef1a4ba4d
14 changed files with 341 additions and 303 deletions
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/cli/cli/command"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/pkg/cmd/root"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/update"
|
||||
"github.com/cli/cli/utils"
|
||||
|
|
@ -73,7 +74,7 @@ func main() {
|
|||
printError(os.Stderr, err, cmd, hasDebug)
|
||||
os.Exit(1)
|
||||
}
|
||||
if command.HasFailed() {
|
||||
if root.HasFailed() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(aliasCmd)
|
||||
aliasCmd.AddCommand(aliasSetCmd)
|
||||
aliasCmd.AddCommand(aliasListCmd)
|
||||
aliasCmd.AddCommand(aliasDeleteCmd)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(completionCmd)
|
||||
completionCmd.Flags().StringP("shell", "s", "", "Shell type: {bash|zsh|fish|powershell}")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(configCmd)
|
||||
configCmd.AddCommand(configGetCmd)
|
||||
configCmd.AddCommand(configSetCmd)
|
||||
|
||||
|
|
|
|||
243
command/root.go
243
command/root.go
|
|
@ -4,7 +4,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
|
@ -13,27 +12,17 @@ import (
|
|||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
apiCmd "github.com/cli/cli/pkg/cmd/api"
|
||||
gistCmd "github.com/cli/cli/pkg/cmd/gist"
|
||||
issueCmd "github.com/cli/cli/pkg/cmd/issue"
|
||||
prCmd "github.com/cli/cli/pkg/cmd/pr"
|
||||
repoCmd "github.com/cli/cli/pkg/cmd/repo"
|
||||
creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/cmd/factory"
|
||||
"github.com/cli/cli/pkg/cmd/root"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/google/shlex"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Version is dynamically set by the toolchain or overridden by the Makefile.
|
||||
|
|
@ -42,9 +31,7 @@ var Version = "DEV"
|
|||
// BuildDate is dynamically set at build time in the Makefile.
|
||||
var BuildDate = "" // YYYY-MM-DD
|
||||
|
||||
var versionOutput = ""
|
||||
|
||||
var defaultStreams *iostreams.IOStreams
|
||||
var RootCmd *cobra.Command
|
||||
|
||||
func init() {
|
||||
if Version == "DEV" {
|
||||
|
|
@ -52,159 +39,12 @@ func init() {
|
|||
Version = info.Main.Version
|
||||
}
|
||||
}
|
||||
Version = strings.TrimPrefix(Version, "v")
|
||||
if BuildDate == "" {
|
||||
RootCmd.Version = Version
|
||||
} else {
|
||||
RootCmd.Version = fmt.Sprintf("%s (%s)", Version, BuildDate)
|
||||
}
|
||||
versionOutput = fmt.Sprintf("gh version %s\n%s\n", RootCmd.Version, changelogURL(Version))
|
||||
RootCmd.AddCommand(versionCmd)
|
||||
RootCmd.SetVersionTemplate(versionOutput)
|
||||
|
||||
RootCmd.PersistentFlags().Bool("help", false, "Show help for command")
|
||||
RootCmd.Flags().Bool("version", false, "Show gh version")
|
||||
// TODO:
|
||||
// RootCmd.PersistentFlags().BoolP("verbose", "V", false, "enable verbose output")
|
||||
|
||||
RootCmd.SetHelpFunc(rootHelpFunc)
|
||||
RootCmd.SetUsageFunc(rootUsageFunc)
|
||||
|
||||
RootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
||||
if err == pflag.ErrHelp {
|
||||
return err
|
||||
}
|
||||
return &cmdutil.FlagError{Err: err}
|
||||
})
|
||||
|
||||
defaultStreams = iostreams.System()
|
||||
|
||||
// TODO: iron out how a factory incorporates context
|
||||
cmdFactory := &cmdutil.Factory{
|
||||
IOStreams: defaultStreams,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
// TODO: decouple from `context`
|
||||
ctx := context.New()
|
||||
cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: avoid setting Accept header for `api` command
|
||||
return httpClient(defaultStreams, cfg, true), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
// TODO: decouple from `context`
|
||||
ctx := context.New()
|
||||
return ctx.BaseRepo()
|
||||
},
|
||||
Remotes: func() (context.Remotes, error) {
|
||||
ctx := context.New()
|
||||
return ctx.Remotes()
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
cfg, err := config.ParseDefaultConfig()
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
cfg = config.NewBlankConfig()
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
currentBranch, err := git.CurrentBranch()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not determine current branch: %w", err)
|
||||
}
|
||||
return currentBranch, nil
|
||||
},
|
||||
}
|
||||
|
||||
RootCmd.AddCommand(apiCmd.NewCmdApi(cmdFactory, nil))
|
||||
RootCmd.AddCommand(gistCmd.NewCmdGist(cmdFactory))
|
||||
|
||||
resolvedBaseRepo := func() (ghrepo.Interface, error) {
|
||||
httpClient, err := cmdFactory.HttpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
ctx := context.New()
|
||||
remotes, err := ctx.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseRepo, err := repoContext.BaseRepo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return baseRepo, nil
|
||||
}
|
||||
|
||||
repoResolvingCmdFactory := *cmdFactory
|
||||
|
||||
repoResolvingCmdFactory.BaseRepo = resolvedBaseRepo
|
||||
|
||||
RootCmd.AddCommand(prCmd.NewCmdPR(&repoResolvingCmdFactory))
|
||||
RootCmd.AddCommand(issueCmd.NewCmdIssue(&repoResolvingCmdFactory))
|
||||
RootCmd.AddCommand(repoCmd.NewCmdRepo(&repoResolvingCmdFactory))
|
||||
RootCmd.AddCommand(creditsCmd.NewCmdCredits(cmdFactory, nil))
|
||||
}
|
||||
|
||||
// RootCmd is the entry point of command-line execution
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "gh <command> <subcommand> [flags]",
|
||||
Short: "GitHub CLI",
|
||||
Long: `Work seamlessly with GitHub from the command line.`,
|
||||
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
Example: heredoc.Doc(`
|
||||
$ gh issue create
|
||||
$ gh repo clone cli/cli
|
||||
$ gh pr checkout 321
|
||||
`),
|
||||
Annotations: map[string]string{
|
||||
"help:feedback": heredoc.Doc(`
|
||||
Fill out our feedback form https://forms.gle/umxd3h31c7aMQFKG7
|
||||
Open an issue using “gh issue create -R cli/cli”
|
||||
`),
|
||||
"help:environment": heredoc.Doc(`
|
||||
GITHUB_TOKEN: an authentication token for API requests. Setting this avoids being
|
||||
prompted to authenticate and overrides any previously stored credentials.
|
||||
|
||||
GH_REPO: specify the GitHub repository in "OWNER/REPO" format for commands that
|
||||
otherwise operate on a local repository.
|
||||
|
||||
GH_EDITOR, GIT_EDITOR, VISUAL, EDITOR (in order of precedence): the editor tool to use
|
||||
for authoring text.
|
||||
|
||||
BROWSER: the web browser to use for opening links.
|
||||
|
||||
DEBUG: set to any value to enable verbose output to standard error. Include values "api"
|
||||
or "oauth" to print detailed information about HTTP requests or authentication flow.
|
||||
|
||||
GLAMOUR_STYLE: the style to use for rendering Markdown. See
|
||||
https://github.com/charmbracelet/glamour#styles
|
||||
|
||||
NO_COLOR: avoid printing ANSI escape sequences for color output.
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Hidden: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Print(versionOutput)
|
||||
},
|
||||
cmdFactory := factory.New(Version)
|
||||
RootCmd = root.NewCmdRoot(cmdFactory, Version, BuildDate)
|
||||
RootCmd.AddCommand(aliasCmd)
|
||||
RootCmd.AddCommand(completionCmd)
|
||||
RootCmd.AddCommand(configCmd)
|
||||
}
|
||||
|
||||
// overridden in tests
|
||||
|
|
@ -245,62 +85,6 @@ func contextForCommand(cmd *cobra.Command) context.Context {
|
|||
return ctx
|
||||
}
|
||||
|
||||
// generic authenticated HTTP client for commands
|
||||
func httpClient(io *iostreams.IOStreams, cfg config.Config, setAccept bool) *http.Client {
|
||||
var opts []api.ClientOption
|
||||
if verbose := os.Getenv("DEBUG"); verbose != "" {
|
||||
opts = append(opts, apiVerboseLog())
|
||||
}
|
||||
|
||||
opts = append(opts,
|
||||
api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", Version)),
|
||||
// antiope-preview: Checks
|
||||
// FIXME: avoid setting this header for `api` command
|
||||
api.AddHeader("Accept", "application/vnd.github.antiope-preview+json"),
|
||||
api.AddHeaderFunc("Authorization", func(req *http.Request) (string, error) {
|
||||
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
|
||||
return fmt.Sprintf("token %s", token), nil
|
||||
}
|
||||
|
||||
hostname := ghinstance.NormalizeHostname(req.URL.Hostname())
|
||||
token, err := cfg.Get(hostname, "oauth_token")
|
||||
if token == "" {
|
||||
var notFound *config.NotFoundError
|
||||
// TODO: check if stdout is TTY too
|
||||
if errors.As(err, ¬Found) && io.IsStdinTTY() {
|
||||
// interactive OAuth flow
|
||||
token, err = config.AuthFlowWithConfig(cfg, hostname, "Notice: authentication required")
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token == "" {
|
||||
// TODO: instruct user how to manually authenticate
|
||||
return "", fmt.Errorf("authentication required for %s", hostname)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("token %s", token), nil
|
||||
}),
|
||||
)
|
||||
|
||||
if setAccept {
|
||||
opts = append(opts,
|
||||
api.AddHeaderFunc("Accept", func(req *http.Request) (string, error) {
|
||||
// antiope-preview: Checks
|
||||
accept := "application/vnd.github.antiope-preview+json"
|
||||
if ghinstance.IsEnterprise(req.URL.Hostname()) {
|
||||
// shadow-cat-preview: Draft pull requests
|
||||
accept += ", application/vnd.github.shadow-cat-preview"
|
||||
}
|
||||
return accept, nil
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return api.NewHTTPClient(opts...)
|
||||
}
|
||||
|
||||
func apiVerboseLog() api.ClientOption {
|
||||
logTraffic := strings.Contains(os.Getenv("DEBUG"), "api")
|
||||
colorize := utils.IsTerminal(os.Stderr)
|
||||
|
|
@ -323,17 +107,6 @@ func colorableErr(cmd *cobra.Command) io.Writer {
|
|||
return err
|
||||
}
|
||||
|
||||
func changelogURL(version string) string {
|
||||
path := "https://github.com/cli/cli"
|
||||
r := regexp.MustCompile(`^v?\d+\.\d+\.\d+(-[\w.]+)?$`)
|
||||
if !r.MatchString(version) {
|
||||
return fmt.Sprintf("%s/releases/latest", path)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/releases/tag/v%s", path, strings.TrimPrefix(version, "v"))
|
||||
return url
|
||||
}
|
||||
|
||||
func ExecuteShellAlias(args []string) error {
|
||||
externalCmd := exec.Command(args[0], args[1:]...)
|
||||
externalCmd.Stderr = os.Stderr
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ import (
|
|||
type Context interface {
|
||||
Branch() (string, error)
|
||||
SetBranch(string)
|
||||
Remotes() (Remotes, error)
|
||||
BaseRepo() (ghrepo.Interface, error)
|
||||
SetBaseRepo(string)
|
||||
Config() (config.Config, error)
|
||||
}
|
||||
|
|
@ -160,7 +158,6 @@ func New() Context {
|
|||
// A Context implementation that queries the filesystem
|
||||
type fsContext struct {
|
||||
config config.Config
|
||||
remotes Remotes
|
||||
branch string
|
||||
baseRepo ghrepo.Interface
|
||||
}
|
||||
|
|
@ -196,60 +193,6 @@ func (c *fsContext) SetBranch(b string) {
|
|||
c.branch = b
|
||||
}
|
||||
|
||||
func (c *fsContext) Remotes() (Remotes, error) {
|
||||
if c.remotes == nil {
|
||||
gitRemotes, err := git.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(gitRemotes) == 0 {
|
||||
return nil, errors.New("no git remotes found")
|
||||
}
|
||||
|
||||
sshTranslate := git.ParseSSHConfig().Translator()
|
||||
resolvedRemotes := translateRemotes(gitRemotes, sshTranslate)
|
||||
|
||||
// determine hostname by looking at the "main" remote
|
||||
var hostname string
|
||||
if mainRemote, err := resolvedRemotes.FindByName("upstream", "github", "origin", "*"); err == nil {
|
||||
hostname = mainRemote.RepoHost()
|
||||
}
|
||||
|
||||
// filter the rest of the remotes to just that hostname
|
||||
filteredRemotes := Remotes{}
|
||||
for _, r := range resolvedRemotes {
|
||||
if r.RepoHost() != hostname {
|
||||
continue
|
||||
}
|
||||
filteredRemotes = append(filteredRemotes, r)
|
||||
}
|
||||
c.remotes = filteredRemotes
|
||||
}
|
||||
|
||||
if len(c.remotes) == 0 {
|
||||
return nil, errors.New("no git remote found for a github.com repository")
|
||||
}
|
||||
return c.remotes, nil
|
||||
}
|
||||
|
||||
func (c *fsContext) BaseRepo() (ghrepo.Interface, error) {
|
||||
if c.baseRepo != nil {
|
||||
return c.baseRepo, nil
|
||||
}
|
||||
|
||||
remotes, err := c.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rem, err := remotes.FindByName("upstream", "github", "origin", "*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.baseRepo = rem
|
||||
return c.baseRepo, nil
|
||||
}
|
||||
|
||||
func (c *fsContext) SetBaseRepo(nwo string) {
|
||||
c.baseRepo, _ = ghrepo.FromFullName(nwo)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ func (r Remote) RepoHost() string {
|
|||
}
|
||||
|
||||
// TODO: accept an interface instead of git.RemoteSet
|
||||
func translateRemotes(gitRemotes git.RemoteSet, urlTranslate func(*url.URL) *url.URL) (remotes Remotes) {
|
||||
func TranslateRemotes(gitRemotes git.RemoteSet, urlTranslate func(*url.URL) *url.URL) (remotes Remotes) {
|
||||
for _, r := range gitRemotes {
|
||||
var repo ghrepo.Interface
|
||||
if r.FetchURL != nil {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func Test_translateRemotes(t *testing.T) {
|
|||
identityURL := func(u *url.URL) *url.URL {
|
||||
return u
|
||||
}
|
||||
result := translateRemotes(gitRemotes, identityURL)
|
||||
result := TranslateRemotes(gitRemotes, identityURL)
|
||||
|
||||
if len(result) != 1 {
|
||||
t.Errorf("got %d results", len(result))
|
||||
|
|
|
|||
105
pkg/cmd/factory/default.go
Normal file
105
pkg/cmd/factory/default.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package factory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
)
|
||||
|
||||
func New(appVersion string) *cmdutil.Factory {
|
||||
io := iostreams.System()
|
||||
|
||||
var cachedConfig config.Config
|
||||
var configError error
|
||||
configFunc := func() (config.Config, error) {
|
||||
if cachedConfig != nil || configError != nil {
|
||||
return cachedConfig, configError
|
||||
}
|
||||
cachedConfig, configError = config.ParseDefaultConfig()
|
||||
if errors.Is(configError, os.ErrNotExist) {
|
||||
cachedConfig = config.NewBlankConfig()
|
||||
configError = nil
|
||||
}
|
||||
return cachedConfig, configError
|
||||
}
|
||||
|
||||
remotesFunc := remotesResolver()
|
||||
|
||||
return &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
Config: configFunc,
|
||||
Remotes: remotesFunc,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
cfg, err := configFunc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: avoid setting Accept header for `api` command
|
||||
return httpClient(io, cfg, appVersion, true), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
remotes, err := remotesFunc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return remotes.FindByName("upstream", "github", "origin", "*")
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
currentBranch, err := git.CurrentBranch()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not determine current branch: %w", err)
|
||||
}
|
||||
return currentBranch, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: pass in a Config instance to parse remotes based on pre-authenticated hostnames
|
||||
func remotesResolver() func() (context.Remotes, error) {
|
||||
var cachedRemotes context.Remotes
|
||||
var remotesError error
|
||||
|
||||
return func() (context.Remotes, error) {
|
||||
if cachedRemotes != nil || remotesError != nil {
|
||||
return cachedRemotes, remotesError
|
||||
}
|
||||
|
||||
gitRemotes, err := git.Remotes()
|
||||
if err != nil {
|
||||
remotesError = err
|
||||
return nil, err
|
||||
}
|
||||
if len(gitRemotes) == 0 {
|
||||
remotesError = errors.New("no git remotes found")
|
||||
return nil, remotesError
|
||||
}
|
||||
|
||||
sshTranslate := git.ParseSSHConfig().Translator()
|
||||
resolvedRemotes := context.TranslateRemotes(gitRemotes, sshTranslate)
|
||||
|
||||
// determine hostname by looking at the primary remotes
|
||||
var hostname string
|
||||
if mainRemote, err := resolvedRemotes.FindByName("upstream", "github", "origin", "*"); err == nil {
|
||||
hostname = mainRemote.RepoHost()
|
||||
}
|
||||
|
||||
// filter the rest of the remotes to just that hostname
|
||||
cachedRemotes = context.Remotes{}
|
||||
for _, r := range resolvedRemotes {
|
||||
if r.RepoHost() != hostname {
|
||||
continue
|
||||
}
|
||||
cachedRemotes = append(cachedRemotes, r)
|
||||
}
|
||||
return cachedRemotes, nil
|
||||
}
|
||||
}
|
||||
68
pkg/cmd/factory/http.go
Normal file
68
pkg/cmd/factory/http.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package factory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
)
|
||||
|
||||
// generic authenticated HTTP client for commands
|
||||
func httpClient(io *iostreams.IOStreams, cfg config.Config, appVersion string, setAccept bool) *http.Client {
|
||||
var opts []api.ClientOption
|
||||
if verbose := os.Getenv("DEBUG"); verbose != "" {
|
||||
logTraffic := strings.Contains(verbose, "api")
|
||||
opts = append(opts, api.VerboseLog(io.ErrOut, logTraffic, io.IsStderrTTY()))
|
||||
}
|
||||
|
||||
opts = append(opts,
|
||||
api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", appVersion)),
|
||||
api.AddHeaderFunc("Authorization", func(req *http.Request) (string, error) {
|
||||
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
|
||||
return fmt.Sprintf("token %s", token), nil
|
||||
}
|
||||
|
||||
hostname := ghinstance.NormalizeHostname(req.URL.Hostname())
|
||||
token, err := cfg.Get(hostname, "oauth_token")
|
||||
if token == "" {
|
||||
var notFound *config.NotFoundError
|
||||
// TODO: check if stdout is TTY too
|
||||
if errors.As(err, ¬Found) && io.IsStdinTTY() {
|
||||
// interactive OAuth flow
|
||||
token, err = config.AuthFlowWithConfig(cfg, hostname, "Notice: authentication required")
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token == "" {
|
||||
// TODO: instruct user how to manually authenticate
|
||||
return "", fmt.Errorf("authentication required for %s", hostname)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("token %s", token), nil
|
||||
}),
|
||||
)
|
||||
|
||||
if setAccept {
|
||||
opts = append(opts,
|
||||
api.AddHeaderFunc("Accept", func(req *http.Request) (string, error) {
|
||||
// antiope-preview: Checks
|
||||
accept := "application/vnd.github.antiope-preview+json"
|
||||
if ghinstance.IsEnterprise(req.URL.Hostname()) {
|
||||
// shadow-cat-preview: Draft pull requests
|
||||
accept += ", application/vnd.github.shadow-cat-preview"
|
||||
}
|
||||
return accept, nil
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return api.NewHTTPClient(opts...)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package command
|
||||
package root
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -67,8 +67,12 @@ func nestedSuggestFunc(command *cobra.Command, arg string) {
|
|||
_ = rootUsageFunc(command)
|
||||
}
|
||||
|
||||
func isRootCmd(command *cobra.Command) bool {
|
||||
return command != nil && !command.HasParent()
|
||||
}
|
||||
|
||||
func rootHelpFunc(command *cobra.Command, args []string) {
|
||||
if command.Parent() == RootCmd && len(args) >= 2 && args[1] != "--help" && args[1] != "-h" {
|
||||
if isRootCmd(command.Parent()) && len(args) >= 2 && args[1] != "--help" && args[1] != "-h" {
|
||||
nestedSuggestFunc(command, args[1])
|
||||
hasFailed = true
|
||||
return
|
||||
|
|
@ -141,7 +145,7 @@ Read the manual at https://cli.github.com/manual`})
|
|||
helpEntries = append(helpEntries, helpEntry{"FEEDBACK", command.Annotations["help:feedback"]})
|
||||
}
|
||||
|
||||
out := colorableOut(command)
|
||||
out := command.OutOrStdout()
|
||||
for _, e := range helpEntries {
|
||||
if e.Title != "" {
|
||||
// If there is a title, add indentation to each line in the body
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package command
|
||||
package root
|
||||
|
||||
import (
|
||||
"testing"
|
||||
147
pkg/cmd/root/root.go
Normal file
147
pkg/cmd/root/root.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
apiCmd "github.com/cli/cli/pkg/cmd/api"
|
||||
gistCmd "github.com/cli/cli/pkg/cmd/gist"
|
||||
issueCmd "github.com/cli/cli/pkg/cmd/issue"
|
||||
prCmd "github.com/cli/cli/pkg/cmd/pr"
|
||||
repoCmd "github.com/cli/cli/pkg/cmd/repo"
|
||||
creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "gh <command> <subcommand> [flags]",
|
||||
Short: "GitHub CLI",
|
||||
Long: `Work seamlessly with GitHub from the command line.`,
|
||||
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
Example: heredoc.Doc(`
|
||||
$ gh issue create
|
||||
$ gh repo clone cli/cli
|
||||
$ gh pr checkout 321
|
||||
`),
|
||||
Annotations: map[string]string{
|
||||
"help:feedback": heredoc.Doc(`
|
||||
Fill out our feedback form https://forms.gle/umxd3h31c7aMQFKG7
|
||||
Open an issue using “gh issue create -R cli/cli”
|
||||
`),
|
||||
"help:environment": heredoc.Doc(`
|
||||
GITHUB_TOKEN: an authentication token for API requests. Setting this avoids being
|
||||
prompted to authenticate and overrides any previously stored credentials.
|
||||
|
||||
GH_REPO: specify the GitHub repository in "OWNER/REPO" format for commands that
|
||||
otherwise operate on a local repository.
|
||||
|
||||
GH_EDITOR, GIT_EDITOR, VISUAL, EDITOR (in order of precedence): the editor tool to use
|
||||
for authoring text.
|
||||
|
||||
BROWSER: the web browser to use for opening links.
|
||||
|
||||
DEBUG: set to any value to enable verbose output to standard error. Include values "api"
|
||||
or "oauth" to print detailed information about HTTP requests or authentication flow.
|
||||
|
||||
GLAMOUR_STYLE: the style to use for rendering Markdown. See
|
||||
https://github.com/charmbracelet/glamour#styles
|
||||
|
||||
NO_COLOR: avoid printing ANSI escape sequences for color output.
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
version = strings.TrimPrefix(version, "v")
|
||||
if buildDate == "" {
|
||||
cmd.Version = version
|
||||
} else {
|
||||
cmd.Version = fmt.Sprintf("%s (%s)", version, buildDate)
|
||||
}
|
||||
versionOutput := fmt.Sprintf("gh version %s\n%s\n", cmd.Version, changelogURL(version))
|
||||
cmd.AddCommand(&cobra.Command{
|
||||
Use: "version",
|
||||
Hidden: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Print(versionOutput)
|
||||
},
|
||||
})
|
||||
cmd.SetVersionTemplate(versionOutput)
|
||||
cmd.Flags().Bool("version", false, "Show gh version")
|
||||
|
||||
cmd.SetOut(f.IOStreams.Out)
|
||||
cmd.SetErr(f.IOStreams.ErrOut)
|
||||
|
||||
cmd.PersistentFlags().Bool("help", false, "Show help for command")
|
||||
cmd.SetHelpFunc(rootHelpFunc)
|
||||
cmd.SetUsageFunc(rootUsageFunc)
|
||||
|
||||
cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
||||
if err == pflag.ErrHelp {
|
||||
return err
|
||||
}
|
||||
return &cmdutil.FlagError{Err: err}
|
||||
})
|
||||
|
||||
// CHILD COMMANDS
|
||||
|
||||
cmd.AddCommand(apiCmd.NewCmdApi(f, nil))
|
||||
cmd.AddCommand(gistCmd.NewCmdGist(f))
|
||||
cmd.AddCommand(creditsCmd.NewCmdCredits(f, nil))
|
||||
|
||||
// below here at the commands that require the "intelligent" BaseRepo resolver
|
||||
repoResolvingCmdFactory := *f
|
||||
repoResolvingCmdFactory.BaseRepo = resolvedBaseRepo(f)
|
||||
|
||||
cmd.AddCommand(prCmd.NewCmdPR(&repoResolvingCmdFactory))
|
||||
cmd.AddCommand(issueCmd.NewCmdIssue(&repoResolvingCmdFactory))
|
||||
cmd.AddCommand(repoCmd.NewCmdRepo(&repoResolvingCmdFactory))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func resolvedBaseRepo(f *cmdutil.Factory) func() (ghrepo.Interface, error) {
|
||||
return func() (ghrepo.Interface, error) {
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
remotes, err := f.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseRepo, err := repoContext.BaseRepo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return baseRepo, nil
|
||||
}
|
||||
}
|
||||
|
||||
func changelogURL(version string) string {
|
||||
path := "https://github.com/cli/cli"
|
||||
r := regexp.MustCompile(`^v?\d+\.\d+\.\d+(-[\w.]+)?$`)
|
||||
if !r.MatchString(version) {
|
||||
return fmt.Sprintf("%s/releases/latest", path)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/releases/tag/v%s", path, strings.TrimPrefix(version, "v"))
|
||||
return url
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package command
|
||||
package root
|
||||
|
||||
import (
|
||||
"testing"
|
||||
Loading…
Add table
Add a link
Reference in a new issue