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  }