gh/0000755000176200001440000000000014042757216010660 5ustar liggesusersgh/NAMESPACE0000644000176200001440000000147314042733430012074 0ustar liggesusers# Generated by roxygen2: do not edit by hand S3method(format,gh_pat) S3method(print,gh_pat) S3method(print,gh_response) S3method(str,gh_pat) export(gh) export(gh_first) export(gh_gql) export(gh_last) export(gh_next) export(gh_prev) export(gh_rate_limit) export(gh_token) export(gh_tree_remote) export(gh_whoami) importFrom(cli,cli_status) importFrom(cli,cli_status_update) importFrom(httr,DELETE) importFrom(httr,GET) importFrom(httr,PATCH) importFrom(httr,POST) importFrom(httr,PUT) importFrom(httr,add_headers) importFrom(httr,content) importFrom(httr,headers) importFrom(httr,http_type) importFrom(httr,status_code) importFrom(httr,write_disk) importFrom(httr,write_memory) importFrom(jsonlite,fromJSON) importFrom(jsonlite,prettify) importFrom(jsonlite,toJSON) importFrom(utils,URLencode) importFrom(utils,capture.output) gh/LICENSE0000644000176200001440000000012113713261205011647 0ustar liggesusersYEAR: 2015-2020 COPYRIGHT HOLDER: Gábor Csárdi, Jennifer Bryan, Hadley Wickham gh/README.md0000644000176200001440000001001514042733417012131 0ustar liggesusers # gh [![R-CMD-check](https://github.com/r-lib/gh/workflows/R-CMD-check/badge.svg)](https://github.com/r-lib/gh/actions) [![Codecov test coverage](https://codecov.io/gh/r-lib/gh/branch/master/graph/badge.svg)](https://codecov.io/gh/r-lib/gh?branch=master) [![](https://www.r-pkg.org/badges/version/gh)](https://www.r-pkg.org/pkg/gh) [![CRAN RStudio mirror downloads](https://cranlogs.r-pkg.org/badges/gh)](https://www.r-pkg.org/pkg/gh) Minimalistic client to access GitHub’s [REST](https://docs.github.com/rest) and [GraphQL](https://docs.github.com/graphql) APIs. ## Installation Install the package from CRAN as usual: ``` r install.packages("gh") ``` ## Usage ``` r library(gh) ``` Use the `gh()` function to access all API endpoints. The endpoints are listed in the [documentation](https://docs.github.com/rest). The first argument of `gh()` is the endpoint. You can just copy and paste the API endpoints from the documentation. Note that the leading slash must be included as well. From you can copy and paste `GET /users/{username}/repos` into your `gh()` call. E.g. ``` r my_repos <- gh("GET /users/{username}/repos", username = "gaborcsardi") vapply(my_repos, "[[", "", "name") #> [1] "alexr" "altlist" "argufy" "disposables" "dotenv" #> [6] "falsy" "franc" "ISA" "keypress" "lpSolve" #> [11] "macBriain" "maxygen" "MISO" "msgtools" "notifier" #> [16] "oskeyring" "parr" "parsedate" "prompt" "r-font" #> [21] "r-source" "rcorpora" "roxygenlabs" "sankey" "secret" #> [26] "spark" "standalones" "svg-term" "tamper" "testthatlabs" ``` The JSON result sent by the API is converted to an R object. Parameters can be passed as extra arguments. E.g. ``` r my_repos <- gh( "/users/{username}/repos", username = "gaborcsardi", sort = "created") vapply(my_repos, "[[", "", "name") #> [1] "oskeyring" "testthatlabs" "lpSolve" "roxygenlabs" "standalones" #> [6] "altlist" "svg-term" "franc" "sankey" "r-source" #> [11] "secret" "msgtools" "notifier" "prompt" "parr" #> [16] "tamper" "alexr" "argufy" "maxygen" "keypress" #> [21] "macBriain" "MISO" "rcorpora" "disposables" "spark" #> [26] "dotenv" "parsedate" "r-font" "falsy" "ISA" ``` ### POST, PATCH, PUT and DELETE requests POST, PATCH, PUT, and DELETE requests can be sent by including the HTTP verb before the endpoint, in the first argument. E.g. to create a repository: ``` r new_repo <- gh("POST /user/repos", name = "my-new-repo-for-gh-testing") ``` and then delete it: ``` r gh("DELETE /repos/{owner}/{repo}", owner = "gaborcsardi", repo = "my-new-repo-for-gh-testing") ``` ### Tokens By default the `GITHUB_PAT` environment variable is used. Alternatively, one can set the `.token` argument of `gh()`. ### Pagination Supply the `page` parameter to get subsequent pages: ``` r my_repos2 <- gh("GET /orgs/{org}/repos", org = "r-lib", page = 2) vapply(my_repos2, "[[", "", "name") #> [1] "rcmdcheck" "vdiffr" "callr" "mockery" "here" #> [6] "revdepcheck" "processx" "vctrs" "debugme" "usethis" #> [11] "rlang" "pkgload" "httrmock" "pkgbuild" "prettycode" #> [16] "roxygen2md" "pkgapi" "zeallot" "liteq" "keyring" #> [21] "sloop" "styler" "ansistrings" "later" "crancache" #> [26] "zip" "osname" "sessioninfo" "available" "cli" ``` ## Environment Variables - The `GITHUB_API_URL` environment variable is used for the default github api url. - One of `GITHUB_PAT` or `GITHUB_TOKEN` environment variables is used, in this order, as default token. ## License MIT © Gábor Csárdi, Jennifer Bryan, Hadley Wickham gh/man/0000755000176200001440000000000014031420134011413 5ustar liggesusersgh/man/gh_gql.Rd0000644000176200001440000000254113760151264013162 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gh_gql.R \name{gh_gql} \alias{gh_gql} \title{A simple interface for the GitHub GraphQL API v4.} \usage{ gh_gql(query, ...) } \arguments{ \item{query}{The GraphQL query, as a string.} \item{...}{Name-value pairs giving API parameters. Will be matched into \code{endpoint} placeholders, sent as query parameters in GET requests, and as a JSON body of POST requests. If there is only one unnamed parameter, and it is a raw vector, then it will not be JSON encoded, but sent as raw data, as is. This can be used for example to add assets to releases. Named \code{NULL} values are silently dropped. For GET requests, named \code{NA} values trigger an error. For other methods, named \code{NA} values are included in the body of the request, as JSON \code{null}.} } \description{ See more about the GraphQL API here: \url{https://docs.github.com/graphql} } \details{ Note: pagination and the \code{.limit} argument does not work currently, as pagination in the GraphQL API is different from the v3 API. If you need pagination with GraphQL, you'll need to do that manually. } \examples{ \dontshow{if (FALSE) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} gh_gql("query { viewer { login }}") \dontshow{\}) # examplesIf} } \seealso{ \code{\link[=gh]{gh()}} for the GitHub v3 API. } gh/man/gh-package.Rd0000644000176200001440000000126713752204427013715 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gh-package.R \docType{package} \name{gh-package} \alias{gh-package} \alias{_PACKAGE} \title{gh: 'GitHub' 'API'} \description{ Minimal client to access the 'GitHub' 'API'. } \seealso{ Useful links: \itemize{ \item \url{https://gh.r-lib.org/} \item \url{https://github.com/r-lib/gh#readme} \item Report bugs at \url{https://github.com/r-lib/gh/issues} } } \author{ \strong{Maintainer}: Gábor Csárdi \email{csardi.gabor@gmail.com} [contributor] Authors: \itemize{ \item Jennifer Bryan \item Hadley Wickham } Other contributors: \itemize{ \item RStudio [copyright holder, funder] } } \keyword{internal} gh/man/gh_next.Rd0000644000176200001440000000224613611545611013355 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/pagination.R \name{gh_next} \alias{gh_next} \alias{gh_prev} \alias{gh_first} \alias{gh_last} \title{Get the next, previous, first or last page of results} \usage{ gh_next(gh_response) gh_prev(gh_response) gh_first(gh_response) gh_last(gh_response) } \arguments{ \item{gh_response}{An object returned by a \code{\link[=gh]{gh()}} call.} } \value{ Answer from the API. } \description{ Get the next, previous, first or last page of results } \details{ Note that these are not always defined. E.g. if the first page was queried (the default), then there are no first and previous pages defined. If there is no next page, then there is no next page defined, etc. If the requested page does not exist, an error is thrown. } \examples{ \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} x <- gh("/users") vapply(x, "[[", character(1), "login") x2 <- gh_next(x) vapply(x2, "[[", character(1), "login") \dontshow{\}) # examplesIf} } \seealso{ The \code{.limit} argument to \code{\link[=gh]{gh()}} supports fetching more than one page. } gh/man/gh_rate_limit.Rd0000644000176200001440000000320713710670253014527 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gh_rate_limit.R \name{gh_rate_limit} \alias{gh_rate_limit} \title{Return GitHub user's current rate limits} \usage{ gh_rate_limit( response = NULL, .token = NULL, .api_url = NULL, .send_headers = NULL ) } \arguments{ \item{response}{\code{gh_response} object from a previous \code{gh} call, rate limit values are determined from values in the response header. Optional argument, if missing a call to "GET /rate_limit" will be made.} \item{.token}{Authentication token. Defaults to \code{GITHUB_PAT} or \code{GITHUB_TOKEN} environment variables, in this order if any is set. See \code{\link[=gh_token]{gh_token()}} if you need more flexibility, e.g. different tokens for different GitHub Enterprise deployments.} \item{.api_url}{Github API url (default: \url{https://api.github.com}). Used if \code{endpoint} just contains a path. Defaults to \code{GITHUB_API_URL} environment variable if set.} \item{.send_headers}{Named character vector of header field values (except \code{Authorization}, which is handled via \code{.token}). This can be used to override or augment the default \code{User-Agent} header: \code{"https://github.com/r-lib/gh"}.} } \value{ A \code{list} object containing the overall \code{limit}, \code{remaining} limit, and the limit \code{reset} time. } \description{ Reports the current rate limit status for the authenticated user, either pulls this information from a previous successful request or directly from the GitHub API. } \details{ Further details on GitHub's API rate limit policies are available at \url{https://docs.github.com/v3/#rate-limiting}. } gh/man/gh_whoami.Rd0000644000176200001440000000513214042732754013665 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gh_whoami.R \name{gh_whoami} \alias{gh_whoami} \title{Info on current GitHub user and token} \usage{ gh_whoami(.token = NULL, .api_url = NULL, .send_headers = NULL) } \arguments{ \item{.token}{Authentication token. Defaults to \code{GITHUB_PAT} or \code{GITHUB_TOKEN} environment variables, in this order if any is set. See \code{\link[=gh_token]{gh_token()}} if you need more flexibility, e.g. different tokens for different GitHub Enterprise deployments.} \item{.api_url}{Github API url (default: \url{https://api.github.com}). Used if \code{endpoint} just contains a path. Defaults to \code{GITHUB_API_URL} environment variable if set.} \item{.send_headers}{Named character vector of header field values (except \code{Authorization}, which is handled via \code{.token}). This can be used to override or augment the default \code{User-Agent} header: \code{"https://github.com/r-lib/gh"}.} } \value{ A \code{gh_response} object, which is also a \code{list}. } \description{ Reports wallet name, GitHub login, and GitHub URL for the current authenticated user, the first bit of the token, and the associated scopes. } \details{ Get a personal access token for the GitHub API from \url{https://github.com/settings/tokens} and select the scopes necessary for your planned tasks. The \code{repo} scope, for example, is one many are likely to need. On macOS and Windows it is best to store the token in the git credential store, where most GitHub clients, including gh, can access it. You can use the gitcreds package to add your token to the credential store:\if{html}{\out{
}}\preformatted{gitcreds::gitcreds_set() }\if{html}{\out{
}} See \url{https://gh.r-lib.org/articles/managing-personal-access-tokens.html} and \url{https://usethis.r-lib.org/articles/articles/git-credentials.html} for more about managing GitHub (and generic git) credentials. On other systems, including Linux, the git credential store is typically not as convenient, and you might want to store your token in the \code{GITHUB_PAT} environment variable, which you can set in your \code{.Renviron} file. } \examples{ \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} gh_whoami() \dontshow{\}) # examplesIf} \dontshow{if (FALSE) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} ## explicit token + use with GitHub Enterprise gh_whoami(.token = "8c70fd8419398999c9ac5bacf3192882193cadf2", .api_url = "https://github.foobar.edu/api/v3") \dontshow{\}) # examplesIf} } gh/man/gh.Rd0000644000176200001440000001743314042726266012331 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gh.R \name{gh} \alias{gh} \title{Query the GitHub API} \usage{ gh( endpoint, ..., per_page = NULL, .token = NULL, .destfile = NULL, .overwrite = FALSE, .api_url = NULL, .method = "GET", .limit = NULL, .accept = "application/vnd.github.v3+json", .send_headers = NULL, .progress = TRUE, .params = list() ) } \arguments{ \item{endpoint}{GitHub API endpoint. Must be one of the following forms: \itemize{ \item \verb{METHOD path}, e.g. \code{GET /rate_limit}, \item \code{path}, e.g. \verb{/rate_limit}, \item \verb{METHOD url}, e.g. \verb{GET https://api.github.com/rate_limit}, \item \code{url}, e.g. \verb{https://api.github.com/rate_limit}. } If the method is not supplied, will use \code{.method}, which defaults to \code{"GET"}.} \item{...}{Name-value pairs giving API parameters. Will be matched into \code{endpoint} placeholders, sent as query parameters in GET requests, and as a JSON body of POST requests. If there is only one unnamed parameter, and it is a raw vector, then it will not be JSON encoded, but sent as raw data, as is. This can be used for example to add assets to releases. Named \code{NULL} values are silently dropped. For GET requests, named \code{NA} values trigger an error. For other methods, named \code{NA} values are included in the body of the request, as JSON \code{null}.} \item{per_page}{Number of items to return per page. If omitted, will be substituted by \code{max(.limit, 100)} if \code{.limit} is set, otherwise determined by the API (never greater than 100).} \item{.token}{Authentication token. Defaults to \code{GITHUB_PAT} or \code{GITHUB_TOKEN} environment variables, in this order if any is set. See \code{\link[=gh_token]{gh_token()}} if you need more flexibility, e.g. different tokens for different GitHub Enterprise deployments.} \item{.destfile}{Path to write response to disk. If \code{NULL} (default), response will be processed and returned as an object. If path is given, response will be written to disk in the form sent.} \item{.overwrite}{If \code{.destfile} is provided, whether to overwrite an existing file. Defaults to \code{FALSE}.} \item{.api_url}{Github API url (default: \url{https://api.github.com}). Used if \code{endpoint} just contains a path. Defaults to \code{GITHUB_API_URL} environment variable if set.} \item{.method}{HTTP method to use if not explicitly supplied in the \code{endpoint}.} \item{.limit}{Number of records to return. This can be used instead of manual pagination. By default it is \code{NULL}, which means that the defaults of the GitHub API are used. You can set it to a number to request more (or less) records, and also to \code{Inf} to request all records. Note, that if you request many records, then multiple GitHub API calls are used to get them, and this can take a potentially long time.} \item{.accept}{The value of the \code{Accept} HTTP header. Defaults to \code{"application/vnd.github.v3+json"} . If \code{Accept} is given in \code{.send_headers}, then that will be used. This parameter can be used to provide a custom media type, in order to access a preview feature of the API.} \item{.send_headers}{Named character vector of header field values (except \code{Authorization}, which is handled via \code{.token}). This can be used to override or augment the default \code{User-Agent} header: \code{"https://github.com/r-lib/gh"}.} \item{.progress}{Whether to show a progress indicator for calls that need more than one HTTP request.} \item{.params}{Additional list of parameters to append to \code{...}. It is easier to use this than \code{...} if you have your parameters in a list already.} } \value{ Answer from the API as a \code{gh_response} object, which is also a \code{list}. Failed requests will generate an R error. Requests that generate a raw response will return a raw vector. } \description{ This is an extremely minimal client. You need to know the API to be able to use this client. All this function does is: \itemize{ \item Try to substitute each listed parameter into \code{endpoint}, using the \code{{parameter}} notation. \item If a GET request (the default), then add all other listed parameters as query parameters. \item If not a GET request, then send the other parameters in the request body, as JSON. \item Convert the response to an R list using \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}}. } } \examples{ \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} ## Repositories of a user, these are equivalent gh("/users/hadley/repos") gh("/users/{username}/repos", username = "hadley") ## Starred repositories of a user gh("/users/hadley/starred") gh("/users/{username}/starred", username = "hadley") \dontshow{\}) # examplesIf} \dontshow{if (FALSE) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} ## Create a repository, needs a token in GITHUB_PAT (or GITHUB_TOKEN) ## environment variable gh("POST /user/repos", name = "foobar") \dontshow{\}) # examplesIf} \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} ## Issues of a repository gh("/repos/hadley/dplyr/issues") gh("/repos/{owner}/{repo}/issues", owner = "hadley", repo = "dplyr") ## Automatic pagination users <- gh("/users", .limit = 50) length(users) \dontshow{\}) # examplesIf} \dontshow{if (FALSE) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} ## Access developer preview of Licenses API (in preview as of 2015-09-24) gh("/licenses") # used to error code 415 gh("/licenses", .accept = "application/vnd.github.drax-preview+json") \dontshow{\}) # examplesIf} \dontshow{if (FALSE) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} ## Access Github Enterprise API ## Use GITHUB_API_URL environment variable to change the default. gh("/user/repos", type = "public", .api_url = "https://github.foobar.edu/api/v3") \dontshow{\}) # examplesIf} \dontshow{if (FALSE) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} ## Use I() to force body part to be sent as an array, even if length 1 ## This works whether assignees has length 1 or > 1 assignees <- "gh_user" assignees <- c("gh_user1", "gh_user2") gh("PATCH /repos/OWNER/REPO/issues/1", assignees = I(assignees)) \dontshow{\}) # examplesIf} \dontshow{if (FALSE) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} ## There are two ways to send JSON data. One is that you supply one or ## more objects that will be converted to JSON automatically via ## jsonlite::toJSON(). In this case sometimes you need to use ## jsonlite::unbox() because fromJSON() creates lists from scalar vectors ## by default. The Content-Type header is automatically added in this ## case. For example this request turns on GitHub Pages, using this ## API: https://docs.github.com/v3/repos/pages/#enable-a-pages-site gh::gh( "POST /repos/{owner}/{repo}/pages", owner = "gaborcsardi", repo = "playground", source = list( branch = jsonlite::unbox("master"), path = jsonlite::unbox("/docs") ), .send_headers = c(Accept = "application/vnd.github.switcheroo-preview+json") ) ## The second way is to handle the JSON encoding manually, and supply it ## as a raw vector in an unnamed argument, and also a Content-Type header: body <- '{ "source": { "branch": "master", "path": "/docs" } }' gh::gh( "POST /repos/{owner}/{repo}/pages", owner = "gaborcsardi", repo = "playground", charToRaw(body), .send_headers = c( Accept = "application/vnd.github.switcheroo-preview+json", "Content-Type" = "application/json" ) ) \dontshow{\}) # examplesIf} } \seealso{ \code{\link[=gh_gql]{gh_gql()}} if you want to use the GitHub GraphQL API, \code{\link[=gh_whoami]{gh_whoami()}} for details on GitHub API token management. } gh/man/gh_token.Rd0000644000176200001440000000425514031420134013506 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gh_token.R \name{gh_token} \alias{gh_token} \title{Return the local user's GitHub Personal Access Token (PAT)} \usage{ gh_token(api_url = NULL) } \arguments{ \item{api_url}{GitHub API URL. Defaults to the \code{GITHUB_API_URL} environment variable, if set, and otherwise to \url{https://api.github.com}.} } \value{ A string of characters, if a PAT is found, or the empty string, otherwise. For convenience, the return value has an S3 class in order to ensure that simple printing strategies don't reveal the entire PAT. } \description{ If gh can find a personal access token (PAT) via \code{gh_token()}, it includes the PAT in its requests. Some requests succeed without a PAT, but many require a PAT to prove the request is authorized by a specific GitHub user. A PAT also helps with rate limiting. If your gh use is more than casual, you want a PAT. gh calls \code{\link[gitcreds:gitcreds_get]{gitcreds::gitcreds_get()}} with the \code{api_url}, which checks session environment variables and then the local Git credential store for a PAT appropriate to the \code{api_url}. Therefore, if you have previously used a PAT with, e.g., command line Git, gh may retrieve and re-use it. You can call \code{\link[gitcreds:gitcreds_get]{gitcreds::gitcreds_get()}} directly, yourself, if you want to see what is found for a specific URL. If no matching PAT is found, \code{\link[gitcreds:gitcreds_get]{gitcreds::gitcreds_get()}} errors, whereas \code{gh_token()} does not and, instead, returns \code{""}. See GitHub's documentation on \href{https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token}{Creating a personal access token}, or use \code{usethis::create_github_token()} for a guided experience, including pre-selection of recommended scopes. Once you have a PAT, you can use \code{\link[gitcreds:gitcreds_get]{gitcreds::gitcreds_set()}} to add it to the Git credential store. From that point on, gh (via \code{\link[gitcreds:gitcreds_get]{gitcreds::gitcreds_get()}}) should be able to find it without further effort on your part. } \examples{ \dontrun{ gh_token() format(gh_token()) str(gh_token()) } } gh/man/print.gh_response.Rd0000644000176200001440000000056713243233512015367 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/print.R \name{print.gh_response} \alias{print.gh_response} \title{Print the result of a GitHub API call} \usage{ \method{print}{gh_response}(x, ...) } \arguments{ \item{x}{The result object.} \item{...}{Ignored.} } \value{ The JSON result. } \description{ Print the result of a GitHub API call } gh/man/gh_tree_remote.Rd0000644000176200001440000000123013611545611014701 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/git.R \name{gh_tree_remote} \alias{gh_tree_remote} \title{Find the GitHub remote associated with a path} \usage{ gh_tree_remote(path = ".") } \arguments{ \item{path}{Path that is contained within a git repo.} } \value{ If the repo has a github remote, a list containing \code{username} and \code{repo}. Otherwise, an error. } \description{ This is handy helper if you want to make gh requests related to the current project. } \examples{ \dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} gh_tree_remote() \dontshow{\}) # examplesIf} } gh/DESCRIPTION0000644000176200001440000000224114042757216012365 0ustar liggesusersPackage: gh Title: 'GitHub' 'API' Version: 1.3.0 Authors@R: c(person(given = "Gábor", family = "Csárdi", role = c("cre", "ctb"), email = "csardi.gabor@gmail.com"), person(given = "Jennifer", family = "Bryan", role = "aut"), person(given = "Hadley", family = "Wickham", role = "aut"), person(given = "RStudio", role = c("cph", "fnd"))) Description: Minimal client to access the 'GitHub' 'API'. License: MIT + file LICENSE URL: https://gh.r-lib.org/, https://github.com/r-lib/gh#readme BugReports: https://github.com/r-lib/gh/issues Imports: cli (>= 2.0.1), gitcreds, httr (>= 1.2), ini, jsonlite Suggests: covr, knitr, mockery, rmarkdown, rprojroot, spelling, testthat (>= 2.3.2), withr VignetteBuilder: knitr Encoding: UTF-8 Language: en-US RoxygenNote: 7.1.1.9001 NeedsCompilation: no Packaged: 2021-04-30 08:22:26 UTC; gaborcsardi Author: Gábor Csárdi [cre, ctb], Jennifer Bryan [aut], Hadley Wickham [aut], RStudio [cph, fnd] Maintainer: Gábor Csárdi Repository: CRAN Date/Publication: 2021-04-30 10:40:14 UTC gh/build/0000755000176200001440000000000014042737101011746 5ustar liggesusersgh/build/vignette.rds0000644000176200001440000000034714042737101014311 0ustar liggesusers @R x@CE7tWn=y62g!$#iHV U,:\w])N3:YIq8&9My#a;};|?7lgUfA0UjQ\Q#IK,ޖLjt^YNƹQ|.J") }) # URL processing helpers ---- test_that("get_baseurl() insists on http(s)", { expect_error(get_baseurl("github.com"), "protocols") expect_error(get_baseurl("github.acme.com"), "protocols") }) test_that("get_baseurl() works", { x <- "https://github.com" expect_equal(get_baseurl("https://github.com"), x) expect_equal(get_baseurl("https://github.com/"), x) expect_equal(get_baseurl("https://github.com/stuff"), x) expect_equal(get_baseurl("https://github.com/stuff/"), x) expect_equal(get_baseurl("https://github.com/more/stuff"), x) x <- "https://api.github.com" expect_equal(get_baseurl("https://api.github.com"), x) expect_equal(get_baseurl("https://api.github.com/rate_limit"), x) x <- "https://github.acme.com" expect_equal(get_baseurl("https://github.acme.com"), x) expect_equal(get_baseurl("https://github.acme.com/"), x) expect_equal(get_baseurl("https://github.acme.com/api/v3"), x) # so (what little) support we have for user@host doesn't regress expect_equal( get_baseurl("https://jane@github.acme.com/api/v3"), "https://jane@github.acme.com" ) }) test_that("is_github_dot_com() works", { expect_true(is_github_dot_com("https://github.com")) expect_true(is_github_dot_com("https://api.github.com")) expect_true(is_github_dot_com("https://api.github.com/rate_limit")) expect_true(is_github_dot_com("https://api.github.com/graphql")) expect_false(is_github_dot_com("https://github.acme.com")) expect_false(is_github_dot_com("https://github.acme.com/api/v3")) expect_false(is_github_dot_com("https://github.acme.com/api/v3/user")) }) test_that("get_hosturl() works", { x <- "https://github.com" expect_equal(get_hosturl("https://github.com"), x) expect_equal(get_hosturl("https://api.github.com"), x) x <- "https://github.acme.com" expect_equal(get_hosturl("https://github.acme.com"), x) expect_equal(get_hosturl("https://github.acme.com/api/v3"), x) }) test_that("get_apiurl() works", { x <- "https://api.github.com" expect_equal(get_apiurl("https://github.com"), x) expect_equal(get_apiurl("https://github.com/"), x) expect_equal(get_apiurl("https://github.com/r-lib/gh/issues"), x) expect_equal(get_apiurl("https://api.github.com"), x) expect_equal(get_apiurl("https://api.github.com/rate_limit"), x) x <- "https://github.acme.com/api/v3" expect_equal(get_apiurl("https://github.acme.com"), x) expect_equal(get_apiurl("https://github.acme.com/OWNER/REPO"), x) expect_equal(get_apiurl("https://github.acme.com/api/v3"), x) }) gh/tests/testthat/test-utils.R0000644000176200001440000000260513725421720016120 0ustar liggesuserstest_that("can detect presence vs absence names", { expect_identical(has_name(list("foo", "bar")), c(FALSE, FALSE)) expect_identical(has_name(list(a = "foo", "bar")), c(TRUE, FALSE)) expect_identical(has_name({ x <- list("foo", "bar"); names(x)[1] <- "a"; x }), c(TRUE, FALSE)) expect_identical(has_name({ x <- list("foo", "bar"); names(x)[1] <- "a"; names(x)[2] <- ""; x }), c(TRUE, FALSE)) expect_identical(has_name({ x <- list("foo", "bar"); names(x)[1] <- ""; x }), c(FALSE, FALSE)) expect_identical(has_name({ x <- list("foo", "bar"); names(x)[1] <- ""; names(x)[2] <- ""; x }), c(FALSE, FALSE)) }) test_that("named NULL is dropped", { tcs <- list( list(list(), list()), list(list(a = 1), list(a = 1)), list(list(NULL), list(NULL)), list(list(a = NULL), list()), list(list(NULL, a = NULL, 1), list(NULL, 1)), list(list(a = NULL, b = 1, 5), list(b = 1, 5)) ) for (tc in tcs) { expect_identical( drop_named_nulls(tc[[1]]), tc[[2]], info = tc ) } }) test_that("named NA is error", { goodtcs <- list( list(), list(NA), list(NA, NA_integer_, a = 1) ) badtcs <- list( list(b = NULL, a = NA), list(a = NA_integer_), list(NA, c = NA_real_) ) for (tc in goodtcs) { expect_silent(check_named_nas(tc)) } for (tc in badtcs) { expect_error(check_named_nas(tc)) } }) gh/tests/testthat/test-gh_request.R0000644000176200001440000000711614042726434017133 0ustar liggesuserstest_that("all forms of specifying endpoint are equivalent", { r1 <- gh_build_request("GET /rate_limit") expect_equal(r1$method, "GET") expect_equal(r1$url, "https://api.github.com/rate_limit") expect_equal(gh_build_request("/rate_limit"), r1) expect_equal(gh_build_request("GET https://api.github.com/rate_limit"), r1) expect_equal(gh_build_request("https://api.github.com/rate_limit"), r1) }) test_that("method arg sets default method", { r <- gh_build_request("/rate_limit", method = "POST") expect_equal(r$method, "POST") }) test_that("parameter substitution is equivalent to direct specification (:)", { subst <- gh_build_request("POST /repos/:org/:repo/issues/:number/labels", params = list(org = "ORG", repo = "REPO", number = "1", "body")) spec <- gh_build_request("POST /repos/ORG/REPO/issues/1/labels", params = list("body")) expect_identical(subst, spec) }) test_that("parameter substitution is equivalent to direct specification", { subst <- gh_build_request("POST /repos/{org}/{repo}/issues/{number}/labels", params = list(org = "ORG", repo = "REPO", number = "1", "body")) spec <- gh_build_request("POST /repos/ORG/REPO/issues/1/labels", params = list("body")) expect_identical(subst, spec) }) test_that("URI templates that need expansion are detected", { expect_true(is_uri_template("/orgs/{org}/repos")) expect_true(is_uri_template("/repos/{owner}/{repo}")) expect_false(is_uri_template("/user/repos")) }) test_that("older 'colon templates' are detected", { expect_true(is_colon_template("/orgs/:org/repos")) expect_true(is_colon_template("/repos/:owner/:repo")) expect_false(is_colon_template("/user/repos")) }) test_that("gh_set_endpoint() works", { # no expansion, no extra params input <- list(endpoint = "/user/repos") expect_equal(input, gh_set_endpoint(input)) # no expansion, with extra params input <- list(endpoint = "/user/repos", params = list(page = 2)) expect_equal(input, gh_set_endpoint(input)) # expansion, no extra params input <- list( endpoint = "/repos/{owner}/{repo}", params = list(owner = "OWNER", repo = "REPO") ) out <- gh_set_endpoint(input) expect_equal( out, list(endpoint = "/repos/OWNER/REPO", params = list()) ) # expansion, with extra params input <- list( endpoint = "/repos/{owner}/{repo}/issues", params = list(state = "open", owner = "OWNER", repo = "REPO", page = 2) ) out <- gh_set_endpoint(input) expect_equal(out$endpoint, "/repos/OWNER/REPO/issues") expect_equal(out$params, list(state = "open", page = 2)) }) test_that("gh_set_endpoint() refuses to substitute an NA", { input <- list( endpoint = "POST /orgs/{org}/repos", params = list(org = NA) ) expect_error(gh_set_endpoint(input), "Named NA") }) test_that("gh_set_endpoint() allows a named NA in body for non-GET", { input <- list( endpoint = "PUT /repos/{owner}/{repo}/pages", params = list(owner = "OWNER", repo = "REPO", cname = NA) ) out <- gh_set_endpoint(input) expect_equal(out$endpoint, "PUT /repos/OWNER/REPO/pages") expect_equal(out$params, list(cname = NA)) }) test_that("gh_set_url() ensures URL is in 'API form'", { input <- list( endpoint = "/user/repos", api_url = "https://github.com" ) out <- gh_set_url(input) expect_equal(out$api_url, "https://api.github.com") input$api_url <- "https://github.acme.com" out <- gh_set_url(input) expect_equal(out$api_url, "https://github.acme.com/api/v3") }) gh/tests/testthat.R0000644000176200001440000000006013730160436013774 0ustar liggesuserslibrary(testthat) library(gh) test_check("gh") gh/vignettes/0000755000176200001440000000000014042737102012660 5ustar liggesusersgh/vignettes/managing-personal-access-tokens.Rmd0000644000176200001440000001602414031420134021461 0ustar liggesusers--- title: "Managing Personal Access Tokens" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Managing Personal Access Tokens} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(gh) ``` gh generally sends a Personal Access Token (PAT) with its requests. Some endpoints of the GitHub API can be accessed without authenticating yourself. But once your API use becomes more frequent, you will want a PAT to prevent problems with rate limits and to access all possible endpoints. This article describes how to store your PAT, so that gh can find it (automatically, in most cases). The function gh uses for this is `gh_token()`. More resources on PAT management: * GitHub documentation on [Creating a personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) * In the [usethis package](https://usethis.r-lib.org): - Vignette: [Managing Git(Hub) Credentials](https://usethis.r-lib.org/articles/articles/git-credentials.html) - `usethis::gh_token_help()` and `usethis::git_sitrep()` help you check if a PAT is discoverable and has suitable scopes - `usethis::create_github_token()` guides you through the process of getting a new PAT * In the [gitcreds package](https://gitcreds.r-lib.org/): - `gitcreds::gitcreds_set()` helps you explicitly put your PAT into the Git credential store ## PAT and host `gh::gh()` allows the user to provide a PAT via the `.token` argument and to specify a host other than "github.com" via the `.api_url` argument. (Some companies and universities run their own instance of GitHub Enterprise.) ```{r, eval = FALSE} gh(endpoint, ..., .token = NULL, ..., .api_url = NULL, ...) ``` However, it's annoying to always provide your PAT or host and it's unsafe for your PAT to appear explicitly in your R code. It's important to make it *possible* for the user to provide the PAT and/or API URL directly, but it should rarely be necessary. `gh::gh()` is designed to play well with more secure, less fiddly methods for expressing what you want. How are `.api_url` and `.token` determined when the user does not provide them? 1. `.api_url` defaults to the value of the `GITHUB_API_URL` environment variable and, if that is unset, falls back to `"https://api.github.com"`. This is always done before worrying about the PAT. 1. The PAT is obtained via a call to `gh_token(.api_url)`. That is, the token is looked up based on the host. ## The gitcreds package gh now uses the gitcreds package to interact with the Git credential store. gh calls `gitcreds::gitcreds_get()` with a URL to try to find a matching PAT. `gitcreds::gitcreds_get()` checks session environment variables and then the local Git credential store. Therefore, if you have previously used a PAT with, e.g., command line Git, gh may retrieve and re-use it. You can call `gitcreds::gitcreds_get()` directly, yourself, if you want to see what is found for a specific URL. ``` r gitcreds::gitcreds_get() ``` If you see something like this: ``` r #> #> protocol: https #> host : github.com #> username: PersonalAccessToken #> password: <-- hidden --> ``` that means that gitcreds could get the PAT from the Git credential store. You can call `gitcreds_get()$password` to see the actual PAT. If no matching PAT is found, `gitcreds::gitcreds_get()` errors. ## PAT in an environment variable If you don't have a Git installation, or your Git installation does not have a working credential store, then you can specify the PAT in an environment variable. For `github.com` you can set the `GITHUB_PAT_GITHUB_COM` or `GITHUB_PAT` variable. For a different GitHub host, call `gitcreds::gitcreds_cache_envvar()` with the API URL to see the environment variable you need to set. For example: ```{r} gitcreds::gitcreds_cache_envvar("https://github.acme.com") ``` ## Recommendations On a machine used for interactive development, we recommend: * Store your PAT(s) in an official credential store. * Do **not** store your PAT(s) in plain text in, e.g., `.Renviron`. In the past, this has been a common and recommended practice for pragmatic reasons. However, gitcreds/gh have now evolved to the point where it's possible for all of us to follow better security practices. * If you use a general-purpose password manager, like 1Password or LastPass, you may *also* want to store your PAT(s) there. Why? If your PAT is "forgotten" from the OS-level credential store, intentionally or not, you'll need to provide it again when prompted. If you don't have any other record of your PAT, you'll have to get a new PAT whenever this happens. This is not the end of the world. But if you aren't disciplined about deleting lost PATs from , you will eventually find yourself in a confusing situation where you can't be sure which PAT(s) are in use. On a headless system, such as on a CI/CD platform, provide the necessary PAT(s) via secure environment variables. Regular environment variables can be used to configure less sensitive settings, such as the API host. Don't expose your PAT by doing something silly like dumping all environment variables to a log file. Note that on GitHub Actions, specifically, a personal access token is [automatically available to the workflow](https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token) as the `GITHUB_TOKEN` secret. That is why many workflows in the R community contain this snippet: ``` yaml env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} ``` This makes the automatic PAT available as the `GITHUB_PAT` environment variable. If that PAT doesn't have the right permissions, then you'll need to explicitly provide one that does (see link above for more). ## Failure If there is no PAT to be had, `gh::gh()` sends a request with no token. (Internally, the `Authorization` header is omitted if the PAT is found to be the empty string, `""`.) What do PAT-related failures look like? If no PAT is sent and the endpoint requires no auth, the request probably succeeds! At least until you run up against rate limits. If the endpoint requires auth, you'll get an HTTP error, possibly this one: ``` GitHub API error (401): 401 Unauthorized Message: Requires authentication ``` If a PAT is first discovered in an environment variable, it is taken at face value. The two most common ways to arrive here are PAT specification via `.Renviron` or as a secret in a CI/CD platform, such as GitHub Actions. If the PAT is invalid, the first affected request will fail, probably like so: ``` GitHub API error (401): 401 Unauthorized Message: Bad credentials ``` This will also be the experience if an invalid PAT is provided directly via `.token`. Even a valid PAT can lead to a downstream error, if it has insufficient scopes with respect to a specific request. gh/R/0000755000176200001440000000000014042732745011061 5ustar liggesusersgh/R/print.R0000644000176200001440000000077713725421720012346 0ustar liggesusers #' Print the result of a GitHub API call #' #' @param x The result object. #' @param ... Ignored. #' @return The JSON result. #' #' @importFrom jsonlite prettify toJSON #' @export #' @method print gh_response print.gh_response <- function(x, ...) { if (inherits(x, c("raw", "path"))) { attr(x, c("method")) <- NULL attr(x, c("response")) <- NULL attr(x, ".send_headers") <- NULL print.default(x) } else { print(toJSON(unclass(x), pretty = TRUE, auto_unbox = TRUE, force = TRUE)) } } gh/R/utils.R0000644000176200001440000000445113713261205012340 0ustar liggesusers trim_ws <- function(x) { sub("\\s*$", "", sub("^\\s*", "", x)) } ## from devtools, among other places compact <- function(x) { is_empty <- vapply(x, function(x) length(x) == 0, logical(1)) x[!is_empty] } ## from purrr, among other places `%||%` <- function(x, y) { if (is.null(x)) { y } else { x } } ## as seen in purrr, with the name `has_names()` has_name <- function(x) { nms <- names(x) if (is.null(nms)) { rep_len(FALSE, length(x)) } else { !(is.na(nms) | nms == "") } } has_no_names <- function(x) all(!has_name(x)) ## if all names are "", strip completely cleanse_names <- function(x) { if (has_no_names(x)) { names(x) <- NULL } x } ## to process HTTP headers, i.e. combine defaults w/ user-specified headers ## in the spirit of modifyList(), except ## x and y are vectors (not lists) ## name comparison is case insensitive ## http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 ## x will be default headers, y will be user-specified modify_vector <- function(x, y = NULL) { if (length(y) == 0L) return(x) lnames <- function(x) tolower(names(x)) c(x[!(lnames(x) %in% lnames(y))], y) } discard <- function(.x, .p, ...) { sel <- probe(.x, .p, ...) .x[is.na(sel) | !sel] } probe <- function(.x, .p, ...) { if (is.logical(.p)) { stopifnot(length(.p) == length(.x)) .p } else { vapply(.x, .p, logical(1), ...) } } drop_named_nulls <- function(x) { if (has_no_names(x)) return(x) named <- has_name(x) null <- vapply(x, is.null, logical(1)) cleanse_names(x[! named | ! null]) } check_named_nas <- function(x) { if (has_no_names(x)) return(x) named <- has_name(x) na <- vapply(x, FUN.VALUE = logical(1), function(v) { is.atomic(v) && anyNA(v) }) bad <- which(named & na) if (length(bad)) { str <- paste0("`", names(x)[bad], "`", collapse = ", ") stop("Named NA parameters are not allowed: ", str) } } can_load <- function(pkg) { isTRUE(requireNamespace(pkg, quietly = TRUE)) } is_interactive <- function() { opt <- getOption("rlib_interactive") if (isTRUE(opt)) { TRUE } else if (identical(opt, FALSE)) { FALSE } else if (tolower(getOption("knitr.in.progress", "false")) == "true") { FALSE } else if (identical(Sys.getenv("TESTTHAT"), "true")) { FALSE } else { interactive() } } gh/R/gh_request.R0000644000176200001440000001360613757672312013365 0ustar liggesusers## Main API URL default_api_url <- function() { Sys.getenv('GITHUB_API_URL', unset = "https://api.github.com") } ## Headers to send with each API request default_send_headers <- c("User-Agent" = "https://github.com/r-lib/gh") gh_build_request <- function(endpoint = "/user", params = list(), token = NULL, destfile = NULL, overwrite = NULL, accept = NULL, send_headers = NULL, api_url = NULL, method = "GET") { working <- list(method = method, url = character(), headers = NULL, query = NULL, body = NULL, endpoint = endpoint, params = params, token = token, accept = c(Accept = accept), send_headers = send_headers, api_url = api_url, dest = destfile, overwrite = overwrite) working <- gh_set_verb(working) working <- gh_set_endpoint(working) working <- gh_set_query(working) working <- gh_set_body(working) working <- gh_set_url(working) working <- gh_set_headers(working) working <- gh_set_dest(working) working[c("method", "url", "headers", "query", "body", "dest")] } ## gh_set_*(x) ## x = a list in which we build up an httr request ## x goes in, x comes out, possibly modified gh_set_verb <- function(x) { if (!nzchar(x$endpoint)) return(x) # No method defined, so use default if (grepl("^/", x$endpoint) || grepl("^http", x$endpoint)) { return(x) } x$method <- gsub("^([^/ ]+)\\s+.*$", "\\1", x$endpoint) stopifnot(x$method %in% c("GET", "POST", "PATCH", "PUT", "DELETE")) x$endpoint <- gsub("^[A-Z]+ ", "", x$endpoint) x } gh_set_endpoint <- function(x) { params <- x$params if (!is_template(x$endpoint) || length(params) == 0L || has_no_names(params)) { return(x) } named_params <- which(has_name(params)) done <- rep_len(FALSE, length(params)) endpoint <- endpoint2 <- x$endpoint for (i in named_params) { endpoint2 <- expand_variable( varname = names(params)[i], value = params[[i]][1], template = endpoint ) if (is.na(endpoint2)) { throw(new_error( "Named NA parameters are not allowed: ", names(params)[i], call. = FALSE )) } if (endpoint2 != endpoint) { endpoint <- endpoint2 done[i] <- TRUE } if (!is_template(endpoint)) { break } } x$endpoint <- endpoint x$params <- x$params[!done] x$params <- cleanse_names(x$params) x } gh_set_query <- function(x) { params <- x$params if (x$method != "GET" || length(params) == 0L) { return(x) } stopifnot(all(has_name(params))) x$query <- params x$params <- NULL x } gh_set_body <- function(x) { if (length(x$params) == 0L) return(x) if (x$method == "GET") { warning("This is a 'GET' request and unnamed parameters are being ignored.") return(x) } if (length(x$params) == 1 && is.raw(x$params[[1]])) { x$body <- x$params[[1]] } else { x$body <- toJSON(x$params, auto_unbox = TRUE) } x } gh_set_url <- function(x) { if (grepl("^https?://", x$endpoint)) { x$url <- URLencode(x$endpoint) x$api_url <- get_baseurl(x$url) } else { x$api_url <- get_apiurl(x$api_url %||% default_api_url()) x$url <- URLencode(paste0(x$api_url, x$endpoint)) } x } get_baseurl <- function(url) { # https://github.uni.edu/api/v3/ if (!any(grepl("^https?://", url))) { stop("Only works with HTTP(S) protocols") } prot <- sub("^(https?://).*$", "\\1", url) # https:// rest <- sub("^https?://(.*)$", "\\1", url) # github.uni.edu/api/v3/ host <- sub("/.*$", "", rest) # github.uni.edu paste0(prot, host) # https://github.uni.edu } # https://api.github.com --> https://github.com # api.github.com --> github.com normalize_host <- function(x) { sub("api[.]github[.]com", "github.com", x) } get_hosturl <- function(url) { url <- get_baseurl(url) normalize_host(url) } # (almost) the inverse of get_hosturl() # https://github.com --> https://api.github.com # https://github.uni.edu --> https://github.uni.edu/api/v3 get_apiurl <- function(url) { host_url <- get_hosturl(url) prot_host <- strsplit(host_url, "://", fixed = TRUE)[[1]] if (is_github_dot_com(host_url)) { paste0(prot_host[[1]], "://api.github.com") } else { paste0(host_url, "/api/v3") } } is_github_dot_com <- function(url) { url <- get_baseurl(url) url <- normalize_host(url) grepl("^https?://github.com", url) } gh_set_headers <- function(x) { # x$api_url must be set properly at this point auth <- gh_auth(x$token %||% gh_token(x$api_url)) send_headers <- gh_send_headers(x$accept, x$send_headers) x$headers <- c(send_headers, auth) x } gh_send_headers <- function(accept_header = NULL, headers = NULL) { modify_vector( modify_vector(default_send_headers, accept_header), headers ) } #' @importFrom httr write_disk write_memory gh_set_dest <- function(x) { if (is.null(x$dest)) { x$dest <- write_memory() } else { x$dest <- write_disk(x$dest, overwrite = x$overwrite) } x } # helpers ---- # https://tools.ietf.org/html/rfc6570 # we support what the RFC calls "Level 1 templates", which only require # simple string expansion of a placeholder consisting of [A-Za-z0-9_] is_template <- function(x) { is_colon_template(x) || is_uri_template(x) } is_colon_template <- function(x) grepl(":", x) is_uri_template <- function(x) grepl("[{]\\w+?[}]", x) template_type <- function(x) { if (is_uri_template(x)) { return("uri") } if (is_colon_template(x)) { return("colon") } } expand_variable <- function(varname, value, template) { type <- template_type(template) if (is.null(type)) { return(template) } pattern <- switch( type, uri = paste0("[{]", varname, "[}]"), colon = paste0(":", varname, "\\b"), stop("Internal error: unrecognized template type") ) gsub(pattern, value, template) } gh/R/gh_whoami.R0000644000176200001440000000434314042732745013152 0ustar liggesusers#' Info on current GitHub user and token #' #' Reports wallet name, GitHub login, and GitHub URL for the current #' authenticated user, the first bit of the token, and the associated scopes. #' #' Get a personal access token for the GitHub API from #' and select the scopes necessary for your #' planned tasks. The `repo` scope, for example, is one many are likely to need. #' #' On macOS and Windows it is best to store the token in the git credential #' store, where most GitHub clients, including gh, can access it. You can #' use the gitcreds package to add your token to the credential store: #' #' ```r #' gitcreds::gitcreds_set() #' ``` #' #' See #' and #' for more about managing GitHub (and generic git) credentials. #' #' On other systems, including Linux, the git credential store is #' typically not as convenient, and you might want to store your token in #' the `GITHUB_PAT` environment variable, which you can set in your #' `.Renviron` file. #' #' @inheritParams gh #' #' @return A `gh_response` object, which is also a `list`. #' @export #' #' @examplesIf identical(Sys.getenv("IN_PKGDOWN"), "true") #' gh_whoami() #' #' @examplesIf FALSE #' ## explicit token + use with GitHub Enterprise #' gh_whoami(.token = "8c70fd8419398999c9ac5bacf3192882193cadf2", #' .api_url = "https://github.foobar.edu/api/v3") gh_whoami <- function(.token = NULL, .api_url = NULL, .send_headers = NULL) { .token <- .token %||% gh_token(.api_url) if (isTRUE(.token == "")) { message("No personal access token (PAT) available.\n", "Obtain a PAT from here:\n", "https://github.com/settings/tokens\n", "For more on what to do with the PAT, see ?gh_whoami.") return(invisible(NULL)) } res <- gh(endpoint = "/user", .token = .token, .api_url = .api_url, .send_headers = .send_headers) scopes <- attr(res, "response")[["x-oauth-scopes"]] res <- res[c("name", "login", "html_url")] res$scopes <- scopes res$token <- format(gh_pat(.token)) ## 'gh_response' class has to be restored class(res) <- c("gh_response", "list") res } gh/R/gh_rate_limit.R0000644000176200001440000000265313710670253014015 0ustar liggesusers#' Return GitHub user's current rate limits #' #' Reports the current rate limit status for the authenticated user, #' either pulls this information from a previous successful request #' or directly from the GitHub API. #' #' Further details on GitHub's API rate limit policies are available at #' . #' #' @param response `gh_response` object from a previous `gh` call, rate #' limit values are determined from values in the response header. #' Optional argument, if missing a call to "GET /rate_limit" will be made. #' #' @inheritParams gh #' #' @return A `list` object containing the overall `limit`, `remaining` limit, and the #' limit `reset` time. #' #' @export gh_rate_limit = function(response = NULL, .token = NULL, .api_url = NULL, .send_headers = NULL) { if (is.null(response)) { # This end point does not count against limit .token <- .token %||% gh_token(.api_url) response <- gh("GET /rate_limit", .token = .token, .api_url = .api_url, .send_headers = .send_headers) } stopifnot(inherits(response, "gh_response")) http_res <- attr(response, "response") reset <- as.integer(c(http_res[["x-ratelimit-reset"]], NA)[1]) reset <- as.POSIXct(reset, origin = "1970-01-01") list( limit = as.integer(c(http_res[["x-ratelimit-limit"]], NA)[1]), remaining = as.integer(c(http_res[["x-ratelimit-remaining"]], NA)[1]), reset = reset ) } gh/R/git.R0000644000176200001440000000471013611545611011764 0ustar liggesusers#' Find the GitHub remote associated with a path #' #' This is handy helper if you want to make gh requests related to the #' current project. #' #' @param path Path that is contained within a git repo. #' @return If the repo has a github remote, a list containing `username` #' and `repo`. Otherwise, an error. #' @export #' @examplesIf interactive() #' gh_tree_remote() gh_tree_remote <- function(path = ".") { github_remote(git_remotes(path)) } github_remote <- function(x) { remotes <- lapply(x, github_remote_parse) remotes <- remotes[!vapply(remotes, is.null, logical(1))] if (length(remotes) == 0) { throw(new_error("No github remotes found", call. = FALSE)) } if (length(remotes) > 1) { if (any(names(remotes) == "origin")) { warning("Multiple github remotes found. Using origin.", call. = FALSE) remotes <- remotes[["origin"]] } else { warning("Multiple github remotes found. Using first.", call. = FALSE) remotes <- remotes[[1]] } } else { remotes[[1]] } } github_remote_parse <- function(x) { if (length(x) == 0) return(NULL) if (!grepl("github", x)) return(NULL) # https://github.com/hadley/devtools.git # https://github.com/hadley/devtools # git@github.com:hadley/devtools.git re <- "github[^/:]*[/:]([^/]+)/(.*?)(?:\\.git)?$" m <- regexec(re, x) match <- regmatches(x, m)[[1]] if (length(match) == 0) return(NULL) list( username = match[2], repo = match[3] ) } git_remotes <- function(path = ".") { conf <- git_config(path) remotes <- conf[grepl("^remote", names(conf))] remotes <- discard(remotes, function(x) is.null(x$url)) urls <- vapply(remotes, "[[", "url", FUN.VALUE = character(1)) names(urls) <- gsub('^remote "(.*?)"$', "\\1", names(remotes)) urls } git_config <- function(path = ".") { config_path <- file.path(repo_root(path), ".git", "config") if (!file.exists(config_path)) { throw(new_error("git config does not exist", call. = FALSE)) } ini::read.ini(config_path, "UTF-8") } repo_root <- function(path = ".") { if (!file.exists(path)) { throw(new_error("Can't find '", path, "'.", call. = FALSE)) } # Walk up to root directory while (!has_git(path)) { if (is_root(path)) { throw(new_error("Could not find git root.", call. = FALSE)) } path <- dirname(path) } path } has_git <- function(path) { file.exists(file.path(path, ".git")) } is_root <- function(path) { identical(path, dirname(path)) } gh/R/errors.R0000644000176200001440000006044414034551254012523 0ustar liggesusers # # Standalone file for better error handling ---------------------------- # # If can allow package dependencies, then you are probably better off # using rlang's functions for errors. # # The canonical location of this file is in the processx package: # https://github.com/r-lib/processx/master/R/errors.R # # ## Features # # - Throw conditions and errors with the same API. # - Automatically captures the right calls and adds them to the conditions. # - Sets `.Last.error`, so you can easily inspect the errors, even if they # were not caught. # - It only sets `.Last.error` for the errors that are not caught. # - Hierarchical errors, to allow higher level error messages, that are # more meaningful for the users, while also keeping the lower level # details in the error object. (So in `.Last.error` as well.) # - `.Last.error` always includes a stack trace. (The stack trace is # common for the whole error hierarchy.) The trace is accessible within # the error, e.g. `.Last.error$trace`. The trace of the last error is # also at `.Last.error.trace`. # - Can merge errors and traces across multiple processes. # - Pretty-print errors and traces, if the cli package is loaded. # - Automatically hides uninformative parts of the stack trace when # printing. # # ## API # # ``` # new_cond(..., call. = TRUE, domain = NULL) # new_error(..., call. = TRUE, domain = NULL) # throw(cond, parent = NULL) # catch_rethrow(expr, ...) # rethrow(expr, cond) # rethrow_call(.NAME, ...) # add_trace_back(cond) # ``` # # ## Roadmap: # - better printing of anonymous function in the trace # # ## NEWS: # # ### 1.0.0 -- 2019-06-18 # # * First release. # # ### 1.0.1 -- 2019-06-20 # # * Add `rlib_error_always_trace` option to always add a trace # # ### 1.0.2 -- 2019-06-27 # # * Internal change: change topenv of the functions to baseenv() # # ### 1.1.0 -- 2019-10-26 # # * Register print methods via onload_hook() function, call from .onLoad() # * Print the error manually, and the trace in non-interactive sessions # # ### 1.1.1 -- 2019-11-10 # # * Only use `trace` in parent errors if they are `rlib_error`s. # Because e.g. `rlang_error`s also have a trace, with a slightly # different format. # # ### 1.2.0 -- 2019-11-13 # # * Fix the trace if a non-thrown error is re-thrown. # * Provide print_this() and print_parents() to make it easier to define # custom print methods. # * Fix annotating our throw() methods with the incorrect `base::`. # # ### 1.2.1 -- 2020-01-30 # # * Update wording of error printout to be less intimidating, avoid jargon # * Use default printing in interactive mode, so RStudio can detect the # error and highlight it. # * Add the rethrow_call_with_cleanup function, to work with embedded # cleancall. # # ### 1.2.2 -- 2020-11-19 # # * Add the `call` argument to `catch_rethrow()` and `rethrow()`, to be # able to omit calls. # # ### 1.2.3 -- 2021-03-06 # # * Use cli instead of crayon # # ### 1.2.4 -- 2012-04-01 # # * Allow omitting the call with call. = FALSE in `new_cond()`, etc. err <- local({ # -- condition constructors ------------------------------------------- #' Create a new condition #' #' @noRd #' @param ... Parts of the error message, they will be converted to #' character and then concatenated, like in [stop()]. #' @param call. A call object to include in the condition, or `TRUE` #' or `NULL`, meaning that [throw()] should add a call object #' automatically. If `FALSE`, then no call is added. #' @param domain Translation domain, see [stop()]. #' @return Condition object. Currently a list, but you should not rely #' on that. new_cond <- function(..., call. = TRUE, domain = NULL) { message <- .makeMessage(..., domain = domain) structure( list(message = message, call = call.), class = c("condition")) } #' Create a new error condition #' #' It also adds the `rlib_error` class. #' #' @noRd #' @param ... Passed to [new_cond()]. #' @param call. Passed to [new_cond()]. #' @param domain Passed to [new_cond()]. #' @return Error condition object with classes `rlib_error`, `error` #' and `condition`. new_error <- function(..., call. = TRUE, domain = NULL) { cond <- new_cond(..., call. = call., domain = domain) class(cond) <- c("rlib_error", "error", "condition") cond } # -- throwing conditions ---------------------------------------------- #' Throw a condition #' #' If the condition is an error, it will also call [stop()], after #' signalling the condition first. This means that if the condition is #' caught by an exiting handler, then [stop()] is not called. #' #' @noRd #' @param cond Condition object to throw. If it is an error condition, #' then it calls [stop()]. #' @param parent Parent condition. Use this within [rethrow()] and #' [catch_rethrow()]. throw <- function(cond, parent = NULL) { if (!inherits(cond, "condition")) { throw(new_error("You can only throw conditions")) } if (!is.null(parent) && !inherits(parent, "condition")) { throw(new_error("Parent condition must be a condition object")) } if (isTRUE(cond$call)) { cond$call <- sys.call(-1) %||% sys.call() } else if (identical(cond$call, FALSE)) { cond$call <- NULL } # Eventually the nframe numbers will help us print a better trace # When a child condition is created, the child will use the parent # error object to make note of its own nframe. Here we copy that back # to the parent. if (is.null(cond$`_nframe`)) cond$`_nframe` <- sys.nframe() if (!is.null(parent)) { cond$parent <- parent cond$call <- cond$parent$`_childcall` cond$`_nframe` <- cond$parent$`_childframe` cond$`_ignore` <- cond$parent$`_childignore` } # We can set an option to always add the trace to the thrown # conditions. This is useful for example in context that always catch # errors, e.g. in testthat tests or knitr. This options is usually not # set and we signal the condition here always_trace <- isTRUE(getOption("rlib_error_always_trace")) if (!always_trace) signalCondition(cond) # If this is not an error, then we'll just return here. This allows # throwing interrupt conditions for example, with the same UI. if (! inherits(cond, "error")) return(invisible()) if (is.null(cond$`_pid`)) cond$`_pid` <- Sys.getpid() if (is.null(cond$`_timestamp`)) cond$`_timestamp` <- Sys.time() # If we get here that means that the condition was not caught by # an exiting handler. That means that we need to create a trace. # If there is a hand-constructed trace already in the error object, # then we'll just leave it there. if (is.null(cond$trace)) cond <- add_trace_back(cond) # Set up environment to store .Last.error, it will be just before # baseenv(), so it is almost as if it was in baseenv() itself, like # .Last.value. We save the print methos here as well, and then they # will be found automatically. if (! "org:r-lib" %in% search()) { do.call("attach", list(new.env(), pos = length(search()), name = "org:r-lib")) } env <- as.environment("org:r-lib") env$.Last.error <- cond env$.Last.error.trace <- cond$trace # If we always wanted a trace, then we signal the condition here if (always_trace) signalCondition(cond) # Top-level handler, this is intended for testing only for now, # and its design might change. if (!is.null(th <- getOption("rlib_error_handler")) && is.function(th)) { th(cond) } else { if (is_interactive()) { # In interactive mode, we print the error message through # conditionMessage() and also add a note about .Last.error.trace. # R will potentially truncate the error message, so we make sure # that the note is shown. Ideally we would print the error # ourselves, but then RStudio would not highlight it. max_msg_len <- as.integer(getOption("warning.length")) if (is.na(max_msg_len)) max_msg_len <- 1000 msg <- conditionMessage(cond) adv <- style_advice( "\nType .Last.error.trace to see where the error occurred" ) dots <- "\033[0m\n[...]" if (bytes(msg) + bytes(adv) + bytes(dots) + 5L> max_msg_len) { msg <- paste0( substr(msg, 1, max_msg_len - bytes(dots) - bytes(adv) - 5L), dots ) } cond$message <- paste0(msg, adv) } else { # In non-interactive mode, we print the error + the traceback # manually, to make sure that it won't be truncated by R's error # message length limit. cat("\n", file = stderr()) cat(style_error(gettext("Error: ")), file = stderr()) out <- capture_output(print(cond)) cat(out, file = stderr(), sep = "\n") out <- capture_output(print(cond$trace)) cat(out, file = stderr(), sep = "\n") # Turn off the regular error printing to avoid printing # the error twice. opts <- options(show.error.messages = FALSE) on.exit(options(opts), add = TRUE) } # Dropping the classes and adding "duplicate_condition" is a workaround # for the case when we have non-exiting handlers on throw()-n # conditions. These would get the condition twice, because stop() # will also signal it. If we drop the classes, then only handlers # on "condition" objects (i.e. all conditions) get duplicate signals. # This is probably quite rare, but for this rare case they can also # recognize the duplicates from the "duplicate_condition" extra class. class(cond) <- c("duplicate_condition", "condition") stop(cond) } } # -- rethrowing conditions -------------------------------------------- #' Catch and re-throw conditions #' #' See [rethrow()] for a simpler interface that handles `error` #' conditions automatically. #' #' @noRd #' @param expr Expression to evaluate. #' @param ... Condition handler specification, the same way as in #' [withCallingHandlers()]. You are supposed to call [throw()] from #' the error handler, with a new error object, setting the original #' error object as parent. See examples below. #' @param call Logical flag, whether to add the call to #' `catch_rethrow()` to the error. #' @examples #' f <- function() { #' ... #' err$catch_rethrow( #' ... code that potentially errors ..., #' error = function(e) { #' throw(new_error("This will be the child error"), parent = e) #' } #' ) #' } catch_rethrow <- function(expr, ..., call = TRUE) { realcall <- if (isTRUE(call)) sys.call(-1) %||% sys.call() realframe <- sys.nframe() parent <- parent.frame() cl <- match.call() cl[[1]] <- quote(withCallingHandlers) handlers <- list(...) for (h in names(handlers)) { cl[[h]] <- function(e) { # This will be NULL if the error is not throw()-n if (is.null(e$`_nframe`)) e$`_nframe` <- length(sys.calls()) e$`_childcall` <- realcall e$`_childframe` <- realframe # We drop after realframe, until the first withCallingHandlers wch <- find_call(sys.calls(), quote(withCallingHandlers)) if (!is.na(wch)) e$`_childignore` <- list(c(realframe + 1L, wch)) handlers[[h]](e) } } eval(cl, envir = parent) } find_call <- function(calls, call) { which(vapply( calls, function(x) length(x) >= 1 && identical(x[[1]], call), logical(1)))[1] } #' Catch and re-throw conditions #' #' `rethrow()` is similar to [catch_rethrow()], but it has a simpler #' interface. It catches conditions with class `error`, and re-throws #' `cond` instead, using the original condition as the parent. #' #' @noRd #' @param expr Expression to evaluate. #' @param ... Condition handler specification, the same way as in #' [withCallingHandlers()]. #' @param call Logical flag, whether to add the call to #' `rethrow()` to the error. rethrow <- function(expr, cond, call = TRUE) { realcall <- if (isTRUE(call)) sys.call(-1) %||% sys.call() realframe <- sys.nframe() withCallingHandlers( expr, error = function(e) { # This will be NULL if the error is not throw()-n if (is.null(e$`_nframe`)) e$`_nframe` <- length(sys.calls()) e$`_childcall` <- realcall e$`_childframe` <- realframe # We just ignore the withCallingHandlers call, and the tail e$`_childignore` <- list( c(realframe + 1L, realframe + 1L), c(e$`_nframe` + 1L, sys.nframe() + 1L)) throw(cond, parent = e) } ) } #' Version of .Call that throw()s errors #' #' It re-throws error from interpreted code. If the error had class #' `simpleError`, like all errors, thrown via `error()` in C do, it also #' adds the `c_error` class. #' #' @noRd #' @param .NAME Compiled function to call, see [.Call()]. #' @param ... Function arguments, see [.Call()]. #' @return Result of the call. rethrow_call <- function(.NAME, ...) { call <- sys.call() nframe <- sys.nframe() withCallingHandlers( # do.call to work around an R CMD check issue do.call(".Call", list(.NAME, ...)), error = function(e) { e$`_nframe` <- nframe e$call <- call if (inherits(e, "simpleError")) { class(e) <- c("c_error", "rlib_error", "error", "condition") } e$`_ignore` <- list(c(nframe + 1L, sys.nframe() + 1L)) throw(e) } ) } package_env <- topenv() #' Version of rethrow_call that supports cleancall #' #' This function is the same as [rethrow_call()], except that it #' uses cleancall's [.Call()] wrapper, to enable resource cleanup. #' See https://github.com/r-lib/cleancall#readme for more about #' resource cleanup. #' #' @noRd #' @param .NAME Compiled function to call, see [.Call()]. #' @param ... Function arguments, see [.Call()]. #' @return Result of the call. rethrow_call_with_cleanup <- function(.NAME, ...) { call <- sys.call() nframe <- sys.nframe() withCallingHandlers( package_env$call_with_cleanup(.NAME, ...), error = function(e) { e$`_nframe` <- nframe e$call <- call if (inherits(e, "simpleError")) { class(e) <- c("c_error", "rlib_error", "error", "condition") } e$`_ignore` <- list(c(nframe + 1L, sys.nframe() + 1L)) throw(e) } ) } # -- create traceback ------------------------------------------------- #' Create a traceback #' #' [throw()] calls this function automatically if an error is not caught, #' so there is currently not much use to call it directly. #' #' @param cond Condition to add the trace to #' #' @return A condition object, with the trace added. add_trace_back <- function(cond) { idx <- seq_len(sys.parent(1L)) frames <- sys.frames()[idx] parents <- sys.parents()[idx] calls <- as.list(sys.calls()[idx]) envs <- lapply(frames, env_label) topenvs <- lapply( seq_along(frames), function(i) env_label(topenvx(environment(sys.function(i))))) nframes <- if (!is.null(cond$`_nframe`)) cond$`_nframe` else sys.parent() messages <- list(conditionMessage(cond)) ignore <- cond$`_ignore` classes <- class(cond) pids <- rep(cond$`_pid` %||% Sys.getpid(), length(calls)) if (is.null(cond$parent)) { # Nothing to do, no parent } else if (is.null(cond$parent$trace) || !inherits(cond$parent, "rlib_error")) { # If the parent does not have a trace, that means that it is using # the same trace as us. We ignore traces from non-r-lib errors. # E.g. rlang errors have a trace, but we do not use that. parent <- cond while (!is.null(parent <- parent$parent)) { nframes <- c(nframes, parent$`_nframe`) messages <- c(messages, list(conditionMessage(parent))) ignore <- c(ignore, parent$`_ignore`) } } else { # If it has a trace, that means that it is coming from another # process or top level evaluation. In this case we'll merge the two # traces. pt <- cond$parent$trace parents <- c(parents, pt$parents + length(calls)) nframes <- c(nframes, pt$nframes + length(calls)) ignore <- c(ignore, lapply(pt$ignore, function(x) x + length(calls))) envs <- c(envs, pt$envs) topenvs <- c(topenvs, pt$topenvs) calls <- c(calls, pt$calls) messages <- c(messages, pt$messages) pids <- c(pids, pt$pids) } cond$trace <- new_trace( calls, parents, envs, topenvs, nframes, messages, ignore, classes, pids) cond } topenvx <- function(x) { topenv(x, matchThisEnv = err_env) } new_trace <- function (calls, parents, envs, topenvs, nframes, messages, ignore, classes, pids) { indices <- seq_along(calls) structure( list(calls = calls, parents = parents, envs = envs, topenvs = topenvs, indices = indices, nframes = nframes, messages = messages, ignore = ignore, classes = classes, pids = pids), class = "rlib_trace") } env_label <- function(env) { nm <- env_name(env) if (nzchar(nm)) { nm } else { env_address(env) } } env_address <- function(env) { class(env) <- "environment" sub("^.*(0x[0-9a-f]+)>$", "\\1", format(env), perl = TRUE) } env_name <- function(env) { if (identical(env, err_env)) { return("") } if (identical(env, globalenv())) { return("global") } if (identical(env, baseenv())) { return("namespace:base") } if (identical(env, emptyenv())) { return("empty") } nm <- environmentName(env) if (isNamespace(env)) { return(paste0("namespace:", nm)) } nm } # -- printing --------------------------------------------------------- print_this <- function(x, ...) { msg <- conditionMessage(x) call <- conditionCall(x) cl <- class(x)[1L] if (!is.null(call)) { cat("<", cl, " in ", format_call(call), ":\n ", msg, ">\n", sep = "") } else { cat("<", cl, ": ", msg, ">\n", sep = "") } print_srcref(x$call) if (!identical(x$`_pid`, Sys.getpid())) { cat(" in process", x$`_pid`, "\n") } invisible(x) } print_parents <- function(x, ...) { if (!is.null(x$parent)) { cat("-->\n") print(x$parent) } invisible(x) } print_rlib_error <- function(x, ...) { print_this(x, ...) print_parents(x, ...) } print_rlib_trace <- function(x, ...) { cl <- paste0(" Stack trace:") cat(sep = "", "\n", style_trace_title(cl), "\n\n") calls <- map2(x$calls, x$topenv, namespace_calls) callstr <- vapply(calls, format_call_src, character(1)) callstr[x$nframes] <- paste0(callstr[x$nframes], "\n", style_error_msg(x$messages), "\n") callstr <- enumerate(callstr) # Ignore what we were told to ignore ign <- integer() for (iv in x$ignore) { if (iv[2] == Inf) iv[2] <- length(callstr) ign <- c(ign, iv[1]:iv[2]) } # Plus always ignore the tail. This is not always good for # catch_rethrow(), but should be good otherwise last_err_frame <- x$nframes[length(x$nframes)] if (!is.na(last_err_frame) && last_err_frame < length(callstr)) { ign <- c(ign, (last_err_frame+1):length(callstr)) } ign <- unique(ign) if (length(ign)) callstr <- callstr[-ign] # Add markers for subprocesses if (length(unique(x$pids)) >= 2) { pids <- x$pids[-ign] pid_add <- which(!duplicated(pids)) pid_str <- style_process(paste0("Process ", pids[pid_add], ":")) callstr[pid_add] <- paste0(" ", pid_str, "\n", callstr[pid_add]) } cat(callstr, sep = "\n") invisible(x) } capture_output <- function(expr) { if (has_cli()) { opts <- options(cli.num_colors = cli::num_ansi_colors()) on.exit(options(opts), add = TRUE) } out <- NULL file <- textConnection("out", "w", local = TRUE) sink(file) on.exit(sink(NULL), add = TRUE) expr if (is.null(out)) invisible(NULL) else out } is_interactive <- function() { opt <- getOption("rlib_interactive") if (isTRUE(opt)) { TRUE } else if (identical(opt, FALSE)) { FALSE } else if (tolower(getOption("knitr.in.progress", "false")) == "true") { FALSE } else if (tolower(getOption("rstudio.notebook.executing", "false")) == "true") { FALSE } else if (identical(Sys.getenv("TESTTHAT"), "true")) { FALSE } else { interactive() } } onload_hook <- function() { reg_env <- Sys.getenv("R_LIB_ERROR_REGISTER_PRINT_METHODS", "TRUE") if (tolower(reg_env) != "false") { registerS3method("print", "rlib_error", print_rlib_error, baseenv()) registerS3method("print", "rlib_trace", print_rlib_trace, baseenv()) } } namespace_calls <- function(call, env) { if (length(call) < 1) return(call) if (typeof(call[[1]]) != "symbol") return(call) pkg <- strsplit(env, "^namespace:")[[1]][2] if (is.na(pkg)) return(call) call[[1]] <- substitute(p:::f, list(p = as.symbol(pkg), f = call[[1]])) call } print_srcref <- function(call) { src <- format_srcref(call) if (length(src)) cat(sep = "", " ", src, "\n") } `%||%` <- function(l, r) if (is.null(l)) r else l format_srcref <- function(call) { if (is.null(call)) return(NULL) file <- utils::getSrcFilename(call) if (!length(file)) return(NULL) dir <- utils::getSrcDirectory(call) if (length(dir) && nzchar(dir) && nzchar(file)) { srcfile <- attr(utils::getSrcref(call), "srcfile") if (isTRUE(srcfile$isFile)) { file <- file.path(dir, file) } else { file <- file.path("R", file) } } else { file <- "??" } line <- utils::getSrcLocation(call) %||% "??" col <- utils::getSrcLocation(call, which = "column") %||% "??" style_srcref(paste0(file, ":", line, ":", col)) } format_call <- function(call) { width <- getOption("width") str <- format(call) callstr <- if (length(str) > 1 || nchar(str[1]) > width) { paste0(substr(str[1], 1, width - 5), " ...") } else { str[1] } style_call(callstr) } format_call_src <- function(call) { callstr <- format_call(call) src <- format_srcref(call) if (length(src)) callstr <- paste0(callstr, "\n ", src) callstr } enumerate <- function(x) { paste0(style_numbers(paste0(" ", seq_along(x), ". ")), x) } map2 <- function (.x, .y, .f, ...) { mapply(.f, .x, .y, MoreArgs = list(...), SIMPLIFY = FALSE, USE.NAMES = FALSE) } bytes <- function(x) { nchar(x, type = "bytes") } # -- printing, styles ------------------------------------------------- has_cli <- function() "cli" %in% loadedNamespaces() style_numbers <- function(x) { if (has_cli()) cli::col_silver(x) else x } style_advice <- function(x) { if (has_cli()) cli::col_silver(x) else x } style_srcref <- function(x) { if (has_cli()) cli::style_italic(cli::col_cyan(x)) } style_error <- function(x) { if (has_cli()) cli::style_bold(cli::col_red(x)) else x } style_error_msg <- function(x) { sx <- paste0("\n x ", x, " ") style_error(sx) } style_trace_title <- function(x) { x } style_process <- function(x) { if (has_cli()) cli::style_bold(x) else x } style_call <- function(x) { if (!has_cli()) return(x) call <- sub("^([^(]+)[(].*$", "\\1", x) rest <- sub("^[^(]+([(].*)$", "\\1", x) if (call == x || rest == x) return(x) paste0(cli::col_yellow(call), rest) } err_env <- environment() parent.env(err_env) <- baseenv() structure( list( .internal = err_env, new_cond = new_cond, new_error = new_error, throw = throw, rethrow = rethrow, catch_rethrow = catch_rethrow, rethrow_call = rethrow_call, add_trace_back = add_trace_back, onload_hook = onload_hook, print_this = print_this, print_parents = print_parents ), class = c("standalone_errors", "standalone")) }) # These are optional, and feel free to remove them if you prefer to # call them through the `err` object. new_cond <- err$new_cond new_error <- err$new_error throw <- err$throw rethrow <- err$rethrow rethrow_call <- err$rethrow_call rethrow_call_with_cleanup <- err$.internal$rethrow_call_with_cleanup gh/R/gh_response.R0000644000176200001440000000464313710670253013523 0ustar liggesusersgh_process_response <- function(response) { stopifnot(inherits(response, "response")) if (status_code(response) >= 300) { gh_error(response) } content_type <- http_type(response) gh_media_type <- headers(response)[["x-github-media-type"]] is_raw <- content_type == "application/octet-stream" || isTRUE(grepl("param=raw$", gh_media_type, ignore.case = TRUE)) is_ondisk <- inherits(response$content, "path") if (is_ondisk) { res <- response$content } else if (grepl("^application/json", content_type, ignore.case = TRUE)) { res <- fromJSON(content(response, as = "text"), simplifyVector = FALSE) } else if (is_raw) { res <- content(response, as = "raw") } else if (content_type == "application/octet-stream" && length(content(response, as = "raw")) == 0) { res <- NULL } else { if (grepl("^text/html", content_type, ignore.case = TRUE)) { warning("Response came back as html :(", call. = FALSE) } res <- list(message = content(response, as = "text")) } attr(res, "method") <- response$request$method attr(res, "response") <- headers(response) attr(res, ".send_headers") <- response$request$headers if (is_ondisk) { class(res) <- c("gh_response", "path") } else if (is_raw) { class(res) <- c("gh_response", "raw") } else { class(res) <- c("gh_response", "list") } res } # https://docs.github.com/v3/#client-errors gh_error <- function(response, call = sys.call(-1)) { heads <- headers(response) res <- content(response) status <- status_code(response) msg <- c( "", paste0("GitHub API error (", status, "): ", heads$status), paste0("Message: ", res$message) ) doc_url <- res$documentation_url if (!is.null(doc_url)) { msg <- append(msg, paste0("Read more at ", doc_url)) } if (status == 404) { msg <- append(msg, c("", paste0("URL not found: ", response$request$url))) } errors <- res$errors if (!is.null(errors)) { errors <- as.data.frame(do.call(rbind, errors)) nms <- c("resource", "field", "code", "message") nms <- nms[nms %in% names(errors)] msg <- append( msg, c("", "Errors:", capture.output(print(errors[nms], row.names = FALSE)) ) ) } cond <- structure(list( call = call, message = paste0(msg, collapse = "\n") ), class = c( "github_error", paste0("http_error_", status), "error", "condition" )) throw(cond) } gh/R/gh_gql.R0000644000176200001440000000132213760151231012432 0ustar liggesusers#' A simple interface for the GitHub GraphQL API v4. #' #' See more about the GraphQL API here: #' #' #' Note: pagination and the `.limit` argument does not work currently, #' as pagination in the GraphQL API is different from the v3 API. #' If you need pagination with GraphQL, you'll need to do that manually. #' #' @inheritParams gh #' @param query The GraphQL query, as a string. #' @export #' @seealso [gh()] for the GitHub v3 API. #' @examplesIf FALSE #' gh_gql("query { viewer { login }}") gh_gql <- function(query, ...) { if (".limit" %in% names(list(...))) { stop("`.limit` does not work with the GraphQL API") } gh(endpoint = "POST /graphql", query = query, ...) } gh/R/gh_token.R0000644000176200001440000000747614031420134013000 0ustar liggesusers#' Return the local user's GitHub Personal Access Token (PAT) #' #' @description #' If gh can find a personal access token (PAT) via `gh_token()`, it includes #' the PAT in its requests. Some requests succeed without a PAT, but many #' require a PAT to prove the request is authorized by a specific GitHub user. A #' PAT also helps with rate limiting. If your gh use is more than casual, you #' want a PAT. #' #' gh calls [gitcreds::gitcreds_get()] with the `api_url`, which checks session #' environment variables and then the local Git credential store for a PAT #' appropriate to the `api_url`. Therefore, if you have previously used a PAT #' with, e.g., command line Git, gh may retrieve and re-use it. You can call #' [gitcreds::gitcreds_get()] directly, yourself, if you want to see what is #' found for a specific URL. If no matching PAT is found, #' [gitcreds::gitcreds_get()] errors, whereas `gh_token()` does not and, #' instead, returns `""`. #' #' See GitHub's documentation on [Creating a personal access #' token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token), #' or use `usethis::create_github_token()` for a guided experience, including #' pre-selection of recommended scopes. Once you have a PAT, you can use #' [gitcreds::gitcreds_set()] to add it to the Git credential store. From that #' point on, gh (via [gitcreds::gitcreds_get()]) should be able to find it #' without further effort on your part. #' #' @param api_url GitHub API URL. Defaults to the `GITHUB_API_URL` environment #' variable, if set, and otherwise to . #' #' @return A string of characters, if a PAT is found, or the empty #' string, otherwise. For convenience, the return value has an S3 class in #' order to ensure that simple printing strategies don't reveal the entire #' PAT. #' #' @export #' #' @examples #' \dontrun{ #' gh_token() #' #' format(gh_token()) #' #' str(gh_token()) #' } gh_token <- function(api_url = NULL) { api_url <- api_url %||% default_api_url() stopifnot(is.character(api_url), length(api_url) == 1) token <- tryCatch( gitcreds::gitcreds_get(get_hosturl(api_url)), error = function(e) NULL ) gh_pat(token$password %||% "") } gh_auth <- function(token) { if (isTRUE(token != "")) { if(any(grepl("\\W", token))) { warning("Token contains whitespace characters") } c("Authorization" = paste("token", trim_ws(token))) } else { character() } } # gh_pat class: exists in order have a print method that hides info ---- new_gh_pat <- function(x) { if (is.character(x) && length(x) == 1) { structure(x, class = "gh_pat") } else { throw(new_error("A GitHub PAT must be a string", call. = FALSE)) } } # validates PAT only in a very narrow, technical, and local sense validate_gh_pat <- function(x) { stopifnot(inherits(x, "gh_pat")) if (x == "" || # https://github.blog/changelog/2021-03-04-authentication-token-format-updates/ grepl("^gh[pousr]_[A-Za-z0-9_]{36,251}$", x) || grepl("[[:xdigit:]]{40}", x)) { x } else { throw(new_error( "GitHub PAT must have one of these forms:", "\n * 40 hexadecimal digits (older PATs)", "\n * A 'ghp_' prefix followed by 36 to 251 more characters (newer PATs)", call. = FALSE )) } } gh_pat <- function(x) { validate_gh_pat(new_gh_pat(x)) } #' @export format.gh_pat <- function(x, ...) { if (x == "") { "" } else { obfuscate(x) } } #' @export print.gh_pat <- function(x, ...) { cat(format(x), sep = "\n") invisible(x) } #' @export str.gh_pat <- function(object, ...) { cat(paste0(" ", format(object), "\n", collapse = "")) invisible() } obfuscate <- function(x, first = 4, last = 4) { paste0( substr(x, start = 1, stop = first), "...", substr(x, start = nchar(x) - last + 1, stop = nchar(x)) ) } gh/R/pagination.R0000644000176200001440000000644014042725454013340 0ustar liggesusers extract_link <- function(gh_response, link) { headers <- attr(gh_response, "response") links <- headers$link if (is.null(links)) { return(NA_character_) } links <- trim_ws(strsplit(links, ",")[[1]]) link_list <- lapply(links, function(x) { x <- trim_ws(strsplit(x, ";")[[1]]) name <- sub("^.*\"(.*)\".*$", "\\1", x[2]) value <- sub("^<(.*)>$", "\\1", x[1]) c(name, value) }) link_list <- structure( vapply(link_list, "[", "", 2), names = vapply(link_list, "[", "", 1) ) if (link %in% names(link_list)) { link_list[[link]] } else { NA_character_ } } gh_has <- function(gh_response, link) { url <- extract_link(gh_response, link) !is.na(url) } gh_has_next <- function(gh_response) { gh_has(gh_response, "next") } gh_link_request <- function(gh_response, link) { stopifnot(inherits(gh_response, "gh_response")) url <- extract_link(gh_response, link) if (is.na(url)) throw(new_error("No ", link, " page")) list(method = attr(gh_response, "method"), url = url, headers = attr(gh_response, ".send_headers")) } gh_link <- function(gh_response, link) { req <- gh_link_request(gh_response, link) raw <- gh_make_request(req) gh_process_response(raw) } gh_extract_pages <- function(gh_response) { last <- extract_link(gh_response, "last") if (grepl("&page=[0-9]+$", last)) { as.integer(sub("^.*page=([0-9]+)$", "\\1", last)) } } #' Get the next, previous, first or last page of results #' #' @details #' Note that these are not always defined. E.g. if the first #' page was queried (the default), then there are no first and previous #' pages defined. If there is no next page, then there is no #' next page defined, etc. #' #' If the requested page does not exist, an error is thrown. #' #' @param gh_response An object returned by a [gh()] call. #' @return Answer from the API. #' #' @seealso The `.limit` argument to [gh()] supports fetching more than #' one page. #' #' @name gh_next #' @export #' @examplesIf identical(Sys.getenv("IN_PKGDOWN"), "true") #' x <- gh("/users") #' vapply(x, "[[", character(1), "login") #' x2 <- gh_next(x) #' vapply(x2, "[[", character(1), "login") gh_next <- function(gh_response) gh_link(gh_response, "next") #' @name gh_next #' @export gh_prev <- function(gh_response) gh_link(gh_response, "prev") #' @name gh_next #' @export gh_first <- function(gh_response) gh_link(gh_response, "first") #' @name gh_next #' @export gh_last <- function(gh_response) gh_link(gh_response, "last") make_progress_bar <- function(gh_request) { state <- new.env(parent = emptyenv()) state$pageno <- 0L state$got <- 0L state$status <- NULL state } update_progress_bar <- function(state, gh_response) { state$pageno <- state$pageno + 1L state$got <- gh_response_length(gh_response) state$pages <- gh_extract_pages(gh_response) %||% state$pages if (is.null(state$status)) { state$status <- cli_status( "{.alert-info Running gh query}", .envir = parent.frame() ) } total <- NULL if (!is.null(state$pages)) { est <- state$pages * (state$got / state$pageno) if (est >= state$got) total <- est } cli_status_update( state$status, c("{.alert-info Running gh query, got {state$got} record{?s}}", if (!is.null(total)) " of about {total}") ) invisible(state) } gh/R/gh-package.R0000644000176200001440000000071414031420127013157 0ustar liggesusers#' @keywords internal #' @aliases gh-package "_PACKAGE" # The following block is used by usethis to automatically manage # roxygen namespace tags. Modify with care! ## usethis namespace: start #' @importFrom httr content add_headers headers status_code http_type GET POST #' PATCH PUT DELETE #' @importFrom jsonlite fromJSON toJSON #' @importFrom utils URLencode capture.output #' @importFrom cli cli_status cli_status_update ## usethis namespace: end NULL gh/R/gh.R0000644000176200001440000002361714042726207011607 0ustar liggesusers#' Query the GitHub API #' #' This is an extremely minimal client. You need to know the API #' to be able to use this client. All this function does is: #' * Try to substitute each listed parameter into `endpoint`, using the #' `{parameter}` notation. #' * If a GET request (the default), then add all other listed parameters #' as query parameters. #' * If not a GET request, then send the other parameters in the request #' body, as JSON. #' * Convert the response to an R list using [jsonlite::fromJSON()]. #' #' @param endpoint GitHub API endpoint. Must be one of the following forms: #' * `METHOD path`, e.g. `GET /rate_limit`, #' * `path`, e.g. `/rate_limit`, #' * `METHOD url`, e.g. `GET https://api.github.com/rate_limit`, #' * `url`, e.g. `https://api.github.com/rate_limit`. #' #' If the method is not supplied, will use `.method`, which defaults #' to `"GET"`. #' @param ... Name-value pairs giving API parameters. Will be matched into #' `endpoint` placeholders, sent as query parameters in GET requests, and as a #' JSON body of POST requests. If there is only one unnamed parameter, and it #' is a raw vector, then it will not be JSON encoded, but sent as raw data, as #' is. This can be used for example to add assets to releases. Named `NULL` #' values are silently dropped. For GET requests, named `NA` values trigger an #' error. For other methods, named `NA` values are included in the body of the #' request, as JSON `null`. #' @param per_page Number of items to return per page. If omitted, #' will be substituted by `max(.limit, 100)` if `.limit` is set, #' otherwise determined by the API (never greater than 100). #' @param .destfile Path to write response to disk. If `NULL` (default), #' response will be processed and returned as an object. If path is given, #' response will be written to disk in the form sent. #' @param .overwrite If `.destfile` is provided, whether to overwrite an #' existing file. Defaults to `FALSE`. #' @param .token Authentication token. Defaults to `GITHUB_PAT` or #' `GITHUB_TOKEN` environment variables, in this order if any is set. #' See [gh_token()] if you need more flexibility, e.g. different tokens #' for different GitHub Enterprise deployments. #' @param .api_url Github API url (default: ). Used #' if `endpoint` just contains a path. Defaults to `GITHUB_API_URL` #' environment variable if set. #' @param .method HTTP method to use if not explicitly supplied in the #' `endpoint`. #' @param .limit Number of records to return. This can be used #' instead of manual pagination. By default it is `NULL`, #' which means that the defaults of the GitHub API are used. #' You can set it to a number to request more (or less) #' records, and also to `Inf` to request all records. #' Note, that if you request many records, then multiple GitHub #' API calls are used to get them, and this can take a potentially #' long time. #' @param .accept The value of the `Accept` HTTP header. Defaults to #' `"application/vnd.github.v3+json"` . If `Accept` is given in #' `.send_headers`, then that will be used. This parameter can be used to #' provide a custom media type, in order to access a preview feature of #' the API. #' @param .send_headers Named character vector of header field values #' (except `Authorization`, which is handled via `.token`). This can be #' used to override or augment the default `User-Agent` header: #' `"https://github.com/r-lib/gh"`. #' @param .progress Whether to show a progress indicator for calls that #' need more than one HTTP request. #' @param .params Additional list of parameters to append to `...`. #' It is easier to use this than `...` if you have your parameters in #' a list already. #' #' @return Answer from the API as a `gh_response` object, which is also a #' `list`. Failed requests will generate an R error. Requests that #' generate a raw response will return a raw vector. #' #' @export #' @seealso [gh_gql()] if you want to use the GitHub GraphQL API, #' [gh_whoami()] for details on GitHub API token management. #' @examplesIf identical(Sys.getenv("IN_PKGDOWN"), "true") #' ## Repositories of a user, these are equivalent #' gh("/users/hadley/repos") #' gh("/users/{username}/repos", username = "hadley") #' #' ## Starred repositories of a user #' gh("/users/hadley/starred") #' gh("/users/{username}/starred", username = "hadley") #' #' @examplesIf FALSE #' ## Create a repository, needs a token in GITHUB_PAT (or GITHUB_TOKEN) #' ## environment variable #' gh("POST /user/repos", name = "foobar") #' #' @examplesIf identical(Sys.getenv("IN_PKGDOWN"), "true") #' ## Issues of a repository #' gh("/repos/hadley/dplyr/issues") #' gh("/repos/{owner}/{repo}/issues", owner = "hadley", repo = "dplyr") #' #' ## Automatic pagination #' users <- gh("/users", .limit = 50) #' length(users) #' #' @examplesIf FALSE #' ## Access developer preview of Licenses API (in preview as of 2015-09-24) #' gh("/licenses") # used to error code 415 #' gh("/licenses", .accept = "application/vnd.github.drax-preview+json") #' #' @examplesIf FALSE #' ## Access Github Enterprise API #' ## Use GITHUB_API_URL environment variable to change the default. #' gh("/user/repos", type = "public", .api_url = "https://github.foobar.edu/api/v3") #' #' @examplesIf FALSE #' ## Use I() to force body part to be sent as an array, even if length 1 #' ## This works whether assignees has length 1 or > 1 #' assignees <- "gh_user" #' assignees <- c("gh_user1", "gh_user2") #' gh("PATCH /repos/OWNER/REPO/issues/1", assignees = I(assignees)) #' #' @examplesIf FALSE #' ## There are two ways to send JSON data. One is that you supply one or #' ## more objects that will be converted to JSON automatically via #' ## jsonlite::toJSON(). In this case sometimes you need to use #' ## jsonlite::unbox() because fromJSON() creates lists from scalar vectors #' ## by default. The Content-Type header is automatically added in this #' ## case. For example this request turns on GitHub Pages, using this #' ## API: https://docs.github.com/v3/repos/pages/#enable-a-pages-site #' #' gh::gh( #' "POST /repos/{owner}/{repo}/pages", #' owner = "gaborcsardi", #' repo = "playground", #' source = list( #' branch = jsonlite::unbox("master"), #' path = jsonlite::unbox("/docs") #' ), #' .send_headers = c(Accept = "application/vnd.github.switcheroo-preview+json") #' ) #' #' ## The second way is to handle the JSON encoding manually, and supply it #' ## as a raw vector in an unnamed argument, and also a Content-Type header: #' #' body <- '{ "source": { "branch": "master", "path": "/docs" } }' #' gh::gh( #' "POST /repos/{owner}/{repo}/pages", #' owner = "gaborcsardi", #' repo = "playground", #' charToRaw(body), #' .send_headers = c( #' Accept = "application/vnd.github.switcheroo-preview+json", #' "Content-Type" = "application/json" #' ) #' ) gh <- function(endpoint, ..., per_page = NULL, .token = NULL, .destfile = NULL, .overwrite = FALSE, .api_url = NULL, .method = "GET", .limit = NULL, .accept = "application/vnd.github.v3+json", .send_headers = NULL, .progress = TRUE, .params = list()) { params <- c(list(...), .params) params <- drop_named_nulls(params) if (is.null(per_page)) { if (!is.null(.limit)) { per_page <- max(min(.limit, 100), 1) } } if (!is.null(per_page)) { params <- c(params, list(per_page = per_page)) } req <- gh_build_request(endpoint = endpoint, params = params, token = .token, destfile = .destfile, overwrite = .overwrite, accept = .accept, send_headers = .send_headers, api_url = .api_url, method = .method) if (req$method == "GET") check_named_nas(params) if (.progress) prbr <- make_progress_bar(req) raw <- gh_make_request(req) res <- gh_process_response(raw) len <- gh_response_length(res) while (!is.null(.limit) && len < .limit && gh_has_next(res)) { if (.progress) update_progress_bar(prbr, res) res2 <- gh_next(res) if (!is.null(names(res2)) && identical(names(res), names(res2))) { res3 <- mapply( # Handle named array case function(x, y, n) { # e.g. GET /search/repositories z <- c(x, y) atm <- is.atomic(z) if (atm && n %in% c("total_count", "incomplete_results")) { y } else if (atm) { unique(z) } else { z } }, res, res2, names(res), SIMPLIFY = FALSE ) } else { # Handle unnamed array case res3 <- c(res, res2) # e.g. GET /orgs/:org/invitations } len <- len + gh_response_length(res2) attributes(res3) <- attributes(res2) res <- res3 } # We only subset for a non-named response. if (! is.null(.limit) && len > .limit && ! "total_count" %in% names(res) && length(res) == len) { res_attr <- attributes(res) res <- res[seq_len(.limit)] attributes(res) <- res_attr } res } gh_response_length <- function(res) { if (!is.null(names(res)) && length(res) > 1 && names(res)[1] == "total_count") { # Ignore total_count, incomplete_results, repository_selection # and take the first list element to get the length lst <- vapply(res, is.list, logical(1)) nm <- setdiff( names(res), c("total_count", "incomplete_results", "repository_selection") ) tgt <- which(lst[nm])[1] if (is.na(tgt)) length(res) else length(res[[ nm[tgt] ]]) } else { length(res) } } gh_make_request <- function(x) { method_fun <- list("GET" = GET, "POST" = POST, "PATCH" = PATCH, "PUT" = PUT, "DELETE" = DELETE)[[x$method]] if (is.null(method_fun)) throw(new_error("Unknown HTTP verb")) raw <- do.call(method_fun, compact(list(url = x$url, query = x$query, body = x$body, add_headers(x$headers), x$dest))) raw } gh/NEWS.md0000644000176200001440000000546114042736000011750 0ustar liggesusers # gh 1.3.0 * gh now shows the correct number of records in its progress bar when paginating (#147). * New `.params` argument in `gh()` to make it easier to pass parameters to it programmatically (#140). # gh 1.2.1 * Token validation accounts for the new format [announced 2021-03-04 ](https://github.blog/changelog/2021-03-04-authentication-token-format-updates/) and implemented on 2021-04-01 (#148, @fmichonneau). # gh 1.2.0 * `gh_gql()` now passes all arguments to `gh()` (#124). * gh now handles responses from pagination better, and tries to properly merge them (#136, @rundel). * gh can retrieve a PAT from the Git credential store, where the lookup is based on the targeted API URL. This now uses the gitcreds package. The environment variables consulted for URL-specific GitHub PATs have changed. - For "https://api.github.com": `GITHUB_PAT_GITHUB_COM` now, instead of `GITHUB_PAT_API_GITHUB_COM` - For "https://github.acme.com/api/v3": `GITHUB_PAT_GITHUB_ACME_COM` now, instead of `GITHUB_PAT_GITHUB_ACME_COM_API_V3` See the documentation of the gitcreds package for details. * The keyring package is no longer used, in favor of the Git credential store. * The documentation for the GitHub REST API has moved to and endpoints are now documented using the URI template style of [RFC 6570](https://tools.ietf.org/html/rfc6570): - Old: `GET /repos/:owner/:repo/issues` - New: `GET /repos/{owner}/{repo}/issues` gh accepts and prioritizes the new style. However, it still does parameter substitution for the old style. * Fixed an error that occurred when calling `gh()` with `.progress = FALSE` (@gadenbuie, #115). * `gh()` accepts named `NA` parameters that are destined for the request body (#139). # gh 1.1.0 * Raw responses from GitHub are now returned as raw vector. * Responses may be written to disk by providing a path in the `.destfile` argument. * gh now sets `.Last.error` to the error object after an uncaught error, and `.Last.error.trace` to the stack trace of the error. * `gh()` now silently drops named `NULL` parameters, and throws an error for named `NA` parameters (#21, #84). * `gh()` now returns better values for empty responses, typically empty lists or dictionaries (#66). * `gh()` now has an `.accept` argument to make it easier to set the `Accept` HTTP header (#91). * New `gh_gql()` function to make it easier to work with the GitHub GraphQL API. * gh now supports separate personal access tokens for GitHub Enterprise sites. See `?gh_token` for details. * gh now supports storing your GitHub personal access tokens (PAT) in the system keyring, via the keyring package. See `?gh_token` for details. * `gh()` can now POST raw data, which allows adding assets to releases (#56). # gh 1.0.1 First public release. gh/MD50000644000176200001440000000504114042757216011170 0ustar liggesusers26cf9bd54b94df5b078c258215b1a558 *DESCRIPTION a7efc96e6c1157db81ba3615dc5895ae *LICENSE e3c5cfc48d8015ca33f05e5f35c55c8c *NAMESPACE a401aede13fc1513363caa19cb66a796 *NEWS.md 70358ad7585159538a667fb4749f87ac *R/errors.R 831c04797d9cc2500c0a5b219e49e3ce *R/gh-package.R 3e50c81c26784969b44e62e180561dc7 *R/gh.R 652005d500a5ce88d1d29678e12c2644 *R/gh_gql.R 2cefa483d08511d4df9429f06a6b7f2b *R/gh_rate_limit.R 2d681379c6ef5f56c60e3498ec732f7d *R/gh_request.R 9b9f3c81a1b54cad83bc955163b24e93 *R/gh_response.R b5c7bfb0bc9b273308734083de78912f *R/gh_token.R 87b3e0259f7e83046456f4e5e8fa3665 *R/gh_whoami.R fe9a5d8826ad1065d035d0cf85fd40dc *R/git.R d51d293c7cc1ac9efb6452a084e8c7b2 *R/pagination.R 47a7af563a2a2188a237795c7ce91e32 *R/print.R e68de00d13fd3f09e8a83446da5955fb *R/utils.R e2ba75248b1cf6e4edccf0b4ba34a3a6 *README.md 7c1213b98c1417a51f4bbdee4e9c1734 *build/vignette.rds be7cefb03851f46467689e4ca2c0e1f6 *inst/WORDLIST d2b7f7e796f15c722631e18a4a49f9c1 *inst/doc/managing-personal-access-tokens.R 5f7f257d4925f3689b566d2806cb2fe4 *inst/doc/managing-personal-access-tokens.Rmd 15e5764a2bbfa6a9fca87600cba5018e *inst/doc/managing-personal-access-tokens.html 13fc2ab0028291b7c55f802378887a93 *man/gh-package.Rd 291a9c108ad0da66c6eb349c13d6734e *man/gh.Rd afdb30f56dec2b660bd393a84e8e2741 *man/gh_gql.Rd e5358b38e37815d75c80ead3edadc2eb *man/gh_next.Rd 6ce166bb4dde4d611de0dde11c67fb43 *man/gh_rate_limit.Rd cef95193c84ce3ec3e7cc120d2000c5b *man/gh_token.Rd 8dc288d4beadb347828028716153ed0c *man/gh_tree_remote.Rd c24ce8769ef9868896e56326328fce6d *man/gh_whoami.Rd 016da8202cc86b463da849981d5d309a *man/print.gh_response.Rd 2896c437c9e0aff80626ab177279c7c0 *tests/testthat.R 0c7565a4c6ea46214bd32653aaa03318 *tests/testthat/helper-offline.R 883c9753974b327487b52bb395f377a2 *tests/testthat/helper.R 05626815bf0b3ffd99c98cf19d2fb277 *tests/testthat/test-errors.R fc7e3ff3d0f0b3c1050097f4bb4da53d *tests/testthat/test-gh.R 15c433e59b00bdd514face8a3867e4a2 *tests/testthat/test-gh_rate_limit.R baee0940a7c4da7f99128ebc4046b369 *tests/testthat/test-gh_request.R 0e83da6552e15a9a1ca0e482c910de60 *tests/testthat/test-gh_token.R e85d31f5e70e51ec636decc4dc348232 *tests/testthat/test-gh_whoami.R 0493bd4eca08298a5e4384a6a21121b7 *tests/testthat/test-git.R 6e04bde3b334ed163536202b73134c76 *tests/testthat/test-mock-repos.R 073fcc251255873a59d1918d12c7dd79 *tests/testthat/test-old-templates.R 942cfab4e734a9cc61513f87ad2c2f8b *tests/testthat/test-spelling.R 6120c3eb0df0572741705ea51ec5e72e *tests/testthat/test-utils.R 5f7f257d4925f3689b566d2806cb2fe4 *vignettes/managing-personal-access-tokens.Rmd gh/inst/0000755000176200001440000000000014042737101011624 5ustar liggesusersgh/inst/doc/0000755000176200001440000000000014042737101012371 5ustar liggesusersgh/inst/doc/managing-personal-access-tokens.Rmd0000644000176200001440000001602414031420134021173 0ustar liggesusers--- title: "Managing Personal Access Tokens" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Managing Personal Access Tokens} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(gh) ``` gh generally sends a Personal Access Token (PAT) with its requests. Some endpoints of the GitHub API can be accessed without authenticating yourself. But once your API use becomes more frequent, you will want a PAT to prevent problems with rate limits and to access all possible endpoints. This article describes how to store your PAT, so that gh can find it (automatically, in most cases). The function gh uses for this is `gh_token()`. More resources on PAT management: * GitHub documentation on [Creating a personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) * In the [usethis package](https://usethis.r-lib.org): - Vignette: [Managing Git(Hub) Credentials](https://usethis.r-lib.org/articles/articles/git-credentials.html) - `usethis::gh_token_help()` and `usethis::git_sitrep()` help you check if a PAT is discoverable and has suitable scopes - `usethis::create_github_token()` guides you through the process of getting a new PAT * In the [gitcreds package](https://gitcreds.r-lib.org/): - `gitcreds::gitcreds_set()` helps you explicitly put your PAT into the Git credential store ## PAT and host `gh::gh()` allows the user to provide a PAT via the `.token` argument and to specify a host other than "github.com" via the `.api_url` argument. (Some companies and universities run their own instance of GitHub Enterprise.) ```{r, eval = FALSE} gh(endpoint, ..., .token = NULL, ..., .api_url = NULL, ...) ``` However, it's annoying to always provide your PAT or host and it's unsafe for your PAT to appear explicitly in your R code. It's important to make it *possible* for the user to provide the PAT and/or API URL directly, but it should rarely be necessary. `gh::gh()` is designed to play well with more secure, less fiddly methods for expressing what you want. How are `.api_url` and `.token` determined when the user does not provide them? 1. `.api_url` defaults to the value of the `GITHUB_API_URL` environment variable and, if that is unset, falls back to `"https://api.github.com"`. This is always done before worrying about the PAT. 1. The PAT is obtained via a call to `gh_token(.api_url)`. That is, the token is looked up based on the host. ## The gitcreds package gh now uses the gitcreds package to interact with the Git credential store. gh calls `gitcreds::gitcreds_get()` with a URL to try to find a matching PAT. `gitcreds::gitcreds_get()` checks session environment variables and then the local Git credential store. Therefore, if you have previously used a PAT with, e.g., command line Git, gh may retrieve and re-use it. You can call `gitcreds::gitcreds_get()` directly, yourself, if you want to see what is found for a specific URL. ``` r gitcreds::gitcreds_get() ``` If you see something like this: ``` r #> #> protocol: https #> host : github.com #> username: PersonalAccessToken #> password: <-- hidden --> ``` that means that gitcreds could get the PAT from the Git credential store. You can call `gitcreds_get()$password` to see the actual PAT. If no matching PAT is found, `gitcreds::gitcreds_get()` errors. ## PAT in an environment variable If you don't have a Git installation, or your Git installation does not have a working credential store, then you can specify the PAT in an environment variable. For `github.com` you can set the `GITHUB_PAT_GITHUB_COM` or `GITHUB_PAT` variable. For a different GitHub host, call `gitcreds::gitcreds_cache_envvar()` with the API URL to see the environment variable you need to set. For example: ```{r} gitcreds::gitcreds_cache_envvar("https://github.acme.com") ``` ## Recommendations On a machine used for interactive development, we recommend: * Store your PAT(s) in an official credential store. * Do **not** store your PAT(s) in plain text in, e.g., `.Renviron`. In the past, this has been a common and recommended practice for pragmatic reasons. However, gitcreds/gh have now evolved to the point where it's possible for all of us to follow better security practices. * If you use a general-purpose password manager, like 1Password or LastPass, you may *also* want to store your PAT(s) there. Why? If your PAT is "forgotten" from the OS-level credential store, intentionally or not, you'll need to provide it again when prompted. If you don't have any other record of your PAT, you'll have to get a new PAT whenever this happens. This is not the end of the world. But if you aren't disciplined about deleting lost PATs from , you will eventually find yourself in a confusing situation where you can't be sure which PAT(s) are in use. On a headless system, such as on a CI/CD platform, provide the necessary PAT(s) via secure environment variables. Regular environment variables can be used to configure less sensitive settings, such as the API host. Don't expose your PAT by doing something silly like dumping all environment variables to a log file. Note that on GitHub Actions, specifically, a personal access token is [automatically available to the workflow](https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token) as the `GITHUB_TOKEN` secret. That is why many workflows in the R community contain this snippet: ``` yaml env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} ``` This makes the automatic PAT available as the `GITHUB_PAT` environment variable. If that PAT doesn't have the right permissions, then you'll need to explicitly provide one that does (see link above for more). ## Failure If there is no PAT to be had, `gh::gh()` sends a request with no token. (Internally, the `Authorization` header is omitted if the PAT is found to be the empty string, `""`.) What do PAT-related failures look like? If no PAT is sent and the endpoint requires no auth, the request probably succeeds! At least until you run up against rate limits. If the endpoint requires auth, you'll get an HTTP error, possibly this one: ``` GitHub API error (401): 401 Unauthorized Message: Requires authentication ``` If a PAT is first discovered in an environment variable, it is taken at face value. The two most common ways to arrive here are PAT specification via `.Renviron` or as a secret in a CI/CD platform, such as GitHub Actions. If the PAT is invalid, the first affected request will fail, probably like so: ``` GitHub API error (401): 401 Unauthorized Message: Bad credentials ``` This will also be the experience if an invalid PAT is provided directly via `.token`. Even a valid PAT can lead to a downstream error, if it has insufficient scopes with respect to a specific request. gh/inst/doc/managing-personal-access-tokens.R0000644000176200001440000000101314042737101020651 0ustar liggesusers## ---- include = FALSE--------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----setup-------------------------------------------------------------------- library(gh) ## ---- eval = FALSE------------------------------------------------------------ # gh(endpoint, ..., .token = NULL, ..., .api_url = NULL, ...) ## ----------------------------------------------------------------------------- gitcreds::gitcreds_cache_envvar("https://github.acme.com") gh/inst/doc/managing-personal-access-tokens.html0000644000176200001440000004421214042737101021424 0ustar liggesusers Managing Personal Access Tokens

Managing Personal Access Tokens

library(gh)

gh generally sends a Personal Access Token (PAT) with its requests. Some endpoints of the GitHub API can be accessed without authenticating yourself. But once your API use becomes more frequent, you will want a PAT to prevent problems with rate limits and to access all possible endpoints.

This article describes how to store your PAT, so that gh can find it (automatically, in most cases). The function gh uses for this is gh_token().

More resources on PAT management:

PAT and host

gh::gh() allows the user to provide a PAT via the .token argument and to specify a host other than “github.com” via the .api_url argument. (Some companies and universities run their own instance of GitHub Enterprise.)

gh(endpoint, ..., .token = NULL, ..., .api_url = NULL, ...)

However, it’s annoying to always provide your PAT or host and it’s unsafe for your PAT to appear explicitly in your R code. It’s important to make it possible for the user to provide the PAT and/or API URL directly, but it should rarely be necessary. gh::gh() is designed to play well with more secure, less fiddly methods for expressing what you want.

How are .api_url and .token determined when the user does not provide them?

  1. .api_url defaults to the value of the GITHUB_API_URL environment variable and, if that is unset, falls back to "https://api.github.com". This is always done before worrying about the PAT.
  2. The PAT is obtained via a call to gh_token(.api_url). That is, the token is looked up based on the host.

The gitcreds package

gh now uses the gitcreds package to interact with the Git credential store.

gh calls gitcreds::gitcreds_get() with a URL to try to find a matching PAT. gitcreds::gitcreds_get() checks session environment variables and then the local Git credential store. Therefore, if you have previously used a PAT with, e.g., command line Git, gh may retrieve and re-use it. You can call gitcreds::gitcreds_get() directly, yourself, if you want to see what is found for a specific URL.

gitcreds::gitcreds_get()

If you see something like this:

#> <gitcreds>
#>   protocol: https
#>   host    : github.com
#>   username: PersonalAccessToken
#>   password: <-- hidden -->

that means that gitcreds could get the PAT from the Git credential store. You can call gitcreds_get()$password to see the actual PAT.

If no matching PAT is found, gitcreds::gitcreds_get() errors.

PAT in an environment variable

If you don’t have a Git installation, or your Git installation does not have a working credential store, then you can specify the PAT in an environment variable. For github.com you can set the GITHUB_PAT_GITHUB_COM or GITHUB_PAT variable. For a different GitHub host, call gitcreds::gitcreds_cache_envvar() with the API URL to see the environment variable you need to set. For example:

gitcreds::gitcreds_cache_envvar("https://github.acme.com")
#> [1] "GITHUB_PAT_GITHUB_ACME_COM"

Recommendations

On a machine used for interactive development, we recommend:

  • Store your PAT(s) in an official credential store.

  • Do not store your PAT(s) in plain text in, e.g., .Renviron. In the past, this has been a common and recommended practice for pragmatic reasons. However, gitcreds/gh have now evolved to the point where it’s possible for all of us to follow better security practices.

  • If you use a general-purpose password manager, like 1Password or LastPass, you may also want to store your PAT(s) there. Why? If your PAT is “forgotten” from the OS-level credential store, intentionally or not, you’ll need to provide it again when prompted.

    If you don’t have any other record of your PAT, you’ll have to get a new PAT whenever this happens. This is not the end of the world. But if you aren’t disciplined about deleting lost PATs from https://github.com/settings/tokens, you will eventually find yourself in a confusing situation where you can’t be sure which PAT(s) are in use.

On a headless system, such as on a CI/CD platform, provide the necessary PAT(s) via secure environment variables. Regular environment variables can be used to configure less sensitive settings, such as the API host. Don’t expose your PAT by doing something silly like dumping all environment variables to a log file.

Note that on GitHub Actions, specifically, a personal access token is automatically available to the workflow as the GITHUB_TOKEN secret. That is why many workflows in the R community contain this snippet:

env:
  GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}

This makes the automatic PAT available as the GITHUB_PAT environment variable. If that PAT doesn’t have the right permissions, then you’ll need to explicitly provide one that does (see link above for more).

Failure

If there is no PAT to be had, gh::gh() sends a request with no token. (Internally, the Authorization header is omitted if the PAT is found to be the empty string, "".)

What do PAT-related failures look like?

If no PAT is sent and the endpoint requires no auth, the request probably succeeds! At least until you run up against rate limits. If the endpoint requires auth, you’ll get an HTTP error, possibly this one:

GitHub API error (401): 401 Unauthorized
Message: Requires authentication

If a PAT is first discovered in an environment variable, it is taken at face value. The two most common ways to arrive here are PAT specification via .Renviron or as a secret in a CI/CD platform, such as GitHub Actions. If the PAT is invalid, the first affected request will fail, probably like so:

GitHub API error (401): 401 Unauthorized
Message: Bad credentials

This will also be the experience if an invalid PAT is provided directly via .token.

Even a valid PAT can lead to a downstream error, if it has insufficient scopes with respect to a specific request.

gh/inst/WORDLIST0000644000176200001440000000025014042730610013011 0ustar liggesusersCMD Codecov Github GraphQL JSON LastPass Minimalistic PATs URI api auth discoverable funder gitcreds github https keyring macOS pre programmatically repo repos usethis