Git Product home page Git Product logo

go-gh's Introduction

Go library for the GitHub CLI

go-gh is a collection of Go modules to make authoring GitHub CLI extensions easier.

Modules from this library will obey GitHub CLI conventions by default:

  • repository.Current() respects the value of the GH_REPO environment variable and reads from git remote configuration as fallback.

  • GitHub API requests will be authenticated using the same mechanism as gh, i.e. using the values of GH_TOKEN and GH_HOST environment variables and falling back to the user's stored OAuth token.

  • Terminal capabilities are determined by taking environment variables GH_FORCE_TTY, NO_COLOR, CLICOLOR, etc. into account.

  • Generating table or Go template output uses the same engine as gh.

  • The browser module activates the user's preferred web browser.

Usage

See the full go-gh reference documentation for more information

package main

import (
	"fmt"
	"log"
	"github.com/cli/go-gh/v2"
	"github.com/cli/go-gh/v2/pkg/api"
)

func main() {
	// These examples assume `gh` is installed and has been authenticated.

	// Shell out to a gh command and read its output.
	issueList, _, err := gh.Exec("issue", "list", "--repo", "cli/cli", "--limit", "5")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(issueList.String())

	// Use an API client to retrieve repository tags.
	client, err := api.DefaultRESTClient()
	if err != nil {
		log.Fatal(err)
	}
	response := []struct{
		Name string
	}{}
	err = client.Get("repos/cli/cli/tags", &response)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(response)
}

See examples for more demonstrations of usage.

Contributing

If anything feels off, or if you feel that some functionality is missing, please check out our contributing docs. There you will find instructions for sharing your feedback and for submitting pull requests to the project. Thank you!

go-gh's People

Contributors

andyfeller avatar bendrucker avatar chelnak avatar dependabot[bot] avatar devonhk avatar ffalor avatar heaths avatar itchyny avatar koozz avatar maaslalani avatar mislav avatar mjpieters avatar mntlty avatar mszostok avatar samcoe avatar shawnps avatar sridharavinash avatar stemar94 avatar williammartin avatar y-yagi avatar yin1999 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-gh's Issues

CurrentRepository() fails when executed inside a GH Workflow

Hey - just discovered something that might be interesting.

When a consuming application calls CurrentRepository() from inside a GH workflow I'm seeing the following error:

unable to determine current repository, none of the git remotes configured for this repository point to a known GitHub host

I think this is because ~/.config/gh/hosts.yml does not exist on a runner because of the TOKEN based auth (vs oauth).

This would cause FilterByHosts to return an error.

I have worked around this by. exporting the GH_HOST environment variable.. however it was not immediately obvious what to do.

If this is expected behaviour we could update the README with a note on compatibility with GH Actions.

Cheers!

docs: Example of reading an octet stream

Hello!

Loving the abstraction so far, great job!

Was wondering if we could add an example that reads an octet-stream similar to this in the cli repo using net/http?

I'm trying a couple of different approaches, but I'm not sure what's the best way to define the response to facilitate reading bytes?

Let me know if I can help in any way as well 😄

Edit: I'm using the rest client for reference, and looking at

b, err := io.ReadAll(resp.Body)
I'm not sure this is possible currently?

Using Get or Do I get:

invalid character '\x1f' looking for beginning of value

Add a GraphQL Mutation with an `input` object to examples

I noticed that there are no examples of Mutations in https://github.com/cli/go-gh/blob/trunk/example_gh_test.go

I think this is important for a few reasons:

  • there's no mention of Mutate in the examples or in the readme. including it in the examples may make it more discoverable
  • unlike queries, many of the GitHub GraphQL mutations take an input object, for example the addStar mutation and its input. because this is a nested object, it has a different format than queries such as
    func ExampleGQLClient_advanced() {
    Figuring out the right Go syntax took me some time even when I had a properly formed GraphQL mutation to compare it to

If you agree, I'd be happy to open a PR to update, and would like to know:

  1. is addStar a good reference, or is there another that looks better?
  2. for variables, would it be better to use https://github.com/shurcooL/githubv4 or to use only standard types?
  3. should the Readme also mention mutations, or is it sufficient to have them in the examples?

Allow RESTClient and GQLClient to support automatic pagination

Pagination is currently not possible using RESTClient and takes lots of manual work using GQLClient. We should support this feature for both. Initial implementation idea is to add a pagination options to ClientOptions which would enable this for both clients and be a noop for the HTTPClient.

Consider exposing a raw http.Client

Hey - this is a great project and helped me get up and running quickly!

I was looking at integrating https://github.com/google/go-github in to my project. It allows you to pass a pre-configured http client when creating a new GitHubClient instance. The abstraction (RESTClient) doesn't implement the interface so cannot be used as a parameter.

Would you consider exposing a configured raw http.Client instance or the config package to allow us to build our own?

I'd be happy to help out with the implementation if it's something of use.

Add support for GH_REPO environment variable

The CurrentRepository function should take into account the GH_REPO environment variable, and if it is specified return that repository rather than the current flow of looking at the filesystem and git remotes.

As part of this work we should make the parsing function used for parsing GH_REPO into a repository available for user consumption.

cc #5

Status code extraction on response/error

I had a quick question/feedback - I was wondering if a status code could be included along with the response from the gh.RESTClient().Get?

for example I was making a call

client, err := gh.RESTClient(options)
<snip>
err = client.Get("enterprises/avocado-corp/audit-log", &response)
Unfortunately err  cannot be type-casted to api.httperror since api.httperror is not exported, so Id have to rely on string parsing to figure out the error code here. Alternatively api.httpError could be exported to api.HTTPError , in which case the caller can then typecast the error and get all the relevant bits in the struct and do things like retry on certain statusCodes etc.
e.g
err = client.Get(blah)
if err != nil {
    httpError := err.(*api.HttpError)
    if httpError.statusCode == 502 {
            ...retry
     }else{
            ...do something else
      }
}

or the other option would be to read the statusCode, if it’s set on the response.

Does that make sense?

I started a PR to export api.HttpError here - 6596865

Thanks for reading ❤️ .

RESTClient not accepting query string

I have a use case where I need to search for repositories containing certain query criteria (say name or language): essentially reproducing the standard repository search. Using the gh CLI this can be achieved via:

gh api -X GET /search/repositories -f q='<name> in:name language: <lang>'

I am trying to reproduce the above with the RESTClient call, passing the query search somehow as header string; however, according to the docs this grammar is not accepted (namely there is no way to pass a query string to the RESTClient object). Practically speaking I am looking to do something along the lines of

opts := api.ClientOptions{
	...
	Headers:  <insert grammar for query string>  <---- this bit must be filled correctly
	Log:       os.Stdout,
}
...
...
client, err := RESTClient(&opts) <--- the query string is passed to the client
...
err = client.Get("search/repositories", &response) <--- notice the endpoint /search/repositories

Question

What is the correct way to achieve the above?

hyperlink truncated at end of row isn't closed correctly

When using a template with a hyperlink at the end, if the the line is truncated the link its closed correctly.

gh api notifications --template '{{tablerow "Reason" "When" "Repo" "Title" -}}
{{ range . -}}
{{tablerow (.reason | autocolor "cyan") (timeago .updated_at) (.repository.full_name) (hyperlink "https://github.com/notifications" (.subject.title | autocolor "yellow")) -}}
{{end -}}'

Below output is from redirecting to a file, and using sed -n l0 file

With a the available space in terminal window the link is closed correctly:

\033]8;;https://github.com/notifications\033\\build(deps): bump pre-commit helm-docs to v1.13.0\033]8;;\033\\$

When the terminal is too small, the link isn't closed

\033]8;;https://github.com/notifications\033\\build(deps):...$

Therefore turning everything after that in terminal too a link, running the following clears it printf '\e]8;;'

Issue while using from remote connection

As I am utilizing this module for using browser on my cli, I tried to generate error using the cli using ssh connection, but no any error is thrown and the browser also cannot start sitting on CLI.

Actual:

Press Enter to continue.. 
[22660:22660:0710/170853.558426:ERROR:ozone_platform_x11.cc(239)] Missing X server or $DISPLAY
[22660:22660:0710/170853.558460:ERROR:env.cc(255)] The platform failed to initialize.  Exiting.
Err: <nil>

Expected: error to be not nil in value.

If anyone wants to watch out the code, then I will dump it here.

`isEnterprise` returns `true` incorrectly on `github.localhost`

Hello! I have an admittedly niche problem - I'm working on a CLI extension against github.localhost, which means setting GH_HOST=github.localhost for each command. Calling isEnterprise returns true in that case, because it is not github.com:

func isEnterprise(host string) bool {
return host != defaultHost
}

That results in an incorrect (and un-fixable) URL here:

func restPrefix(hostname string) string {
if isEnterprise(hostname) {
return fmt.Sprintf("https://%s/api/v3/", hostname)
}
return "https://api.github.com/"
}

As far as I can tell, there's no way for me to say "use GH_HOST, but also use api.<GH_HOST> instead of <GH_HOST>/api/v3". Is that correct, or is there a patch needed here? Happy to open a PR adding an exception for github.localhost if that makes sense, or some more consistent way of setting the full URL?


FWIW, I did think that maybe have GH_HOST=http://api.github.localhost would work, but that gives:

Post "https://http//api.github.localhost/api/v3/repos/:owner/:repo/<REDACTED>"

Flag for printing only GraphQL information

This project has made my work significantly easier, thank you!

I am finding that often I am wanting to print only the GraphQL query & GraphQL variables but not the HTTP information. I know you can set os.Setenv("GH_DEBUG", "api") to print the GraphQL & HTTP information. I am finding that when I am developing my GraphQL struct that it would be useful to reduce some of the noise and only print out the GraphQL information.

Add the ability to introduce repository specific configurations for gh-cli and its extensions

Feature request description

I would like to propose the addition of a feature that enables the integration of repository-specific configurations into the pkg/config package, drawing inspiration from the way Git handles local .git/config versus XDG_CONFIG_HOME configurations. This enhancement could prove invaluable for various extensions creators who could leverage distinct settings tailored to each repository's requirements.

Motivation

Consider a chat-op deployment scenario - a good gh extension that integrates well with external platforms like Discord or Slack, having the ability to define and commit repository-specific configurations directly within the repository itself can offer significant advantages. For instance, imagine a use case where different repositories need to route communications to specific channels on these platforms. The current config package is limited in that the configs are global (AFAIK - please let me know if this is incorrect) which leaves the current approach of providing options each time a command is invoked which can become cumbersome, especially when the values for channels differ across repositories. This is just one scenario where repo specific configs might be useful but I'm sure extension creators can think of creative ways to use this feature 😄

Implementation

The proposed implementation could involve creating a dedicated configuration file within the repository, such as .ghconfig, where users can define repository-specific settings. This file would be committed alongside other source code and assets.

Conclusion

Introducing repository-specific configurations to the "gh" library has the potential to enhance the usability, consistency, and flexibility of the tool in scenarios where distinct settings are essential on a per-repository basis. This feature aligns with the natural progression of version control practices and would undoubtedly contribute to streamlining workflows and improving overall efficiency.

Issue with `gh.CurrentRepository`: `unable to determine current repository, none of the git remotes configured for this repository point to a known GitHub host`

Hello! 👋

I have recently created a GitHub CLI extension, gh-collab-scanner, that relies on gh.CurrentRepository to detect the current GitHub repository.

I don't understand why it does not work on my Ubuntu laptop: unable to determine current repository, none of the git remotes configured for this repository point to a known GitHub host error occurs, see nicokosi/gh-collab-scanner#13 (but it works on my macOS laptop).

Thanks in advance for your help.

`asciisanitizer.Sanitizer` mishandled the `�` unicode character

We have encountered an error when using the GitHub cli to fetch commits in an MDN repository. And I found this error is coursed by the sanitizer which is used by GitHub cli.

So I created a demo to reproduce the problem:

the plain text to transform:

�, plain text

When we read the plain text, and use transform with the the sanitizer , we would got an error:

image

But this should be the correct text. I found the error is returned here.

So I read the signature of utf8.DecodeRune. It may also return utf8.RuneError if the bytes are correctly decoded. And if there does be a decode error, it will return (RuneError, 0) or (RuneError, 1).

So we can't judge whether there is a decoding error just based on the first value returned, like the text I used above, which uses this unicode character. The sanitizer mishandled it.

@eXamadeus GitHub CLI currently has no mechanism for switching between multiple GitHub accounts and I don't really have a workaround to suggest for you at this moment, sorry.

    @eXamadeus GitHub CLI currently has no mechanism for switching between multiple GitHub accounts and I don't really have a workaround to suggest for you at this moment, sorry.

The only approach I could imagine, but would not recommend to anyone, would be to authenticate with 1st account, save a copy of ~/.config/gh/hosts.yml somewhere & delete the original file, authenticate again with the 2nd account, and now you can swap the ~/.config/gh/hosts.yml file with the backup file when you need to switch accounts.

Since this solution involves using SSH for git protocol, make sure both configuration files include the git_protocol: ssh line.

Originally posted by @mislav in cli/cli#326 (comment)

Incorrect tag format for latest release

Hey - thank you for pushing out the latest release. Please could you correct the tag as it has a . between the v and semver.

https://github.com/cli/go-gh/releases/tag/v.0.0.2

Change example_gh_test.go to package `gh_test`

Being in the same package, the example test file can refer to functions such as RESTClient and Exec as local, as opposed to gh.RESTClient and gh.Exec. This presents some challenges:

  • the examples cannot be copied as is
  • it can be confusing to Go newcomers, who may not be familiar with how to solve the issue

I would like to suggest changing the package of the example file and updating the functions in it.

Template support for additional functions

It'd be great if we could add more functions to the template package, perhaps using the "WithOptions" pattern as mentioned elsewhere. The existing templates are useful, but for extensions to add other useful helpers for their own needs, passing in a map to merge after initializing the default list (so extensions could override) could prove useful.

GQLCLient should return GQLError for query and mutation methods

Right now the query and mutations methods on GQLCLient return an error type from shurcooL-graphql which does not match the concrete error type from the do methods. This causes unnecessary complexity when trying to do type assertion on the errors returned from the GQLCLient methods. To fix this we can wrap the errors returned from query and mutations methods in a GQLError.

Check auth status

If the CLI isn't already authenticated, extensions can fail in various ways depending on what they call. For example, using gh.CurrentRepository() - even in a repo - fails with errors.New("unable to determine current repository, none of the git remotes configured for this repository point to a known GitHub host"). If you specify a repo explicitly, gh.GQLClient() can fail with an internal config.NotFoundError which, given it's in internal, cannot be referenced so might as well be a generic error. To the user, the error simply prints "not found" which isn't actionable. Even the more wordy error mentioned first isn't that actionable, though more useful.

Instead, what about something like gh.IsAuthenticated() bool? It could, for example, call config.Load() and then check the Config.AuthToken for a known host. This would allow extensions to easily check up front if they should prompt the user to authenticate.

Alternatively, expose any auth-related errors in pkg or something so we can do type assertions.

initial feedback

suggestions for the example:

  • Include use of graphql
  • Include the gh external call fallback

Regarding the package API:

  • Is it worth including a shortcut way to get a REST/GQL client that auto-resolves host/token/remote based on the kind of logic we do in gh itself? As opposed to always requiring the three step process (pick remote, determine host, get+set token) to create clients?

Misc:

  • I agree that inline docs with examples for godoc to pick up is A+ like we talked about in sync in addition to a full example
  • something I've seen is running a full example as part of some build automation to make sure that it stays up to date. I'm not sure if that's too much effort here but it might be worth considering.

releasing/installing extensions

First, this is awesome. I started out assuming I was going to need to hack my commands into the CLI proper and then found the extension mechanism. Perfect.

I have a question about how to release the extension so others can install it. I wrote up a quick Go-based extension, put it in a gh-* repo and installed it locally (gh extension install .). Worked like a charm. So good I want to share. I saw the template put in a release workflow so I went ahead and tagged and pushed and the workflow ran and I duly got a mess of binaries in a release. Great.

I flipped back to my client and ran gh extension install org/gh-repo and it complains that extension is uninstallable: missing executable. I'm not sure how this is supposed to work. The doc said a few things about having to have "the executable" in the root of the repo but for compiled code

  • What's the executable for native code where many platforms are supported?
  • If we are meant to put all of the various executables at the root of the repo,
    • what's the naming convention for the executables at the root for all the different platforms?
    • it would be great to have some helpers to generate all those. Even better if we can use Actions and the release workflow.
  • Can you pick a Git ref (sha, tag, branch) of the repo that people will get by default? Users randomly getting the latest WIP form the default branch or forcing the dev team's normal development out of the default branch feels troublesome.
  • These files can start to get large. (my trivial one is ~8MB but then times 7 platforms)... and I'll inevitably keep accidentally pushing new versions all the time with my WIP code changes
  • What's the role of a release and the workflow in all of this?

Seems ideal here to use the Release mechanism and say that installing gets the executables from latest release by default and you can say something like install org/[email protected] if you want a specific release.

Support for paginating API responses?

I'm using this for getting changed files between two commits, I presume this will split across pages, is there a way using the HTTPClient to get at the Link headers?

Add context to possibly long-running methods (external API)

Describe the feature or problem you’d like to solve

Currently, there is no option to cancel the ongoing request.

Here is a small example of pagination request where this would be helpful https://gist.github.com/mszostok/b68ff95f85d4b4ff8a27aeed56f9d3ca.

This only fetches GitHub stars, so the payload is not heavy, however if you want to fetch all issues then you have even a bigger problem.

Proposed solution

How will it benefit CLI and its users?

  • It improves UX as you can cancel executed command, and it will be released immediately instead of being unresponsive for a few seconds.

  • It reduces the quota consumption, as you won't execute next calls if not needed and also save the bandwidth as you will inform server that the client is not waiting for the response anymore.

Additional context

There are at least two options how it can be achieved:

  1. Add new methods with the WithContext suffix. For example:

    // GQLClient is the interface that wraps methods for the different types of
    // API requests that are supported by the server.
    type GQLClient interface {
      // Do executes a GraphQL query request.
      // The response is populated into the response argument.
      Do(query string, variables map[string]interface{}, response interface{}) error
    
      // DoWithContext executes a GraphQL query request.
      // The response is populated into the response argument.
      DoWithContext(ctx context.Context, query string, variables map[string]interface{}, response interface{}) error
    }

    Similar approach as Go has for http.Request. It's not a breaking change.

  2. Change all func signature and add the context.Context as a first parameter. It's a breaking change in external API.

There is also an option to add context.Context to struct but it won't work in this case and it's ugly and problematic.

If you agree with one of the provided option, I can create a dedicated PR 👍

Expose `ghLookupPath` or a better way to run `gh`

Been using go-gh for a few things and liking it so far!

I have a use case where I want to run gh as a long running command and I want to take over the writer for stdout and stderr. With the current APIs, I can't do this.

See example of how I am doing it right now by directly invoking gh: https://github.com/josebalius/gh-csfs/blob/main/internal/csfs/ssh.go#L37-L56

This solution works perfectly, but I am hardcoding gh, so it would be nice to either expose the lookup path function or a way to inject writers.

TokenForHost returns string "oauth_token" as source when config is read from file

TokenForHost source return value should be a value like GH_TOKEN or GITHUB_TOKEN when the token comes from an environment variable, or a file path (e.g. /path/to/hosts.yml) when the value comes from a file. However, a static string oauth_token is returned instead of a file name:

return token, oauthToken

Since oauth_token source is not useful to find out where a value comes from, printing the file name would be better.

Ref. #44

RestClient no longer returns an HTTPError

Before the bump to v2, I could do this:

import {
  ...
  "github.com/cli/go-gh/v2/pkg/api"
}
...

err = restClient.Get(path, &resp)
if err != nil {
  if err.(api.HTTPError).StatusCode == 404 {
    log.Fatal("special message")
  }
  log.Fatal("general message")
}

Since updating, that is no longer a valid casting. I tried doing errors.As, but that didn't seem to work either.

Based on some other repositories I found, I tried this:

import {
  ...
  ghApi "github.com/cli/go-gh/v2/pkg/api"
  "github.com/cli/cli/v2/api"
}
...

restClient, err := ghApi.DefaultRESTClient()
if err != nil {
  log.Fatal(err)
}
...

err = restClient.Get(path, &resp)
if err != nil {
  var httpErr api.HTTPError
  if errors.As(err, &httpErr) && httpErr.StatusCode == 404 {
    log.Fatal("special message")
  }
  log.Fatal("general message")
}

This is always going to the general Fatal response message and not the special message. It seems that the errors.As casting is not working here, and the error is a simple string error. I'd love to be able to parse this status code without examining the error string (which does return "HTTP 404: Not Found...").

Is there some new, undocumented method to consume these errors, or was this an accidental regression? Thanks.

API Authentication Fails When Using OAuth or App Tokens

I've noticed what seems like some unexpected behavior when using OAuth (start with gho_) or App (start with ghs_) tokens. I wrote an extension that utilizes the ClientOptions struct and when I provide ClientOptions.AuthToken for either OAuth or App generated tokens, I get a 401.

Upon analyzing what's taking place, it appears that the authorization header that go-gh is using has token instead of bearer. This works for PATs, but not for the aforementioned token types.

To get around this I had to set ClientOptions.AuthToken and then also override ClientOptions.Headers providing the Authorization:bearer <token> key-value manually.

If I try to just add the header key-value override, the lib falls back to using built-in authentication (gh auth login) instead of the provided token.

At least, this is the functionality I'm seeing. Help?

[Discussion] Desired features

This looks like a great start and has good features for basic binary extensions, but when I opened cli/cli#4264 I was hoping to see more low-level implementations either ported or refactored out into a shared library both the CLI and binary extension developers could use. For example,

  • Support for -R command line switch, or at least the APIs to make adding one work. I do see that environment variables that gh uses are parsed, but it would be great to provide the same command line switches (even if you don't force a dependency on Cobra Command).
  • Expose some of the formatting like the templating APIs already there and ones I've helped improve. Binary extensions could just shell out to gh but this seems otherwise unnecessary if more of the formatting code was expose from cli/cli/pkg.

resolveOptions not updating ClientOptions

I noticed this when running some tests with the new version.

resolveOptions is not updating the pointer reference. So any changes made locally in the method are not reflected in the caller.

For example: gh.HTTPClient(nil) causes a nil value for ClientOptions to be passed to the underlying NewHTTPClient.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.