github.com/chelnak/go-gh@v0.0.2/gh.go (about) 1 // Package gh is a library for CLI Go applications to help interface with the gh CLI tool, 2 // and the GitHub API. 3 // 4 // Note that the examples in this package assume gh and git are installed. They do not run in 5 // the Go Playground used by pkg.go.dev. 6 package gh 7 8 import ( 9 "bytes" 10 "errors" 11 "fmt" 12 "net/url" 13 "os/exec" 14 15 iapi "github.com/chelnak/go-gh/internal/api" 16 "github.com/chelnak/go-gh/internal/config" 17 "github.com/chelnak/go-gh/internal/git" 18 "github.com/chelnak/go-gh/internal/ssh" 19 "github.com/chelnak/go-gh/pkg/api" 20 "github.com/cli/safeexec" 21 ) 22 23 // Exec gh command with provided arguments. 24 func Exec(args ...string) (stdOut, stdErr bytes.Buffer, err error) { 25 path, err := path() 26 if err != nil { 27 err = fmt.Errorf("could not find gh executable in PATH. error: %w", err) 28 return 29 } 30 return run(path, nil, args...) 31 } 32 33 func path() (string, error) { 34 return safeexec.LookPath("gh") 35 } 36 37 func run(path string, env []string, args ...string) (stdOut, stdErr bytes.Buffer, err error) { 38 cmd := exec.Command(path, args...) 39 cmd.Stdout = &stdOut 40 cmd.Stderr = &stdErr 41 if env != nil { 42 cmd.Env = env 43 } 44 err = cmd.Run() 45 if err != nil { 46 err = fmt.Errorf("failed to run gh: %s. error: %w", stdErr.String(), err) 47 return 48 } 49 return 50 } 51 52 // RESTClient builds a client to send requests to GitHub REST API endpoints. 53 // As part of the configuration a hostname, auth token, and default set of headers are resolved 54 // from the gh environment configuration. These behaviors can be overridden using the opts argument. 55 func RESTClient(opts *api.ClientOptions) (api.RESTClient, error) { 56 var cfg config.Config 57 var token string 58 var err error 59 if opts == nil { 60 opts = &api.ClientOptions{} 61 } 62 if opts.Host == "" || opts.AuthToken == "" { 63 cfg, err = config.Load() 64 if err != nil { 65 return nil, err 66 } 67 } 68 if opts.Host == "" { 69 opts.Host = cfg.Host() 70 } 71 if opts.AuthToken == "" { 72 token, err = cfg.AuthToken(opts.Host) 73 if err != nil { 74 return nil, err 75 } 76 opts.AuthToken = token 77 } 78 return iapi.NewRESTClient(opts.Host, opts), nil 79 } 80 81 // GQLClient builds a client to send requests to GitHub GraphQL API endpoints. 82 // As part of the configuration a hostname, auth token, and default set of headers are resolved 83 // from the gh environment configuration. These behaviors can be overridden using the opts argument. 84 func GQLClient(opts *api.ClientOptions) (api.GQLClient, error) { 85 var cfg config.Config 86 var token string 87 var err error 88 if opts == nil { 89 opts = &api.ClientOptions{} 90 } 91 if opts.Host == "" || opts.AuthToken == "" { 92 cfg, err = config.Load() 93 if err != nil { 94 return nil, err 95 } 96 } 97 if opts.Host == "" { 98 opts.Host = cfg.Host() 99 } 100 if opts.AuthToken == "" { 101 token, err = cfg.AuthToken(opts.Host) 102 if err != nil { 103 return nil, err 104 } 105 opts.AuthToken = token 106 } 107 return iapi.NewGQLClient(opts.Host, opts), nil 108 } 109 110 // CurrentRepository uses git remotes to determine the GitHub repository 111 // the current directory is tracking. 112 func CurrentRepository() (Repository, error) { 113 remotes, err := git.Remotes() 114 if err != nil { 115 return nil, err 116 } 117 if len(remotes) == 0 { 118 return nil, errors.New("unable to determine current repository, no git remotes configured for this repository") 119 } 120 121 sshConfig := ssh.ParseConfig() 122 translateRemotes(remotes, sshConfig.Translator()) 123 124 cfg, err := config.Load() 125 if err != nil { 126 return nil, err 127 } 128 129 hosts := cfg.Hosts() 130 131 filteredRemotes := remotes.FilterByHosts(hosts) 132 if len(filteredRemotes) == 0 { 133 return nil, errors.New("unable to determine current repository, none of the git remotes configured for this repository point to a known GitHub host") 134 } 135 136 r := filteredRemotes[0] 137 return repo{host: r.Host, name: r.Repo, owner: r.Owner}, nil 138 } 139 140 func translateRemotes(remotes git.RemoteSet, urlTranslate func(*url.URL) *url.URL) { 141 for _, r := range remotes { 142 if r.FetchURL != nil { 143 r.FetchURL = urlTranslate(r.FetchURL) 144 } 145 if r.PushURL != nil { 146 r.PushURL = urlTranslate(r.PushURL) 147 } 148 } 149 } 150 151 // Repository is the interface that wraps repository information methods. 152 type Repository interface { 153 Host() string 154 Name() string 155 Owner() string 156 } 157 158 type repo struct { 159 host string 160 name string 161 owner string 162 } 163 164 func (r repo) Host() string { 165 return r.host 166 } 167 168 func (r repo) Name() string { 169 return r.name 170 } 171 172 func (r repo) Owner() string { 173 return r.owner 174 }