gh/0000755000176200001440000000000013612511651010651 5ustar liggesusersgh/NAMESPACE0000644000176200001440000000142413612445243012074 0ustar liggesusers# Generated by roxygen2: do not edit by hand S3method(print,gh_response) export(gh) export(gh_first) export(gh_gql) export(gh_last) export(gh_next) export(gh_prev) export(gh_token) export(gh_tree_remote) export(gh_whoami) export(slugify_url) importFrom(cli,cli_alert_info) 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/LICENSE0000644000176200001440000000012113243233512011645 0ustar liggesusersYEAR: 2015-2016 COPYRIGHT HOLDER: Gábor Csárdi, Jennifer Bryan, Hadley Wickham gh/README.md0000644000176200001440000001110513612274250012127 0ustar liggesusers # gh > GitHub API [![Linux Build Status](https://travis-ci.org/r-lib/gh.svg?branch=master)](https://travis-ci.org/r-lib/gh) [![Windows Build status](https://ci.appveyor.com/api/projects/status/github/r-lib/gh?svg=true)](https://ci.appveyor.com/project/gaborcsardi/gh) [![](http://www.r-pkg.org/badges/version/gh)](http://www.r-pkg.org/pkg/gh) [![CRAN RStudio mirror downloads](http://cranlogs.r-pkg.org/badges/gh)](http://www.r-pkg.org/pkg/gh) [![Coverage Status](https://img.shields.io/codecov/c/github/r-lib/gh/master.svg)](https://codecov.io/github/r-lib/gh?branch=master) Minimalistic client to access [GitHub's API v3](https://developer.github.com/v3/). ## 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://developer.github.com/v3/). The first argument of `gh()` is the endpoint. Note that the leading slash must be included as well. Parameters can be passed as extra arguments. E.g. ```r my_repos <- gh("/user/repos", type = "public") vapply(my_repos, "[[", "", "name") ``` ``` #> [1] "after" "argufy" "ask" #> [4] "baseimports" "citest" "clisymbols" #> [7] "cmaker" "cmark" "conditions" #> [10] "crayon" "debugme" "devtools" #> [13] "diffobj" "disposables" "dotenv" #> [16] "elasticsearch-jetty" "falsy" "fswatch" #> [19] "gitty" "httr" "httrmock" #> [22] "ISA" "keypress" "lintr" #> [25] "macBriain" "maxygen" "MISO" #> [28] "parr" "parsedate" "pingr" ``` The JSON result sent by the API is converted to an R object. If the end point itself has parameters, these can also be passed as extra arguments: ```r j_repos <- gh("/users/:username/repos", username = "jeroen") vapply(j_repos, "[[", "", "name") ``` ``` #> [1] "apps" "asantest" "awk" #> [4] "base64" "bcrypt" "blog" #> [7] "brotli" "cheerio" "cmark" #> [10] "commonmark" "covr" "cranlogs" #> [13] "curl" "cyphr" "daff" #> [16] "data" "data.table.extras" "devtools" #> [19] "DiagrammeR" "docdbi" "docplyr" #> [22] "docs-travis-ci-com" "dplyr" "encode" #> [25] "evaluate" "feather" "fib" #> [28] "figures" "gdtools" "geojson" ``` ### 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 /users/:username/repos", username = "gaborcsardi", type = "public", page = 2) vapply(my_repos2, "[[", "", "name") ``` ``` #> [1] "pkgconfig" "playground" #> [3] "praise" "prettycode" #> [5] "prettyunits" "progress" #> [7] "prompt" "r-font" #> [9] "R6" "rcorpora" #> [11] "readline" "remoji" #> [13] "resume" "rhub-presentations" #> [15] "rintrojs" "roxygen" #> [17] "scidb" "spark" #> [19] "sparklyr" "splicing" #> [21] "tamper" "testthat" #> [23] "trump" "user2016-tutorial-shiny" #> [25] "webdriver" "whoami" ``` ## 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/0000755000176200001440000000000013612445240011424 5ustar liggesusersgh/man/gh_gql.Rd0000644000176200001440000000452613612423656013172 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, ..., .token = NULL, .destfile = NULL, .overwrite = FALSE, .api_url = NULL, .send_headers = NULL ) } \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, and named \code{NA} values trigger an error.} \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 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 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{.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"}.} } \description{ See more about the GraphQL API here: \url{https://developer.github.com/v4/} } \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_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_whoami.Rd0000644000176200001440000000555213612361004013657 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. The token itself is a string of 40 letters and digits. You can store it any way you like and provide explicitly via the \code{.token} argument to \code{\link[=gh]{gh()}}. However, many prefer to define an environment variable \code{GITHUB_PAT} (or \code{GITHUB_TOKEN}) with this value in their \code{.Renviron} file. Add a line that looks like this, substituting your PAT:\preformatted{GITHUB_PAT=8c70fd8419398999c9ac5bacf3192882193cadf2 } Put a line break at the end! If you’re using an editor that shows line numbers, there should be (at least) two lines, where the second one is empty. Restart R for this to take effect. Call \code{gh_whoami()} to confirm success. To get complete information on the authenticated user, call \code{gh("/user")}. For token management via API (versus the browser), use the \href{https://developer.github.com/v3/oauth_authorizations}{Authorizations API}. This API requires Basic Authentication using your username and password, not tokens, and is outside the scope of the gh package. } \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.Rd0000644000176200001440000001504413612423656012324 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/package.R \docType{package} \name{gh} \alias{gh} \alias{gh-package} \title{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 ) } \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, and named \code{NA} values trigger an error.} \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 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 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 paramter 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.} } \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{ Minimal wrapper to access GitHub's 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: \itemize{ \item Try to substitute each listed parameter into \code{endpoint}, using the \verb{: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} } \seealso{ Useful links: \itemize{ \item \url{https://github.com/r-lib/gh#readme} \item Report bugs at \url{https://github.com/r-lib/gh/issues} } \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. } \author{ \strong{Maintainer}: Gábor Csárdi \email{csardi.gabor@gmail.com} [contributor] Authors: \itemize{ \item Jennifer Bryan \item Hadley Wickham } } gh/man/gh_token.Rd0000644000176200001440000001120213612445240013505 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 \code{GITHUB_API_URL} environment variable if set, otherwise \url{https://api.github.com}.} } \value{ A string, with the token, or a zero length string scalar, if no token is available. } \description{ You can read more about PATs here: \url{https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/} and you can access your PATs here (if logged in to GitHub): \url{https://github.com/settings/tokens}. } \details{ Set the \code{GITHUB_PAT} environment variable to avoid having to include your PAT in the code. If you work with multiple GitHub deployments, e.g. via GitHub Enterprise, then read 'PATs for GitHub Enterprise' below. If you want a more secure solution than putting authentication tokens into environment variables, read 'Storing PATs in the system keyring' below. } \section{NA}{ gh supports storing your PAT in the system keyring, on Windows, macOS and Linux, using the keyring package. To turn on keyring support, you need to set the \code{GH_KEYRING} environment variables to \code{true}, in your \code{.Renviron} file or profile. If keyring support is turned on, then for each PAT environment variable, gh first checks whether the key with that value is set in the system keyring, and if yes, it will use its value as the PAT. I.e. without a custom \code{GITHUB_API_URL} variable, it checks the \code{GITHUB_PAT_API_GITHUB_COM} key first, then the env var with the same name, then the \code{GITHUB_PAT} key, etc. Such a check looks like this:\if{html}{\out{
}}\preformatted{keyring::key_get("GITHUB_PAT_API_GITHUB_COM") }\if{html}{\out{
}} and it uses the default keyring backend and the default keyring within that backend. See \code{\link[keyring:default_backend]{keyring::default_backend()}} for details and changing these defaults. If the selected keyring is locked, and the session is interactive, then gh will try to unlock it. If the keyring is locked, and the session is not interactive, then gh will not use the keyring. Note that some keyring backends cannot be locked (e.g. the one that uses environment variables). On some OSes, e.g. typically on macOS, you need to allow R to access the system keyring. You can allow this separately for each access, or for all future accesses, until you update or re-install R. You typically need to give access to each R GUI (e.g. RStudio) and the command line R program separately. To store your PAT on the keyring run\if{html}{\out{
}}\preformatted{keyring::key_set("GITHUB_PAT") }\if{html}{\out{
}} } \section{Storing PATs in the system keyring}{ gh supports storing your PAT in the system keyring, on Windows, macOS and Linux, using the keyring package. To turn on keyring support, you need to set the \code{GH_KEYRING} environment variables to \code{true}, in your \code{.Renviron} file or profile. If keyring support is turned on, then for each PAT environment variable, gh first checks whether the key with that value is set in the system keyring, and if yes, it will use its value as the PAT. I.e. without a custom \code{GITHUB_API_URL} variable, it checks the \code{GITHUB_PAT_API_GITHUB_COM} key first, then the env var with the same name, then the \code{GITHUB_PAT} key, etc. Such a check looks like this:\if{html}{\out{
}}\preformatted{keyring::key_get("GITHUB_PAT_API_GITHUB_COM") }\if{html}{\out{
}} and it uses the default keyring backend and the default keyring within that backend. See \code{\link[keyring:default_backend]{keyring::default_backend()}} for details and changing these defaults. If the selected keyring is locked, and the session is interactive, then gh will try to unlock it. If the keyring is locked, and the session is not interactive, then gh will not use the keyring. Note that some keyring backends cannot be locked (e.g. the one that uses environment variables). On some OSes, e.g. typically on macOS, you need to allow R to access the system keyring. You can allow this separately for each access, or for all future accesses, until you update or re-install R. You typically need to give access to each R GUI (e.g. RStudio) and the command line R program separately. To store your PAT on the keyring run\if{html}{\out{
}}\preformatted{keyring::key_set("GITHUB_PAT") }\if{html}{\out{
}} } \seealso{ \code{\link[=slugify_url]{slugify_url()}} for computing the environment variables that gh uses to search for API URL specific PATs. } 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/man/slugify_url.Rd0000644000176200001440000000110613612361004014250 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gh_token.R \name{slugify_url} \alias{slugify_url} \title{Compute the suffix that gh uses for GitHub API URL specific PATs} \usage{ slugify_url(url) } \arguments{ \item{url}{Character vector HTTP/HTTPS URLs.} } \value{ Character vector of suffixes. } \description{ Compute the suffix that gh uses for GitHub API URL specific PATs } \examples{ # The main GH site slugify_url("https://api.github.com") # A custom one slugify_url("https://github.acme.com") } \seealso{ \code{\link[=gh_token]{gh_token()}} } gh/DESCRIPTION0000644000176200001440000000146213612511651012362 0ustar liggesusersPackage: gh Title: 'GitHub' 'API' Version: 1.1.0 Authors@R: c(person("Gábor", "Csárdi",, "csardi.gabor@gmail.com", c("cre", "ctb")), person("Jennifer", "Bryan", role = "aut"), person("Hadley", "Wickham", role = "aut")) Description: Minimal client to access the 'GitHub' 'API'. License: MIT + file LICENSE LazyData: true URL: https://github.com/r-lib/gh#readme BugReports: https://github.com/r-lib/gh/issues Suggests: covr, keyring, pingr, testthat, withr Imports: cli, ini, jsonlite, httr (>= 1.2) RoxygenNote: 7.0.2.9000 Encoding: UTF-8 NeedsCompilation: no Packaged: 2020-01-24 01:39:13 UTC; gaborcsardi Author: Gábor Csárdi [cre, ctb], Jennifer Bryan [aut], Hadley Wickham [aut] Maintainer: Gábor Csárdi Repository: CRAN Date/Publication: 2020-01-24 06:50:17 UTC gh/tests/0000755000176200001440000000000013612361004012006 5ustar liggesusersgh/tests/testthat/0000755000176200001440000000000013612511651013653 5ustar liggesusersgh/tests/testthat/test-github_remote.R0000644000176200001440000000171613243233512017612 0ustar liggesuserscontext("github_remote") test_that("picks origin if available", { remotes <- list( upstream = "https://github.com/x/1", origin = "https://github.com/x/2" ) expect_warning(gr <- github_remote(remotes), "Using origin") expect_equal(gr$repo, "2") }) test_that("otherwise picks first", { remotes <- list( a = "https://github.com/x/1", b = "https://github.com/x/2" ) expect_warning(gr <- github_remote(remotes), "Using first") expect_equal(gr$repo, "1") }) # Parsing ----------------------------------------------------------------- test_that("parses common url forms", { expected <- list(username = "x", repo = "y") expect_equal(github_remote_parse("https://github.com/x/y.git"), expected) expect_equal(github_remote_parse("https://github.com/x/y"), expected) expect_equal(github_remote_parse("git@github.com:x/y.git"), expected) }) test_that("returns NULL if can't parse", { expect_equal(github_remote_parse("blah"), NULL) }) gh/tests/testthat/test-mock-repos.R0000644000176200001440000000674213446505601017046 0ustar liggesusers context("repos") test_that("repos, some basics", { skip_if_offline() skip_on_cran() skip("needs mocking") res <- gh("/user/repos", .token = tt()) expect_true(all(c("id", "name", "full_name") %in% names(res[[1]]))) res <- gh("/users/:username/repos", username = "gaborcsardi", .token = tt()) expect_true(all(c("id", "name", "full_name") %in% names(res[[1]]))) res <- gh("/orgs/:org/repos", org = "r-lib", type = "sources", .token = tt()) expect_true("desc" %in% vapply(res, "[[", "name", FUN.VALUE = "")) res <- gh("/repositories", .token = tt()) expect_true(all(c("id", "name", "full_name") %in% names(res[[1]]))) res <- gh( "POST /user/repos", name = "gh-testing", description = "Test repo for gh", homepage = "https://github.com/r-lib/gh", private = FALSE, has_issues = FALSE, has_wiki = FALSE, .token = tt() ) expect_equal(res$name, "gh-testing") expect_equal(res$description, "Test repo for gh") expect_equal(res$homepage, "https://github.com/r-lib/gh") expect_false(res$private) expect_false(res$has_issues) expect_false(res$has_wiki) ## TODO: POST /orgs/:org/repos res <- gh( "/repos/:owner/:repo", owner = gh_test_owner, repo = "gh-testing", .token = tt() ) expect_equal(res$name, "gh-testing") expect_equal(res$description, "Test repo for gh") expect_equal(res$homepage, "https://github.com/r-lib/gh") expect_false(res$private) expect_false(res$has_issues) expect_false(res$has_wiki) res <- gh( "PATCH /repos/:owner/:repo", owner = gh_test_owner, repo = "gh-testing", name = "gh-testing", description = "Still a test repo", .token = tt() ) expect_equal(res$name, "gh-testing") expect_equal(res$description, "Still a test repo") res <- gh( "GET /repos/:owner/:repo/contributors", owner = gh_test_owner, repo = "myrepo", .token = tt() ) expect_true("gh-testing" %in% vapply(res, "[[", "", "login")) res <- gh( "GET /repos/:owner/:repo/languages", owner = "r-lib", repo = "desc", .token = tt() ) expect_true("R" %in% names(res)) ## TODO: GET /repos/:owner/:repo/teams does not seem to work res <- gh( "GET /repos/:owner/:repo/teams", owner = "gh-testing-org", repo = "org-repo", .token = tt() ) expect_true("myteam" %in% vapply(res, "[[", "", "name")) res <- gh( "GET /repos/:owner/:repo/tags", owner = "gh-testing", repo = "myrepo", .token = tt() ) expect_true(res[[1]]$name == "v0.0.1") res <- gh( "DELETE /repos/:owner/:repo", owner = "gh-testing", repo = "gh-testing", .token = tt() ) expect_equal(res[[1]], "") # TODO: better return value here? }) test_that("repo files", { skip_if_offline() skip_on_cran() skip("needs mocking") res <- gh( "GET /repos/:owner/:repo/contents/:path", owner = "r-lib", repo = "gh", path = "DESCRIPTION", .send_headers = c(Accept = "application/vnd.github.v3.raw"), .token = tt() ) expect_equal(attr(res, "response")[["x-github-media-type"]], "github.v3; param=raw") expect_equal(class(res), c("gh_response", "raw")) tmp <- tempfile() res <- gh("/orgs/:org/repos", org = "r-lib", type = "sources", .token = tt()) res_file <- gh("/orgs/:org/repos", org = "r-lib", type = "sources", destfile = tmp, .token = tt()) expect_equal(class(res_file), c("gh_response", "path")) expect_equivalent(res, jsonlite::fromJSON(res_file, simplifyVector = FALSE)) }) gh/tests/testthat/test-token.R0000644000176200001440000000475213612445240016103 0ustar liggesusers test_that("api specific token is used", { env <- c( GH_KEYRING = "false", GITHUB_API_URL = "https://github.acme.com", GITHUB_PAT_GITHUB_ACME_COM = "good", GITHUB_PAT_GITHUB_ACME2_COM = "good2", GITHUB_PAT = "bad", GITHUB_TOKEN = "bad2" ) withr::with_envvar(env, { expect_equal(gh_token(), "good") expect_equal(gh_token("https://github.acme2.com"), "good2") }) env2 <- c( GH_KEYRING = "false", GITHUB_API_URL = NA, GITHUB_PAT_API_GITHUB_COM = "good", GITHUB_PAT = "bad", GITHUB_TOKEN = "bad2" ) withr::with_envvar(env2, { expect_equal(gh_token(), "good") expect_equal(gh_token("https://api.github.com"), "good") }) }) test_that("fall back to GITHUB_PAT", { env <- c( GH_KEYRING = "false", GITHUB_API_URL = "https://github.acme.com", GITHUB_PAT_GITHUB_ACME2_COM = "acme2", GITHUB_PAT = "pat", GITHUB_TOKEN = "token" ) withr::with_envvar(env, { expect_equal(gh_token(), "pat") expect_equal(gh_token("https://github.acme4.com"), "pat") }) env2 <- c( GH_KEYRING = "false", GITHUB_API_URL = "https://github.acme.com", GITHUB_PAT = "pat", GITHUB_TOKEN = "token" ) withr::with_envvar(env2, { expect_equal(gh_token(), "pat") expect_equal(gh_token("https://github.acme4.com"), "pat") }) env3 <- c( GH_KEYRING = "false", GITHUB_API_URL = NA, GITHUB_PAT_API_GITHUB_COM = NA, GITHUB_PAT = "pat", GITHUB_TOKEN = "token" ) withr::with_envvar(env3, { expect_equal(gh_token(), "pat") expect_equal(gh_token("https://api.github.com"), "pat") }) }) test_that("fall back to GITHUB_TOKEN", { env <- c( GH_KEYRING = "false", GITHUB_API_URL = "https://github.acme.com", GITHUB_PAT_GITHUB_ACME2_COM = "acme2", GITHUB_PAT = NA, GITHUB_TOKEN = "token" ) withr::with_envvar(env, { expect_equal(gh_token(), "token") expect_equal(gh_token("https://github.acme4.com"), "token") }) env2 <- c( GH_KEYRING = "false", GITHUB_API_URL = "https://github.acme.com", GITHUB_PAT = NA, GITHUB_TOKEN = "token" ) withr::with_envvar(env2, { expect_equal(gh_token(), "token") expect_equal(gh_token("https://github.acme4.com"), "token") }) env3 <- c( GH_KEYRING = "false", GITHUB_API_URL = NA, GITHUB_PAT_API_GITHUB_COM = NA, GITHUB_PAT = NA, GITHUB_TOKEN = "token" ) withr::with_envvar(env3, { expect_equal(gh_token(), "token") expect_equal(gh_token("https://api.github.com"), "token") }) }) gh/tests/testthat/test-na-null.R0000644000176200001440000000140013611545611016316 0ustar liggesusers 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-mock-error.R0000644000176200001440000000101113243233512017021 0ustar liggesuserscontext("github_error") test_that("errors return a github_error object", { skip_if_offline() skip_on_cran() e <- tryCatch(gh("/missing", .token = tt()), error = identity) expect_s3_class(e, "github_error") expect_s3_class(e, "http_error_404") }) test_that("can catch a given status directly", { skip_if_offline() skip_on_cran() e <- tryCatch( gh("/missing", .token = tt()), "http_error_404" = identity ) expect_s3_class(e, "github_error") expect_s3_class(e, "http_error_404") }) gh/tests/testthat/helper-offline.R0000644000176200001440000000062313243233512016673 0ustar liggesusers skip_if_offline <- (function() { offline <- NA function() { if (is.na(offline)) { offline <<- tryCatch( is.na(pingr::ping_port("github.com", count = 1, timeout = 1)), error = function(e) TRUE ) } if (offline) skip("Offline") } })() skip_if_no_token <- function() { if (is.na(Sys.getenv("GH_TESTING", NA_character_))) { skip("No GitHub token") } } gh/tests/testthat/helper.R0000644000176200001440000000075713243233512015263 0ustar liggesusers ## If there is no GH_TESTING env var, then ## * we are replaying previously recorded responses ## * we are performing unseen requests, but this should not happen. ## ## If there is a GH_TESTING env var, then we are in dev mode. ## * It's still being sorted out exactly what this means re: ## replaying and recording. For now, same default as above. ## https://github.com/gaborcsardi/httrmock/issues/5 ## * Reveal debugging info from httrmock. tt <- function() Sys.getenv("GH_TESTING", NA) gh/tests/testthat/test-build_request.R0000644000176200001440000000175113243233512017623 0ustar liggesuserscontext("build_request") test_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) }) gh/tests/testthat/test-utils.R0000644000176200001440000000122713243233512016112 0ustar liggesuserscontext("utils") test_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)) }) gh/tests/testthat/test-mock-whoami.R0000644000176200001440000000176213243233512017171 0ustar liggesuserscontext("whoami") test_that("whoami works in presence of PAT", { skip_if_offline() skip_on_cran() skip_if_no_token() res <- gh_whoami(.token = tt()) expect_s3_class(res, "gh_response") expect_identical(res[["login"]], "gh-testing") expect_match(res[["scopes"]], "\\brepo\\b") expect_match(res[["scopes"]], "\\buser\\b") }) test_that("whoami works in absence of PAT", { skip_if_offline() skip_on_cran() expect_message(res <- gh_whoami(.token = ""), "No personal access token \\(PAT\\) available.") expect_null(res) }) test_that("whoami errors with bad PAT", { skip("re-activate when request matching sorted out (gaborcsardi/httrmock#3)") skip_if_offline() skip_on_cran() e <- tryCatch(gh_whoami(.token = NA), error = identity) expect_s3_class(e, "github_error") expect_s3_class(e, "http_error_401") e <- tryCatch(gh_whoami(.token = "blah"), error = identity) expect_s3_class(e, "github_error") expect_s3_class(e, "http_error_401") }) gh/tests/testthat.R0000644000176200001440000000021213612361004013764 0ustar liggesuserslibrary(testthat) library(gh) # Don't want to use keyrings on CRAN withr::with_envvar( c(GH_NO_KEYRING = "true"), test_check("gh") ) gh/R/0000755000176200001440000000000013612445243011055 5ustar liggesusersgh/R/print.R0000644000176200001440000000076113446505601012340 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)) } } gh/R/utils.R0000644000176200001440000000460613612361004012336 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 (tolower(getOption("rstudio.notebook.executing", "false")) == "true") { FALSE } else if (identical(Sys.getenv("TESTTHAT"), "true")) { FALSE } else { interactive() } } gh/R/gh_request.R0000644000176200001440000000715013612423127013346 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 (!grepl(":", 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) { n <- names(params)[i] p <- params[[i]][1] endpoint2 <- gsub(paste0(":", n, "\\b"), p, endpoint) if (endpoint2 != endpoint) { endpoint <- endpoint2 done[i] <- TRUE } } 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_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_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 <- x$api_url %||% default_api_url() x$url <- URLencode(paste0(x$api_url, x$endpoint)) } x } #' @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 } gh_send_headers <- function(accept_header = NULL, headers = NULL) { modify_vector( modify_vector(default_send_headers, accept_header), headers ) } gh/R/gh_whoami.R0000644000176200001440000000531113612361004013132 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. The token itself is a string of 40 letters and digits. You #' can store it any way you like and provide explicitly via the `.token` #' argument to [gh()]. #' #' However, many prefer to define an environment variable `GITHUB_PAT` (or #' `GITHUB_TOKEN`) with this value in their `.Renviron` file. Add a #' line that looks like this, substituting your PAT: #' #' ``` #' GITHUB_PAT=8c70fd8419398999c9ac5bacf3192882193cadf2 #' ``` #' #' Put a line break at the end! If you’re using an editor that shows line #' numbers, there should be (at least) two lines, where the second one is empty. #' Restart R for this to take effect. Call `gh_whoami()` to confirm #' success. #' #' To get complete information on the authenticated user, call #' `gh("/user")`. #' #' For token management via API (versus the browser), use the #' [Authorizations API](https://developer.github.com/v3/oauth_authorizations). #' This API requires Basic Authentication using your username and password, #' not tokens, and is outside the scope of the gh package. #' #' @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 <- obfuscate(.token) ## 'gh_response' class has to be restored class(res) <- c("gh_response", "list") res } obfuscate <- function(x, first = 2, last = 0) { paste0(substr(x, start = 1, stop = first), "...", substr(x, start = nchar(x) - last + 1, stop = nchar(x))) } 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.R0000644000176200001440000004500313611545611012515 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 crayon 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() 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. #' @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 (is.null(cond$call) || isTRUE(cond$call)) { cond$call <- sys.call(-1) %||% sys.call() } # 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. 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$print.rlib_error <- print_rlib_error env$print.rlib_trace <- print_rlib_trace 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 { # 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. #' @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, ...) { realcall <- 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` <- sys.parent() 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()]. rethrow <- function(expr, cond) { realcall <- 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` <- sys.parent() 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) } ) } # -- 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(topenv(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)) { # If the parent does not have a trace, that means that it is using # the same trace as us. 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 } 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, 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_rlib_error <- 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") } if (!is.null(x$parent)) { cat("-->\n") print(x$parent) } invisible(x) } print_rlib_trace <- function(x, ...) { cl <- setdiff(x$classes, c("error", "condition")) cl <- paste0(" ERROR TRACE for ", paste(cl, collapse = ", "), "") 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(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) } 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) } # -- printing, styles ------------------------------------------------- has_crayon <- function() "crayon" %in% loadedNamespaces() style_numbers <- function(x) { if (has_crayon()) crayon::silver(x) else x } style_srcref <- function(x) { if (has_crayon()) crayon::italic(crayon::cyan(x)) } style_error <- function(x) { sx <- paste0("\n x ", x, " ") if (has_crayon()) crayon::bold(crayon::red(sx)) else sx } style_trace_title <- function(x) { if (has_crayon()) crayon::bold(x) else x } style_process <- function(x) { if (has_crayon()) crayon::bold(x) else x } style_call <- function(x) { if (!has_crayon()) return(x) call <- sub("^([^(]+)[(].*$", "\\1", x) rest <- sub("^[^(]+([(].*)$", "\\1", x) if (call == x || rest == x) return(x) paste0(crayon::yellow(call), rest) } env <- environment() parent.env(env) <- baseenv() structure( list( .internal = 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 ), 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 gh/R/gh_response.R0000644000176200001440000000465113611712326013520 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://developer.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.R0000644000176200001440000000150213611547126012441 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, ..., .token = NULL, .destfile = NULL, .overwrite = FALSE, .api_url = NULL, .send_headers = NULL) { if (".limit" %in% names(list(...))) { stop("`.limit` does not work with the GraphQL API") } gh(endpoint = "POST /graphql", query = query, ..., .token = .token) } gh/R/gh_token.R0000644000176200001440000001416213612445243013002 0ustar liggesusers #' Return the local user's GitHub Personal Access Token (PAT) #' #' You can read more about PATs here: #' #' and you can access your PATs here (if logged in to GitHub): #' . #' #' Set the `GITHUB_PAT` environment variable to avoid having to include #' your PAT in the code. If you work with multiple GitHub deployments, #' e.g. via GitHub Enterprise, then read 'PATs for GitHub Enterprise' below. #' #' If you want a more secure solution than putting authentication tokens #' into environment variables, read 'Storing PATs in the system keyring' #' below. #' #' @section: PATs for GitHub Enterprise: #' #' gh lets you use different PATs for different GitHub API URLs, by looking #' for the PAT in an URL specific environment variable first. It uses #' [slugify_url()] to compute a suffix from the API URL, by extracting the #' host name and removing the protocol and the path from it, and replacing #' special characters with underscores. This suffix is added to #' `GITHUB_PAT_` then. For example for the default API URL: #' , the `GITHUB_PAT_API_GITHUB_COM` environment #' variable is consulted first. #' #' You can set the default API URL via the `GITHUB_API_URL` environment #' variable. #' #' If the API URL specific environment variable is not set, then gh falls #' back to `GITHUB_PAT` and then to `GITHUB_TOKEN'. #' #' @section Storing PATs in the system keyring: #' #' gh supports storing your PAT in the system keyring, on Windows, macOS #' and Linux, using the keyring package. To turn on keyring support, you #' need to set the `GH_KEYRING` environment variables to `true`, in your #' `.Renviron` file or profile. #' #' If keyring support is turned on, then for each PAT environment variable, #' gh first checks whether the key with that value is set in the system #' keyring, and if yes, it will use its value as the PAT. I.e. without a #' custom `GITHUB_API_URL` variable, it checks the #' `GITHUB_PAT_API_GITHUB_COM` key first, then the env var with the same #' name, then the `GITHUB_PAT` key, etc. Such a check looks like this: #' #' ```r #' keyring::key_get("GITHUB_PAT_API_GITHUB_COM") #' ``` #' #' and it uses the default keyring backend and the default keyring within #' that backend. See [keyring::default_backend()] for details and changing #' these defaults. #' #' If the selected keyring is locked, and the session is interactive, #' then gh will try to unlock it. If the keyring is locked, and the session #' is not interactive, then gh will not use the keyring. Note that some #' keyring backends cannot be locked (e.g. the one that uses environment #' variables). #' #' On some OSes, e.g. typically on macOS, you need to allow R to access the #' system keyring. You can allow this separately for each access, or for #' all future accesses, until you update or re-install R. You typically #' need to give access to each R GUI (e.g. RStudio) and the command line #' R program separately. #' #' To store your PAT on the keyring run #' ```r #' keyring::key_set("GITHUB_PAT") #' ``` #' #' @param api_url Github API url. Defaults to `GITHUB_API_URL` #' environment variable if set, otherwise . #' #' @return A string, with the token, or a zero length string scalar, #' if no token is available. #' #' @seealso [slugify_url()] for computing the environment variables that #' gh uses to search for API URL specific PATs. #' @export gh_token <- function(api_url = NULL) { api_url <- api_url %||% default_api_url() token_env_var <- paste0("GITHUB_PAT_", slugify_url(api_url)) get_first_token_found(c(token_env_var, "GITHUB_PAT", "GITHUB_TOKEN")) } #' @importFrom cli cli_alert_info should_use_keyring <- function() { # Opt in? if (tolower(Sys.getenv("GH_KEYRING", "")) != "true") return(FALSE) # Can we load the package? if (!can_load("keyring")) { cli_alert_info("{.pkg gh}: the {.pkg keyring} package is not available") return(FALSE) } # If is_locked() errors, the keyring cannot be locked, and we'll use it err <- FALSE tryCatch( locked <- keyring::keyring_is_locked(), error = function(e) err <- TRUE ) if (err) return(TRUE) # Otherwise if locked, and non-interactive session, we won't use it if (locked && ! is_interactive()) { cli_alert_info("{.pkg gh}: default keyring is locked") return(FALSE) } # Otherwise if locked, we try to unlock it here. Otherwise key_get() # would unlock it, but if that fails, we'll get multiple unlock dialogs # It is better to fail here, once and for all. if (locked) { err <- FALSE tryCatch(keyring::keyring_unlock(), error = function(e) err <- TRUE) if (err) { cli_alert_info("{.pkg gh}: failed to unlock default keyring") return(FALSE) } } TRUE } get_first_token_found <- function(vars) { if (length(vars) == 0) return("") has_keyring <- should_use_keyring() val <- "" key_get <- function(v) { if (has_keyring) tryCatch(keyring::key_get(v), error = function(e) NULL) } for (var in vars) { if ((val <- key_get(var) %||% "") != "") break if ((val <- Sys.getenv(var, "")) != "") break } val } gh_auth <- function(token) { if (isTRUE(token != "")) { c("Authorization" = paste("token", token)) } else { character() } } #' Compute the suffix that gh uses for GitHub API URL specific PATs #' #' @param url Character vector HTTP/HTTPS URLs. #' @return Character vector of suffixes. #' #' @seealso [gh_token()] #' @export #' @examples #' # The main GH site #' slugify_url("https://api.github.com") #' #' # A custom one #' slugify_url("https://github.acme.com") slugify_url <- function(url) { if (!any(grepl("^https?://", url))) { stop("Only works with HTTP(S) protocols") } x2 <- sub("^.*://([^/]*@)?", "", url) x3 <- sub("/+$", "", x2) x4 <- gsub("[./]+", "_", x3) x5 <- gsub("[^-a-zA-Z0-9_]", "", x4) toupper(x5) } get_baseurl <- function(x) { if (!any(grepl("^https?://", x))) stop("Only works with HTTP(S) protocols") prot <- sub("^(https?://).*$", "\\1", x) rest <- sub("^https?://(.*)$", "\\1", x) host <- sub("/.*$", "", rest) paste0(prot, host) } gh/R/pagination.R0000644000176200001440000000644013611606335013335 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 <- state$got + 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/package.R0000644000176200001440000001646513612423653012610 0ustar liggesusers #' GitHub API #' #' Minimal wrapper to access GitHub's API. #' #' @docType package #' @name gh "_PACKAGE" #' 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, #' and named `NA` values trigger an error. #' @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 paramter 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. #' #' @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. #' #' @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 #' @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)) 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 <- drop_named_nulls(params) check_named_nas(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 (.progress) prbr <- make_progress_bar(req) raw <- gh_make_request(req) res <- gh_process_response(raw) while (!is.null(.limit) && length(res) < .limit && gh_has_next(res)) { update_progress_bar(prbr, res) res2 <- gh_next(res) res3 <- c(res, res2) attributes(res3) <- attributes(res2) res <- res3 } if (! is.null(.limit) && length(res) > .limit) { res_attr <- attributes(res) res <- res[seq_len(.limit)] attributes(res) <- res_attr } 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.md0000644000176200001440000000204213612445256011754 0ustar liggesusers # 1.1.0 * Raw reponses from GitHub are now returned as raw vector. * Responses may be wrtten 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). # 1.0.1 First public release. gh/MD50000644000176200001440000000346213612511651011166 0ustar liggesusers7b61827ddae52e0618e0501a55e22a52 *DESCRIPTION 6e2bfe8ab7185ace1e16748eccf00613 *LICENSE ed7849e5e0ca9ee8dcfc8c3e0f6e994d *NAMESPACE f3c966135e202e782267f730affc2ff7 *NEWS.md c6d0d6bfd3ec2dab7a5043e04e68dee8 *R/errors.R 203a372f4efd1b61abd02ef34ffa4fe5 *R/gh_gql.R cea2564f6ebdb0f6487be88a0ab97881 *R/gh_request.R cc851d1bbfca4bd66af479724083e567 *R/gh_response.R a340e854a21a9a6aae0e81159e5ae494 *R/gh_token.R 251f84ea4af2c05665f6b6554eaf4d99 *R/gh_whoami.R fe9a5d8826ad1065d035d0cf85fd40dc *R/git.R 3928ede0346c3fc83d0d98d5396315b8 *R/package.R 542fa9d9cc103c75f40de12112d535d5 *R/pagination.R d16c16e9652ab1df46ff973edc657f47 *R/print.R 88081c19eb1d86e4be381272e865d502 *R/utils.R 7d8f8f8b5685982abb3e563ab40d0219 *README.md 1852a1b0a8ecf888797468f15cef7d4b *man/gh.Rd df7fa8e0c06cbfcbccbc87df0e533d52 *man/gh_gql.Rd e5358b38e37815d75c80ead3edadc2eb *man/gh_next.Rd ecb4afe5ca4aebef0c69e3e404a839ba *man/gh_token.Rd 8dc288d4beadb347828028716153ed0c *man/gh_tree_remote.Rd 81592cce152507de14e33b343ab75d6d *man/gh_whoami.Rd 016da8202cc86b463da849981d5d309a *man/print.gh_response.Rd efac233ae44c51449f01d323dfee841e *man/slugify_url.Rd d0c90d0eb37412ad3921eb75aebb6dde *tests/testthat.R ef66d955bc844f203b685ea981f621af *tests/testthat/helper-offline.R 9e259b79838b74792d363f5b5c7d94ec *tests/testthat/helper.R edd42bb707098d64a1571c079bbbeabc *tests/testthat/test-build_request.R bf0ee44e0b27e0a51cac15fa5737dfe4 *tests/testthat/test-github_remote.R a4b92720497e669a65ce913af6350222 *tests/testthat/test-mock-error.R c7723b901c2147f7720916420b24c12e *tests/testthat/test-mock-repos.R f15bf29d803b86071b167897e268e22f *tests/testthat/test-mock-whoami.R 6c40a447b90ccb2b4ff789c361f1a3a3 *tests/testthat/test-na-null.R 965555aa5c97a176289ee2c21203ee92 *tests/testthat/test-token.R 4a295eeb98f175301e985e3275010d0f *tests/testthat/test-utils.R