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
|
// Package gh is a library for CLI Go applications to help interface with the gh CLI tool,
// and the GitHub API.
//
// Note that the examples in this package assume gh and git are installed. They do not run in
// the Go Playground used by pkg.go.dev.
package gh
import (
"bytes"
"errors"
"fmt"
"net/http"
"os"
"os/exec"
iapi "github.com/cli/go-gh/internal/api"
"github.com/cli/go-gh/internal/git"
irepo "github.com/cli/go-gh/internal/repository"
"github.com/cli/go-gh/pkg/api"
"github.com/cli/go-gh/pkg/auth"
"github.com/cli/go-gh/pkg/config"
repo "github.com/cli/go-gh/pkg/repository"
"github.com/cli/go-gh/pkg/ssh"
"github.com/cli/safeexec"
)
// Exec gh command with provided arguments.
func Exec(args ...string) (stdOut, stdErr bytes.Buffer, err error) {
path, err := path()
if err != nil {
err = fmt.Errorf("could not find gh executable in PATH. error: %w", err)
return
}
return run(path, nil, args...)
}
func path() (string, error) {
return safeexec.LookPath("gh")
}
func run(path string, env []string, args ...string) (stdOut, stdErr bytes.Buffer, err error) {
cmd := exec.Command(path, args...)
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
if env != nil {
cmd.Env = env
}
err = cmd.Run()
if err != nil {
err = fmt.Errorf("failed to run gh: %s. error: %w", stdErr.String(), err)
return
}
return
}
// RESTClient builds a client to send requests to GitHub REST API endpoints.
// As part of the configuration a hostname, auth token, default set of headers,
// and unix domain socket are resolved from the gh environment configuration.
// These behaviors can be overridden using the opts argument.
func RESTClient(opts *api.ClientOptions) (api.RESTClient, error) {
if opts == nil {
opts = &api.ClientOptions{}
}
if optionsNeedResolution(opts) {
err := resolveOptions(opts)
if err != nil {
return nil, err
}
}
return iapi.NewRESTClient(opts.Host, opts), nil
}
// GQLClient builds a client to send requests to GitHub GraphQL API endpoints.
// As part of the configuration a hostname, auth token, default set of headers,
// and unix domain socket are resolved from the gh environment configuration.
// These behaviors can be overridden using the opts argument.
func GQLClient(opts *api.ClientOptions) (api.GQLClient, error) {
if opts == nil {
opts = &api.ClientOptions{}
}
if optionsNeedResolution(opts) {
err := resolveOptions(opts)
if err != nil {
return nil, err
}
}
return iapi.NewGQLClient(opts.Host, opts), nil
}
// HTTPClient builds a client that can be passed to another library.
// As part of the configuration a hostname, auth token, default set of headers,
// and unix domain socket are resolved from the gh environment configuration.
// These behaviors can be overridden using the opts argument. In this instance
// providing opts.Host will not change the destination of your request as it is
// the responsibility of the consumer to configure this. However, if opts.Host
// does not match the request host, the auth token will not be added to the headers.
// This is to protect against the case where tokens could be sent to an arbitrary
// host.
func HTTPClient(opts *api.ClientOptions) (*http.Client, error) {
if opts == nil {
opts = &api.ClientOptions{}
}
if optionsNeedResolution(opts) {
err := resolveOptions(opts)
if err != nil {
return nil, err
}
}
client := iapi.NewHTTPClient(opts)
return &client, nil
}
// CurrentRepository uses git remotes to determine the GitHub repository
// the current directory is tracking.
func CurrentRepository() (repo.Repository, error) {
override := os.Getenv("GH_REPO")
if override != "" {
return repo.Parse(override)
}
remotes, err := git.Remotes()
if err != nil {
return nil, err
}
if len(remotes) == 0 {
return nil, errors.New("unable to determine current repository, no git remotes configured for this repository")
}
translator := ssh.NewTranslator()
for _, r := range remotes {
if r.FetchURL != nil {
r.FetchURL = translator.Translate(r.FetchURL)
}
if r.PushURL != nil {
r.PushURL = translator.Translate(r.PushURL)
}
}
hosts := auth.KnownHosts()
filteredRemotes := remotes.FilterByHosts(hosts)
if len(filteredRemotes) == 0 {
return nil, errors.New("unable to determine current repository, none of the git remotes configured for this repository point to a known GitHub host")
}
r := filteredRemotes[0]
return irepo.New(r.Host, r.Owner, r.Repo), nil
}
func optionsNeedResolution(opts *api.ClientOptions) bool {
if opts.Host == "" {
return true
}
if opts.AuthToken == "" {
return true
}
if opts.UnixDomainSocket == "" && opts.Transport == nil {
return true
}
return false
}
func resolveOptions(opts *api.ClientOptions) error {
cfg, _ := config.Read()
if opts.Host == "" {
opts.Host, _ = auth.DefaultHost()
}
if opts.AuthToken == "" {
opts.AuthToken, _ = auth.TokenForHost(opts.Host)
if opts.AuthToken == "" {
return fmt.Errorf("authentication token not found for host %s", opts.Host)
}
}
if opts.UnixDomainSocket == "" && cfg != nil {
opts.UnixDomainSocket, _ = cfg.Get([]string{"http_unix_socket"})
}
return nil
}
|