gitcreds/0000755000176200001440000000000014306343457012067 5ustar liggesusersgitcreds/NAMESPACE0000644000176200001440000000053214212611043013267 0ustar liggesusers# Generated by roxygen2: do not edit by hand S3method(format,gitcreds) S3method(print,gitcreds) export(gitcreds_approve) export(gitcreds_cache_envvar) export(gitcreds_delete) export(gitcreds_fill) export(gitcreds_get) export(gitcreds_list) export(gitcreds_list_helpers) export(gitcreds_parse_output) export(gitcreds_reject) export(gitcreds_set) gitcreds/LICENSE0000644000176200001440000000005414210415267013064 0ustar liggesusersYEAR: 2020 COPYRIGHT HOLDER: Gábor Csárdi gitcreds/README.md0000644000176200001440000001251314306337227013346 0ustar liggesusers # gitcreds > Query git credentials from R [![R build status](https://github.com/r-lib/gitcreds/workflows/R-CMD-check/badge.svg)](https://github.com/r-lib/gitcreds/actions) [![Codecov test coverage](https://codecov.io/gh/r-lib/gitcreds/branch/main/graph/badge.svg)](https://app.codecov.io/gh/r-lib/gitcreds?branch=main) [![R-CMD-check](https://github.com/r-lib/gitcreds/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/r-lib/gitcreds/actions/workflows/R-CMD-check.yaml) ## Features - (Re)use the same credentials in command line git, R and the RStudio IDE., etc. Users can set their GitHub token once and use it everywhere. - Typically more secure than storing passwords and tokens in `.Renviron` files. - gitcreds has a cache that makes credential lookup very fast. - gitcreds supports multiple users and multiple hosts, including Enterprise GitHub installations. - If git or git credential helpers are not available, e.g. typically on a Linux server, or a CI, then gitcreds can fall back to use environment variables, and it still supports multiple users and hosts. ## Installation Install the package from CRAN: ``` r install.packages("gitcreds") ``` ## Usage gitcreds is typically used upstream, in R packages that need to authenticate to git or GitHub. End users of these packages might still find it useful to call gitcreds directly, to set up their credentials, or check that they have been set up correctly. You can also use gitcreds in an R script. In this case you are both the end user and the upstream developer. ### Usage as an end user ``` r library(gitcreds) ``` Use `gitcreds_get()` to check your GitHub or other git credentials. It returns a named list, with a `password` entry. The password is not printed by default: ``` r gitcreds_get() ``` ``` r #> #> protocol: https #> host : github.com #> username: gaborcsardi #> password: <-- hidden --> ``` Use `gitcreds_set()` to add new credentials, or replace existing ones. It always asks you before replacing existing credentials: ``` r gitcreds_set() ``` ``` r #> -> Your current credentials for 'https://github.com': #> #> protocol: https #> host : github.com #> username: gaborcsardi #> password: <-- hidden --> #> #> -> What would you like to do? #> #> 1: Keep these credentials #> 2: Replace these credentials #> 3: See the password / token #> #> Selection: 2 #> #> ? Enter new password or token: secret #> -> Removing current credentials... #> -> Adding new credentials... #> -> Removing credentials from cache... #> -> Done. ``` Use `gitcreds_delete()` to delete credentials. It always asks you before actually deleting any credentials: ``` r gitcreds_delete() ``` ``` r #> -> Your current credentials for 'https://github.com': #> #> protocol: https #> host : github.com #> username: token #> password: <-- hidden --> #> #> -> What would you like to do? #> #> 1: Keep these credentials #> 2: Delete these credentials #> 3: See the password / token #> #> Selection: 2 #> -> Removing current credentials... #> -> Removing credentials from cache... #> -> Done. ``` ### Usage as a package author If you want to use git’s credentials in your package, call `gitcreds_get()`. You probably want to handle the various errors it can return. Here is an example for a function that optionally neeeds a GitHub token. It searches the code of a GitHub repository: ``` r github_search <- function(query, repo = "wch/r-source") { token <- tryCatch( gitcreds::gitcreds_get(), error = function(e) NULL ) url <- "https://api.github.com/search/code" q <- list(q = paste0(query, "+repo:", repo)) token <- paste0("token ", token$password) httr::GET(url, query = q, httr::add_headers(Authorization = token)) } ``` The next example always needs a GitHub token, so it fails without one. It lists the public repositories of the current user: ``` r msg <- function(wh) { msgs <- c( no_git = paste0( "No git installation found. You need to install git and set up ", "your GitHub Personal Access token using `gitcreds::gitcreds_set()`."), no_creds = paste0( "No git credentials found. Please set up your GitHub Personal Access ", "token using `gitcreds::gitcreds_set()`.") ) msgs[wh] } my_private_repos <- function() { token <- tryCatch( gitcreds::gitcreds_get(), gitcreds_nogit_error = function(e) stop(msg("no_git")), gitcreds_no_credentials = function(e) stop(msg("no_creds")) ) url <- "https://api.github.com/user/repos" q <- list(visibility = "public") token <- paste0("token ", token$password) httr::GET(url, query = q, httr::add_headers(Authorization = token)) } ``` Point your users to `gitcreds_set()` for adding/updating their credentials, or write your own wrapper for this. If you want more control or a different UI, take a look at the lower level `gitcreds_fill()`, `gitcreds_approve()` and `gitcreds_reject()` functions. See also [gitcreds for package authors](https://gitcreds.r-lib.org/articles/package.html). ## Code of Conduct Please note that the gitcreds project is released with a [Contributor Code of Conduct](https://contributor-covenant.org/version/2/1/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. ## License MIT © [RStudio](https://github.com/rstudio) gitcreds/man/0000755000176200001440000000000014214106402012623 5ustar liggesusersgitcreds/man/gitcreds_get.Rd0000644000176200001440000003375514306336044015603 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/aaa-doc.R \name{gitcreds_get} \alias{gitcreds_get} \alias{gitcreds} \alias{gitcreds_set} \alias{gitcreds_delete} \alias{gitcreds_list_helpers} \title{Query and set git credentials} \usage{ gitcreds_get(url = "https://github.com", use_cache = TRUE, set_cache = TRUE) gitcreds_set(url = "https://github.com") gitcreds_delete(url = "https://github.com") gitcreds_list_helpers() } \arguments{ \item{url}{URL to get, set or delete credentials for. It may contain a user name, which is typically (but not always) used by the credential helpers. It may also contain a path, which is typically (but not always) ignored by the credential helpers.} \item{use_cache}{Whether to try to use the environment variable cache before turning to git to look up the credentials for \code{url}. See \code{\link[=gitcreds_cache_envvar]{gitcreds_cache_envvar()}}.} \item{set_cache}{Whether to set the environment variable cache after receiving the credentials from git. See \code{\link[=gitcreds_cache_envvar]{gitcreds_cache_envvar()}}.} } \value{ \code{gitcreds_get()} returns a \code{gitcreds} object, a named list of strings, the fields returned by the git credential handler. Typically the fields are \code{protocol}, \code{host}, \code{username}, \code{password}. Some credential helpers support path-dependent credentials and also return a \code{path} field. \code{gitcreds_set()} returns nothing. \code{gitcreds_delete()} returns \code{FALSE} if it did not find find any credentials to delete, and thus it did not call \verb{git credential reject}. Otherwise it returns \code{TRUE}. \code{gitcreds_get()} errors if git is not installed, no credential helpers are configured or no credentials are found. \code{gitcreds_set()} errors if git is not installed, or setting the new credentials fails. \code{gitcreds_delete()} errors if git is not installed or the git calls fail. See \code{vignette("package", package = "gitcreds")} if you want to handle these errors. \code{gitcreds_list_helpers()} returns a character vector, corresponding to the \code{credential.helper} git configuration key. Usually it contains a single credential helper, but it is possible to configure multiple helpers. } \description{ This manual page is for \emph{users} of packages that depend on gitcreds for managing tokens or passwords to GitHub or other git repositories. If you are a package author and want to import gitcreds for this functionality, see \code{vignette("package", package = "gitcreds")}. Otherwise please start at 'Basics' below. } \section{Basics}{ \code{gitcreds_get()} queries git credentials. It is typically used by package code that needs to authenticate to GitHub or another git repository. The end user might call \code{gitcreds_get()} directly to check that the credentials are properly set up. \code{gitcreds_set()} adds or updates git credentials in the credential store. It is typically called by the user, and it only works in interactive sessions. It always asks for acknowledgement before it overwrites existing credentials. \code{gitcreds_delete()} deletes git credentials from the credential store. It is typically called by the user, and it only works in interactive sessions. It always asks for acknowledgement. \code{gitcreds_list_helpers()} lists the active credential helpers. \subsection{git versions}{ These functions use the \verb{git credential} system command to query and set git credentials. They need an external git installation. You can download git from https://git-scm.com/downloads. A recent version is best, but at least git 2.9 is suggested. gitcreds should work out of the box on macOS with git versions 2.9.2 or later, and on Windows with git versions 2.12.1 or later, using the default git settings. On Windows, for git versions from 2.9.2 up until 2.12.1 you probably need to set the default credential helper to \code{wincred}. It is usually simpler to update git to a recent version. To see your current git version run \code{git --version} from your shell. Or from R: \if{html}{\out{
}}\preformatted{system("git --version") }\if{html}{\out{
}} If you need to avoid installing git, see 'Environment variables' below. } \subsection{GitHub}{ \subsection{New setup}{ To set up password-less authentication to GitHub: \enumerate{ \item Create a personal access token (PAT). See https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token. \item Call \code{gitcreds_set()} and give this token as the password. \item Run \code{gitcreds_get(use_cache = FALSE)} to check that the new PAT is set up. To see the token, you can run \code{gitcreds_get(use_cache = FALSE)$password}. } } \subsection{Migrating from the \code{GITHUB_PAT} environment variable}{ If you already have a GitHub token, and use the \code{GITHUB_PAT} or \code{GITHUB_TOKEN} environment variable in your \code{.Renviron} file or elsewhere, no changes are neccessary. gitcreds will automatically use this variable. However, we still suggest that you add your token to the git credential store with \code{gitcreds_set()} and remove \code{GITHUB_PAT} from your \code{.Renviron} file. The credential store is more secure than storing tokens in files, and command line git also uses the credential store for password-less authentication. } } } \section{Advanced topics}{ \subsection{Cached credentials}{ Because querying the git credential store might not be very fast, \code{gitcreds_get()} caches credentials in environment variables by default. Credentials for different URLs are stored in different environment variables. The name of the environment variable is calculated with \code{\link[=gitcreds_cache_envvar]{gitcreds_cache_envvar()}}. To remove the cache, remove this environment variable with \code{\link[=Sys.unsetenv]{Sys.unsetenv()}}. } \subsection{Environment variables}{ If you want to avoid installing git, or using the credential store for some reason, you can supply credentials in environment variables, e.g. via the \code{.Renviron} file. Use \code{\link[=gitcreds_cache_envvar]{gitcreds_cache_envvar()}} to query the environment variable you need to set for a URL: \enumerate{ \item You can set this environment variable to the token or password itself. \item If you also need a user name, then use the \code{user:password} form, i.e. separate them with a colon. (If your user name or passwrd has \code{:} characters, then you need to escape them with a preceding backslash.) } } \subsection{Proxies}{ git should pick up the proxy configuration from the \code{http_proxy}, \code{https_proxy}, and \code{all_proxy} environment variables. To override these, you can set the \code{http.proxy} git configuration key. More info here: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy and here: https://github.com/microsoft/Git-Credential-Manager-Core/blob/master/docs/netconfig.md } \subsection{Credential helpers}{ git credential helpers are an extensible, configurable mechanism to store credentials. Different git installations have different credentials helpers. On Windows the default helper stores credentials in the system credential store. On macOS, it stores them in the macOS Keychain. Other helpers cache credentials in a server process or in a file on the file system. gitcreds only works if a credential helper is configured. For the current git version (2.29.0), this is the case by default on Windows and macOS (for git from HomeBrew), but most Linux distributions do not set up a default credential helper. You can use \code{gitcreds_list_helpers()} to see the \emph{active} credential helper(s) for a repository. Make sure you set the working directory to the git tree before calling \code{gitcreds_list_helpers()}. } \subsection{The current working directory}{ git allows repository specific configuration, via the \code{.git/config} file. The \code{config} file might specify a different credential helper, a different user name, etc. This means that \code{gitcreds_get()} etc. will potentially work differently depending on the current working directory. This is especially relevant for package code that changes the working directory temporarily. } \subsection{Non-GitHub accounts}{ Non-GitHub URLs work mostly the same way as GitHub URLs. \code{gitcreds_get()} and \code{gitcreds_set()} default to GitHub, so you'll need to explicitly set their \code{url} argument. Some credential helpers, e.g. Git Credential Manager for Windows (\code{manager}) and Git Credential Manager Core (\code{manager-core}) work slightly differently for GitHub and non-GitHub URLs, see their documentation for details. } \subsection{Multiple accounts}{ The various credential helpers support having multiple accounts on the same server in different ways. Here are our recommendations. \subsection{macOS}{ \enumerate{ \item Use the (currently default) \code{osxkeychain} credential helper. \item In Keychain Access, remove all your current credentials for the host(s) you are targeting. E.g. for GitHub, search for github.com Internet Passwords. \item Then add the credential that you want to use for "generic access". This is the credential that will be used for URLs without user names. The user name for this credential does not matter, but you can choose something descriptive, e.g. "token", or "generic". \item Configure git to use this username by default. E.g. if you chose "generic", then run \if{html}{\out{
}}\preformatted{git config --global credential.username generic }\if{html}{\out{
}} \item Add all the other credentials, with appropriate user names. These are the user names that you need to put in the URLs for the repositories or operations you want to use them for. (GitHub does not actually use the user names if the password is a PAT, but they are used to look up the correct token in the credential store.) } } \subsection{Windows with git 2.29.0 or later}{ \enumerate{ \item We suggest that you update to the latest git version, but at least 2.29.0, and use the \code{manager-core} helper which is now default. If you installed \code{manager-core} separately from git, we suggest that you remove it, because it might cause confusion as to which helper is actually used. \item Remove all current credentials first, for the host you are targeting. You can do this in 'Credential Manager' or \code{gitcreds::gitcreds_list()} to find them and 'Credential Manager' or the oskeyring package to remove them. You can also use the oskeyring package to back up the tokens and passwords. \item Then add the credential that you want to use for "generic access". This is the credential that will be used for URLs without user names. The user name for this credential does not matter, but you can choose something descriptive, e.g. "PersonalAccessToken", "token", or "generic". \item Configure git to use this username by default. E.g. if you chose "generic", then run \if{html}{\out{
}}\preformatted{git config --global credential.username generic }\if{html}{\out{
}} \item Add all the other credentials, with appropriate user names. These are the user names that you need to put in the URLs for the repositories or operations you want to use them for. (GitHub does not actually use the user names if the password is a PAT, but they are used to look up the correct token from the credential store.) } } \subsection{Windows with older git versions, 2.28.0 and before}{ \subsection{A single GitHub account}{ If you only need to manage a single github.com credential, together with possibly multiple credentials to other hosts (including GitHub Enterprise hosts), then you can use the default \code{manager} helper, and get away with the default auto-detected GCM authority setting. In this case, you can add your github.com credential with an arbitrary user name, and for each other host you can configure a default user name, and/or include user names in the URLs to these hosts. This is how to set a default user name for a host called \verb{https://example.com}: \if{html}{\out{
}}\preformatted{git config --global credential.https://example.com.username myusername }\if{html}{\out{
}} } \subsection{Multiple GitHub credentials}{ If you need to manage multiple github.com credentials, then you can still use the \code{manager} helper, but you need to change the GCM authority by setting an option or an environment variable, see \url{https://github.com/microsoft/Git-Credential-Manager-for-Windows/blob/master/Docs/Configuration.md#authority.} This is how to change GCM authority in the config: \if{html}{\out{
}}\preformatted{git config --global credential.authority Basic }\if{html}{\out{
}} You can also change it only for github.com: \if{html}{\out{
}}\preformatted{git config --global credential.github.com.authority Basic }\if{html}{\out{
}} Then you can configure a default user name, this will be used for URLs without a user name: \if{html}{\out{
}}\preformatted{git config --global credential.username generic }\if{html}{\out{
}} Now you can add you credentials, the default one with the "generic" user name, and all the others with their specific user and host names. Alternatively, you can install a newer version of Git Credential Manager Core (GCM Core), at least version 2.0.252-beta, and use the \code{manager-core} helper. You'll potentially need to delete the older \code{manager-core} helper that came with git itself. With the newer version of GCM Core, you can use the same method as for newer git versions, see above. } } } \subsection{Multiple credential helpers}{ It is possible to configure multiple credential helpers. If multiple helpers are configured for a repository, then \code{gitcreds_get()} will go over them until a credential is found. \code{gitcreds_set()} will try to set the new credentials in \emph{every} configured credential helper. You can use \code{\link[=gitcreds_list_helpers]{gitcreds_list_helpers()}} to list all configured helpers. } } \examples{ \dontrun{ gitcreds_get() gitcreds_get("https://github.com") gitcreds_get("https://myuser@github.com/myorg/myrepo") } } gitcreds/man/gitcreds_cache_envvar.Rd0000644000176200001440000000157714210415267017444 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/aaa-doc.R \name{gitcreds_cache_envvar} \alias{gitcreds_cache_envvar} \title{Environment variable to cache the password for a URL} \usage{ gitcreds_cache_envvar(url) } \arguments{ \item{url}{Character vector of URLs, they may contain user names and paths as well. See details below.} } \value{ Character vector of environment variables. } \description{ \code{gitcreds_get()} caches credentials in environment variables. \code{gitcreds_cache_envvar()} calculates the environment variaable name that is used as the cache, for a URL. } \examples{ gitcreds_cache_envvar("https://github.com") gitcreds_cache_envvar("https://api.github.com/path/to/endpoint") gitcreds_cache_envvar("https://jane@github.com") gitcreds_cache_envvar("https://another.site.github.com") } \seealso{ \code{\link[=gitcreds_get]{gitcreds_get()}}. } gitcreds/man/gitcreds-package.Rd0000644000176200001440000000152514306336044016323 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gitcreds-package.R \docType{package} \name{gitcreds-package} \alias{gitcreds-package} \alias{_PACKAGE} \title{gitcreds: Query 'git' Credentials from 'R'} \description{ Query, set, delete credentials from the 'git' credential store. Manage 'GitHub' tokens and other 'git' credentials. This package is to be used by other packages that need to authenticate to 'GitHub' and/or other 'git' repositories. } \seealso{ Useful links: \itemize{ \item \url{https://gitcreds.r-lib.org/} \item \url{https://github.com/r-lib/gitcreds} \item Report bugs at \url{https://github.com/r-lib/gitcreds/issues} } } \author{ \strong{Maintainer}: Gábor Csárdi \email{csardi.gabor@gmail.com} Other contributors: \itemize{ \item RStudio [copyright holder, funder] } } \keyword{internal} gitcreds/man/gitcreds-api.Rd0000644000176200001440000000246014210415267015477 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/aaa-doc.R \name{gitcreds_fill} \alias{gitcreds_fill} \alias{gitcreds_approve} \alias{gitcreds_reject} \title{Access the low level credential API} \usage{ gitcreds_fill(input, args = character(), dummy = TRUE) gitcreds_approve(creds, args = character()) gitcreds_reject(creds, args = character()) } \arguments{ \item{input}{Named list to pass to \verb{git credential fill}.} \item{args}{Extra args, used \emph{before} \code{fill}, to allow \verb{git -c ... fill}.} \item{dummy}{Whether to append a dummy credential helper to the list of credential helpers.} \item{creds}{\code{gitcreds} object (named list) to add or remove.} } \value{ The standard output of the \code{git} command, line by line. } \description{ These function are primarily for package authors, who want more control over the user interface, so they want to avoid calling \code{\link[=gitcreds_get]{gitcreds_get()}} and \code{\link[=gitcreds_set]{gitcreds_set()}} directly. } \details{ \code{gitcreds_fill()} calls \verb{git credential fill} to query git credentials. \code{gitcreds_approve()} calls \verb{git credential approve} to add new credentials. } \seealso{ \code{\link[=gitcreds_parse_output]{gitcreds_parse_output()}} to parse the output of \code{gitcreds_fill()}. } gitcreds/man/gitcreds_list.Rd0000644000176200001440000001245014210415267015763 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/list-creds.R \name{gitcreds_list} \alias{gitcreds_list} \title{List all credentials stored by a git credential helper} \usage{ gitcreds_list( url = "https://github.com", credential_helper = NULL, protocol = NULL ) } \arguments{ \item{url}{URL to list credentials for. If \code{NULL} then the credentials are listed for all URLs. Note that for a host the results might be different if you specify or omit this argument. \code{gitcreds_list()} uses heuristics when the \code{url} is not specified. If is always best to specify the URL.} \item{credential_helper}{Credential helper to use. If this is \code{NULL}, then the configured credential helper is used. If multiple credential helpers are configured, then the first one is used, with a warning.} \item{protocol}{Protocol to list credentials for. If \code{NULL} and \code{url} includes a protocol then that is used. Otherwise \code{"https"} is used.} } \value{ A list of \code{oskeyring_macos_item} objects. See \code{\link[oskeyring:macos_keychain]{oskeyring::macos_item()}}. } \description{ This function is meant to be used interactively, to help you when configuring credential helpers. It is especially useful if you have multiple accounts on a host. } \details{ Note that this function does not use the credential helper itself, so it does not have to be installed. But it may also give false results, so interpret the results with caution, and also use the tool provided by your OS, to look at the credentials: 'Keychain Access' on macOS and 'Credential Manager' on Windows. Only a small number of credential helpers are supported currently. Here is a brief description of each. \subsection{\code{osxkeychain} on macOS}{ This is the default credential helper on macOS. It has some peculiarities: \itemize{ \item If you don't specify a username in the URL, then it will return the \emph{oldest} credentials that match the specified host name, with an arbitrary user name. \item If the user name is specified in the URL, then it is used to look up the credentials. } To change or delete the listed credentials, see the oskeyring package or the 'Keychain Access' macOS app. } \subsection{\code{manager}, on Windows}{ This is Git Credential Manager for Windows, see https://github.com/microsoft/Git-Credential-Manager-for-Windows It is currently the default helper on Windows, included in the git installer. It has some oddities, especially with multiple GitHub users: \itemize{ \item The \code{github} authority (which is used by default for \code{github.com} URLs) cannot handle multiple users. It always sets the \code{target_name} of the Windows credential to \verb{git:} where \verb{} does not contain the user name. Since \code{target_name} is a primary key, it is not possible to add multiple GitHub users with the default configuration. \item To support multiple users, switch to the \code{Basic} authority, e.g. by setting the \code{GCM_AUTHORITY} env var to \code{Basic}. Then the user name will be included in \code{target_name}, and everything works fine. \item For this helper \code{gitcreds_list()} lists all records with a matching host name. } } \subsection{\code{manager-core} on Windows}{ This is Git Credential Manager Core, see https://github.com/microsoft/Git-Credential-Manager-Core On Windows it behaves almost the same way as \code{manager}, with some differences: \itemize{ \item Instead of \emph{authorities}, it has providers. \code{github.com} URLs use the \code{github} provider by default. For better support for multiple GitHub accounts, switch to the \code{generic} provider by setting the \code{GCM_PROVIDER} env var to \code{generic}. \item \code{gitcreds_list()} will list all credentials with a matching host, irrespectively of the user name in the input URL. } } \subsection{\code{manager-core}, \emph{before} version 2.0.246-beta, on macOS}{ This is Git Credential Manager Core, see https://github.com/microsoft/Git-Credential-Manager-Core This helper has some peculiarities w.r.t. user names: \itemize{ \item If the "github" provider is used (which is the default for \code{github.com} URLs), then it completely ignores user names, even if they are explicitly specified in the query. \item For other providers, the user name (if specified) is saved in the Keychain item. \item For this helper, \code{gitcreds_list()} always lists all records that match the \emph{host}, even if the user name does not match, because it is impossible to tell if the user name would be used in a proper git credential lookup. } To change or delete the listed credentials, see the oskeyring package or the 'Keychain Access' macOS app. } \subsection{\code{manager-core}, version 2.0.246-beta or newer, on macOS}{ This is a newer version of Git Credential Manager Core, that supports multiple users better: \itemize{ \item if a user name is provided, then it saves it in the credential store, and it uses this user name for looking up credentials, even for the \code{github} provider. \item \code{gitcreds_list()} always lists all records that match the host, even if the user name does not match. \item Credentials that were created by an older version of \code{manager-core}, with the \code{generic} provider, do not work with the newer version of \code{manager-core}, because the format of the Keychain item is different. } } } gitcreds/man/gitcreds_parse_output.Rd0000644000176200001440000000121614210415267017540 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/aaa-doc.R \name{gitcreds_parse_output} \alias{gitcreds_parse_output} \title{Parse standard output from \verb{git credential fill}} \usage{ gitcreds_parse_output(txt, url) } \arguments{ \item{txt}{Character vector, standard output lines from \verb{git credential fill}.} \item{url}{URL we queried, to be able to create a better error message.} } \value{ \code{gitcreds} object. } \description{ Parse standard output from \verb{git credential fill} } \details{ For dummy credentials (i.e. the lack of credentials), it throws an error of class \code{gitcreds_no_credentials}. } gitcreds/DESCRIPTION0000644000176200001440000000216614306343457013602 0ustar liggesusersPackage: gitcreds Title: Query 'git' Credentials from 'R' Version: 0.1.2 Authors@R: c( person("Gábor", "Csárdi", , "csardi.gabor@gmail.com", role = c("aut", "cre")), person("RStudio", role = c("cph", "fnd")) ) Description: Query, set, delete credentials from the 'git' credential store. Manage 'GitHub' tokens and other 'git' credentials. This package is to be used by other packages that need to authenticate to 'GitHub' and/or other 'git' repositories. License: MIT + file LICENSE URL: https://gitcreds.r-lib.org/, https://github.com/r-lib/gitcreds BugReports: https://github.com/r-lib/gitcreds/issues Depends: R (>= 3.4) Suggests: codetools, covr, knitr, mockery, oskeyring, rmarkdown, testthat (>= 3.0.0), withr VignetteBuilder: knitr Config/Needs/website: tidyverse/tidytemplate Encoding: UTF-8 RoxygenNote: 7.2.1.9000 SystemRequirements: git Config/testthat/edition: 3 NeedsCompilation: no Packaged: 2022-09-08 10:28:07 UTC; gaborcsardi Author: Gábor Csárdi [aut, cre], RStudio [cph, fnd] Maintainer: Gábor Csárdi Repository: CRAN Date/Publication: 2022-09-08 10:42:55 UTC gitcreds/build/0000755000176200001440000000000014306341667013167 5ustar liggesusersgitcreds/build/vignette.rds0000644000176200001440000000036714306341667015534 0ustar liggesusersuP0t{DE _hhkd!!ǀr&Y#D=ffwG1\s=,9>I4YZUgu[b"ƥN-d,摂r ɻKdBqY'jM3z3Ȉ9b,b3o/`-"OI%>կ[otvT PB?nv-@m1utIQGBpa۝Sgitcreds/tests/0000755000176200001440000000000014210415267013222 5ustar liggesusersgitcreds/tests/testthat/0000755000176200001440000000000014306343457015071 5ustar liggesusersgitcreds/tests/testthat/test-standalone.R0000644000176200001440000000062514212610163020307 0ustar liggesusers test_that("gitcreds is standalone", { stenv <- environment(gitcreds_get) objs <- ls(stenv, all.names = TRUE) funs <- Filter(function(x) is.function(stenv[[x]]), objs) funobjs <- mget(funs, stenv) for (f in funobjs) expect_identical(environmentName(topenv(f)), "base") expect_message( mapply(codetools::checkUsage, funobjs, funs, MoreArgs = list(report = message)), NA) }) gitcreds/tests/testthat/test-gitcreds-list.R0000644000176200001440000001372514214106402020737 0ustar liggesusers gc_test_that("gitcreds_list", { # no helper local_helpers(character()) expect_error( gitcreds_list(), class = "gitcreds_no_helper" ) # unknown helper local_helpers(basename(tempfile())) expect_error( gitcreds_list(), class = "gitcreds_unknown_helper" ) # the right helper function is chosen mockery::stub(gitcreds_list, "gitcreds_list_osxkeychain", "osx") mockery::stub(gitcreds_list, "gitcreds_list_manager", "gcm") mockery::stub(gitcreds_list, "gitcreds_list_manager_core", "gcmcore") expect_equal(gitcreds_list(credential_helper = "osxkeychain"), "osx") expect_equal(gitcreds_list(credential_helper = "manager"), "gcm") expect_equal(gitcreds_list(credential_helper = "manager-core"), "gcmcore") # warn for multiple helpers local_helpers(c("foo", "bar")) mockery::stub(gitcreds_list, "switch", NULL) expect_warning( gitcreds_list(), class = "gitcreds_multiple_helpers" ) }) gc_test_that("gitcreds_list_osxkeychain", os = "macos", helpers = "osxkeychain", { # needs oskeyring fun <- function() { mockery::stub(gitcreds_list_osxkeychain, "requireNamespace", FALSE) gitcreds_list_osxkeychain() } expect_error( fun(), "needs the `oskeyring` package" ) # no credentials just yet expect_equal(gitcreds_list(), list()) cred <- list( url = "https://github.com", username = "PersonalAccessToken", password = "secret" ) gitcreds_approve(cred) lst <- gitcreds_list() expect_equal(length(lst), 1L) expect_equal(lst[[1]]$attributes$server, "github.com") lst2 <- gitcreds_list(url = NULL) expect_true(length(lst2) >= 1) expect_true("github.com" %in% vapply(lst2, function(it) it$attributes$server, "")) cred2 <- list( url = "https://github2.com", username = "user", password = "secret2" ) gitcreds_approve(cred2) lst3 <- gitcreds_list() expect_equal(length(lst3), 1L) expect_equal(lst3[[1]]$attributes$server, "github.com") lst4 <- gitcreds_list("https://github2.com") expect_equal(length(lst4), 1L) expect_equal(lst4[[1]]$attributes$server, "github2.com") lst5 <- gitcreds_list(url = NULL) expect_true(length(lst5) >= 2) expect_true("github.com" %in% vapply(lst5, function(it) it$attributes$server, "")) expect_true("github2.com" %in% vapply(lst5, function(it) it$attributes$server, "")) }) gc_test_that("gitcreds_list_manager_core", os = c("windows", "macos"), { mockery::stub(gitcreds_list_manager_core, "gitcreds_list_manager_core_macos", "macos") mockery::stub(gitcreds_list_manager_core, "gitcreds_list_manager_core_win", "win") mockery::stub(gitcreds_list_manager_core, "gitcreds$get_os", "windows") expect_equal(gitcreds_list_manager_core(), "win") mockery::stub(gitcreds_list_manager_core, "gitcreds$get_os", "macos") expect_equal(gitcreds_list_manager_core(), "macos") mockery::stub(gitcreds_list_manager_core, "gitcreds$get_os", "linux") expect_error( gitcreds_list_manager_core(), "Unsupported OS" ) }) gc_test_that("gitcreds_list_manager_core_macos", os = "macos", helper = "manager-core", { # needs oskeyring fun <- function() { mockery::stub(gitcreds_list_manager_core_macos, "requireNamespace", FALSE) gitcreds_list_manager_core_macos() } expect_error( fun(), "needs the `oskeyring` package" ) # no credentials just yet expect_equal(gitcreds_list(), list()) cred <- list( url = "https://github.com", username = "PersonalAccessToken", password = "secret" ) gitcreds_approve(cred) lst <- gitcreds_list() expect_equal(length(lst), 1L) expect_equal(lst[[1]]$attributes$service, "git:https://github.com") lst2 <- gitcreds_list(url = NULL) expect_true(length(lst2) == 1) expect_equal(lst2[[1]]$attributes$service, "git:https://github.com") cred2 <- list( url = "https://github2.com", username = "user", password = "secret2" ) gitcreds_approve(cred2) lst3 <- gitcreds_list() expect_equal(length(lst3), 1L) expect_equal(lst3[[1]]$attributes$service, "git:https://github.com") lst4 <- gitcreds_list("https://github2.com") expect_equal(length(lst4), 1L) expect_equal(lst4[[1]]$attributes$service, "git:https://github2.com") lst5 <- gitcreds_list(url = NULL) expect_true(length(lst5) >= 2) expect_true( "git:https://github.com" %in% vapply(lst5, function(it) it$attributes$service, "") ) expect_true( "git:https://github2.com" %in% vapply(lst5, function(it) it$attributes$service, "") ) }) gc_test_that("gitcreds_list_manager_core_win", os = "windows", helper = "manager-core", { if (packageVersion("oskeyring") <= "0.1.0") skip("Needs newer oskeyring") # needs oskeyring fun <- function() { mockery::stub(gitcreds_list_manager_core_macos, "requireNamespace", FALSE) gitcreds_list_manager_core_macos() } expect_error( fun(), "needs the `oskeyring` package" ) # no credentials just yet expect_equal(gitcreds_list(), list()) cred <- list( url = "https://github.com", username = "PersonalAccessToken", password = "secret" ) gitcreds_approve(cred) lst <- gitcreds_list() expect_equal(length(lst), 1L) expect_equal(lst[[1]]$target_name, "git:https://github.com") lst2 <- gitcreds_list(url = NULL) expect_true(length(lst2) == 1) expect_equal(lst2[[1]]$target_name, "git:https://github.com") cred2 <- list( url = "https://github2.com", username = "user", password = "secret2" ) gitcreds_approve(cred2) lst3 <- gitcreds_list() expect_equal(length(lst3), 1L) expect_equal(lst3[[1]]$target_name, "git:https://github.com") lst4 <- gitcreds_list("https://github2.com") expect_equal(length(lst4), 1L) expect_equal(lst4[[1]]$target_name, "git:https://github2.com") lst5 <- gitcreds_list(url = NULL) expect_true(length(lst5) >= 2) expect_true( "git:https://github.com" %in% vapply(lst5, function(it) it$target_name, "") ) expect_true( "git:https://github2.com" %in% vapply(lst5, function(it) it$target_name, "") ) }) gitcreds/tests/testthat/test-gitcreds-set.R0000644000176200001440000000622614210415267020565 0ustar liggesusers gc_test_that("gitcreds_set_new", os = c("windows", "macos"), { mockery::stub(gitcreds$gitcreds_set_new, "readline", "new-secret") mockery::stub(gitcreds$gitcreds_set_new, "cat", NULL) gitcreds$gitcreds_set_new("https://github.com") cred <- gitcreds_get(set_cache = FALSE) expect_equal(cred$host, "github.com") expect_equal(cred$password, "new-secret") }) gc_test_that("gitcreds_set_replace", os = c("windows", "macos"), { cred <- list( url = "https://github.com", username = "PersonalAccessToken", password = "secret" ) gitcreds_approve(cred) mockery::stub(gitcreds$gitcreds_set_replace, "ack", FALSE) mockery::stub(gitcreds$gitcreds_set_replace, "readline", "new-secret-2") mockery::stub(gitcreds$gitcreds_set_replace, "cat", NULL) expect_error( gitcreds$gitcreds_set_replace("https://github.com", gitcreds_get()), class = "gitcreds_abort_replace_error" ) mockery::stub(gitcreds$gitcreds_set_replace, "ack", TRUE) gitcreds$gitcreds_set_replace("https://github.com", gitcreds_get()) cred <- gitcreds_get(use_cache = FALSE) expect_equal(cred$host, "github.com") expect_equal(cred$password, "new-secret-2") }) gc_test_that("gitcreds_set", { # fails if not interactive mockery::stub(gitcreds_set, "is_interactive", FALSE) expect_error( gitcreds_set(), class = "gitcreds_not_interactive_error" ) called <- NULL mockery::stub(gitcreds_set, "is_interactive", TRUE) mockery::stub(gitcreds_set, "gitcreds_set_replace", function(...) { called <<- "gitcreds_set_replace" }) mockery::stub(gitcreds_set, "gitcreds_set_new", function(...) { called <<- "gitcreds_set_new" }) # calls set_new if none called <- NULL gitcreds_set() expect_equal(called, "gitcreds_set_new") # calls set_replace if there is one cred <- list( url = "https://github.com", username = "PersonalAccessToken", password = "secret" ) gitcreds_approve(cred) called <- NULL gitcreds_set() expect_equal(called, "gitcreds_set_replace") # does not use the cache gitcreds_get() gitcreds_reject(cred) called <- NULL gitcreds_set() expect_equal(called, "gitcreds_set_new") # deletes the cache expect_null(gitcreds$gitcreds_get_cache( gitcreds_cache_envvar("https://github.com") )) }) gc_test_that("multiple matching credentials", { cred <- list( url = "https://github.com", username = "PersonalAccessToken", password = "secret" ) gitcreds_approve(cred) cred2 <- list( url = "https://github.com", username = "PersonalAccessToken2", password = "secret2" ) gitcreds_approve(cred2) mockery::stub(gitcreds$gitcreds_set_replace, "ack", FALSE) mockery::stub(gitcreds$gitcreds_set_replace, "readline", "new-secret-2") mockery::stub(gitcreds$gitcreds_set_replace, "cat", NULL) expect_error( gitcreds$gitcreds_set_replace("https://github.com", gitcreds_get()), class = "gitcreds_abort_replace_error" ) mockery::stub(gitcreds$gitcreds_set_replace, "ack", TRUE) gitcreds$gitcreds_set_replace("https://github.com", gitcreds_get()) cred <- gitcreds_get(use_cache = FALSE) expect_equal(cred$host, "github.com") expect_equal(cred$password, "new-secret-2") }) gitcreds/tests/testthat/test-gitcreds-get.R0000644000176200001440000000203514212610163020535 0ustar liggesusers gc_test_that("gitcreds_get", os = c("windows", "macos"), { cred <- list( url = "https://github.com", username = "PersonalAccessToken", password = "secret" ) gitcreds_approve(cred) chk <- function(cr) { expect_s3_class(cr, "gitcreds") expect_equal(cr$protocol, "https") expect_equal(cr$host, "github.com") expect_equal(cr$username, "PersonalAccessToken") expect_equal(cr$password, "secret") } cred2 <- gitcreds_get() chk(cred2) # cache is set gitcreds_reject(cred) cred3 <- gitcreds_get() chk(cred3) # use_cache is FALSE expect_error( gitcreds_get(use_cache = FALSE), class = "gitcreds_no_credentials" ) # set_cache is FALSE gitcreds$gitcreds_delete_cache(gitcreds_cache_envvar("https://github.com")) expect_null(gitcreds$gitcreds_get_cache( gitcreds_cache_envvar("https://github.com") )) gitcreds_approve(cred) cred4 <- gitcreds_get(set_cache = FALSE) chk(cred4) expect_null(gitcreds$gitcreds_get_cache( gitcreds_cache_envvar("https://github.com") )) }) gitcreds/tests/testthat/test-username.R0000644000176200001440000000337114210415267020005 0ustar liggesusers gc_test_that("gitcreds_username_for_url", { tmp <- withr::local_tempdir() withr::local_dir(tmp) gitcreds$git_run(c("init", ".")) gitcreds$git_run(c("config", "credential.username", "global")) gitcreds$git_run(c("config", "credential.https://example.com.username", "spec")) expect_equal(gitcreds$gitcreds_username_for_url("https://example.com"), "spec") expect_equal(gitcreds$gitcreds_username_for_url("https://foo.com"), "global") expect_equal(gitcreds$gitcreds_username(), "global") expect_equal(gitcreds$gitcreds_username("https://foo.com"), "global") expect_equal(gitcreds$gitcreds_username("https://example.com"), "spec") }) gc_test_that("gitcreds$gitcreds_username_generic", { tmp <- withr::local_tempdir() withr::local_dir(tmp) gitcreds$git_run(c("init", ".")) gitcreds$git_run(c("config", "credential.username", "global")) gitcreds$git_run(c("config", "credential.https://example.com.username", "spec")) expect_equal(gitcreds$gitcreds_username_generic(), "global") }) gc_test_that("errors", { mock <- function(args, ...) { args[1] <- basename(tempfile()) gitcreds:::gitcreds$git_run(args, ...) } mockery::stub(gitcreds$gitcreds_username_for_url, "git_run", mock) mockery::stub(gitcreds$gitcreds_username_generic, "git_run", mock) expect_null(gitcreds$gitcreds_username_for_url("https://github.com")) expect_null(gitcreds$gitcreds_username_generic()) mock2 <- function(...) { gitcreds:::gitcreds$git_run(c("config", "--unset", "xxxxxx.yyyyy")) } mockery::stub(gitcreds$gitcreds_username_for_url, "git_run", mock2) mockery::stub(gitcreds$gitcreds_username_generic, "git_run", mock2) expect_error(gitcreds$gitcreds_username_for_url("https://github.com")) expect_error(gitcreds$gitcreds_username_generic()) }) gitcreds/tests/testthat/test-list-helpers.R0000644000176200001440000000063014210415267020574 0ustar liggesusers gc_test_that("gitcreds_list_helpers", os = c("windows", "macos"), { # no helpers at all local_helpers(character()) expect_equal(gitcreds_list_helpers(), character()) # custom helpers local_helpers(c("foo", "bar")) expect_equal(gitcreds_list_helpers(), c("foo", "bar")) # resetting helpers local_helpers(c("reset", "\"\"", "foobar")) expect_equal(gitcreds_list_helpers(), c("foobar")) }) gitcreds/tests/testthat/test-gitcreds-delete.R0000644000176200001440000000167014212623233021226 0ustar liggesusers gc_test_that("gitcreds_delete", { # fails if not interactive mockery::stub(gitcreds_delete, "is_interactive", FALSE) expect_error( gitcreds_delete(), class = "gitcreds_not_interactive_error" ) mockery::stub(gitcreds_delete, "is_interactive", TRUE) # FALSE if nothing was deleted expect_false(gitcreds_delete()) # can be aborted cred <- list( url = "https://github.com", username = "PersonalAccessToken", password = "secret" ) gitcreds_approve(cred) gitcreds_get() mockery::stub(gitcreds_delete, "ack", FALSE) expect_error( gitcreds_delete(), class = "gitcreds_abort_delete_error" ) # really deletes mockery::stub(gitcreds_delete, "ack", TRUE) gitcreds_delete() expect_error( gitcreds_get(use_cache = FALSE), class = "gitcreds_no_credentials" ) # deletes cache as well expect_null(gitcreds$gitcreds_get_cache( gitcreds_cache_envvar("https://github.com") )) }) gitcreds/tests/testthat/test-format.R0000644000176200001440000000164714210415267017462 0ustar liggesusers test_that("format.gitcreds", { cred <- gitcreds$new_gitcreds( protocol = "https", host = "github.com", username = "user", password = "secret", arbitrary = "xxxxxxxxx" ) fmt <- format(cred) expect_true(any(grepl("arbitrary", fmt, fixed = TRUE))) expect_true(any(grepl("xxxxxx", fmt, fixed = TRUE))) expect_false(any(grepl("secret", fmt, fixed = TRUE))) expect_true(any(grepl("hidden", fmt, fixed = TRUE))) }) test_that("print.gitcreds", { cred <- gitcreds$new_gitcreds( protocol = "https", host = "github.com", username = "user", password = "secret", arbitrary = "xxxxxxxxx" ) out <- capture.output(ret <- print(cred)) expect_equal(ret, cred) expect_true(any(grepl("arbitrary", out, fixed = TRUE))) expect_true(any(grepl("xxxxxx", out, fixed = TRUE))) expect_false(any(grepl("secret", out, fixed = TRUE))) expect_true(any(grepl("hidden", out, fixed = TRUE))) }) gitcreds/tests/testthat/helper.R0000644000176200001440000000773714214106402016472 0ustar liggesusers is_ci <- function() { Sys.getenv("CI", "") != "" } gc_test_that <- function(desc, code, os = NULL, helpers = NULL) { if (!is_ci()) return() if (!is.null(os)) { if (! gitcreds$get_os() %in% os) return() } if (is.null(helpers)) { os <- gitcreds$get_os() if (os == "windows") { helpers <- c("manager-core") } else if (os == "macos") { helpers <- c("osxkeychain", "manager-core") } else { helpers <- character() } } # Need to create a closure here, because mockery will create the # stubs in that environment code <- substitute(code) lapply(helpers, function(helper) { local_helpers(helper) withr::local_envvar(GITCREDS_TEST_HELPER = helper) label <- paste0(desc, " [", helper, "]") test <- substitute(gc_test_that_run(label, code), list(code = code)) eval(test) }) } gc_test_that_run <- function(desc, code) { if (interactive() && !isTRUE(getOption("gitcreds_test_consent"))) { ch <- utils::menu( title = paste0("\n\nThis testsuite will delete your ", "git credentials and config !!!"), choices = c("Yeah, fine.", "Wait, what?") ) if (ch == 2) { cat("\nAborting...\n"); invokeRestart("abort") } options(gitcreds_test_consent = TRUE) } if (gitcreds$get_os() == "windows") { cleanup_windows() on.exit(cleanup_windows(), add = TRUE) } if (gitcreds$get_os() == "macos") { cleanup_macos() on.exit(cleanup_macos(), add = TRUE) } envnames <- grep("^GITHUB_PAT", names(Sys.getenv()), value = TRUE) envs <- structure(rep(NA_character_, length(envnames)), names = envnames) withr::local_envvar(c( envs, GCM_AUTHORITY = NA_character_, GCM_PROVIDER = NA_character_ )) test_that(desc, { code }) } cleanup_windows <- function() { cleanup_windows_manager() cleanup_windows_manager_core() } cleanup_windows_manager <- function() { recs <- gitcreds_list(credential_helper = "manager", url = NULL) for (rec in recs) { try_silently({ cred <- list( url = sub("^git:", "", rec$target_name), username = rec$username ) gitcreds_reject(cred, c("-c", "credential.helper=manager")) }) } } cleanup_windows_manager_core <- function() { recs <- gitcreds_list(credential_helper = "manager-core", url = NULL) for (rec in recs) { try_silently({ cred <- list( url = sub("^git:", "", rec$target_name), username = rec$username ) gitcreds_reject(cred, c("-c", "credential.helper=manager-core")) }) } } cleanup_macos <- function() { cleanup_macos_osxkeychain() cleanup_macos_manager_core() } try_silently <- function(expr) { try(expr, silent = TRUE) } cleanup_macos_osxkeychain <- function() { recs <- gitcreds_list(credential_helper = "osxkeychain", url = NULL) for (rec in recs) { try_silently({ cred <- list( host = rec$attributes$server, protocol = rec$attributes$protocol, username = rec$attributes$account ) gitcreds_reject(cred, c("-c", "credential.helper=osxkeychain")) }) } } cleanup_macos_manager_core <- function() { recs <- gitcreds_list(credential_helper = "manager-core", url = NULL) for (rec in recs) { try_silently({ cred <- list( url = sub("^git:", "", rec$attributes$service), username = rec$attributes$account ) gitcreds_reject(cred, c("-c", "credential.helper=manager-core")) }) } } clear_helpers <- function() { # Might return startus 5 if no helpers are set try_silently(git_run(c("config", "--global", "--unset-all", "credential.helper"))) try_silently(git_run(c("config", "--global", "--remove-section", "credential"))) } local_helpers <- function(helpers, .local_envir = parent.frame()) { withr::defer(clear_helpers(), envir = .local_envir) clear_helpers() gitcreds$git_run(c("config", "--global", "--add", "credential.helper", "\"\"")) for (helper in helpers) { gitcreds$git_run(c("config", "--global", "--add", "credential.helper", helper)) } } gitcreds/tests/testthat/test-utils.R0000644000176200001440000001373314212610163017323 0ustar liggesusers test_that("set_env() round trip works with just 1 env var", { withr::local_envvar(c(FOO = NA_character_)) oenv <- gitcreds$set_env(c(FOO = "abc")) expect_equal(Sys.getenv("FOO"), "abc") gitcreds$set_env(oenv) expect_equal(Sys.getenv("FOO", "unset"), "unset") }) test_that("set_env() NA unsets", { withr::local_envvar(c(FOO = NA_character_)) Sys.setenv(FOO = "bar") gitcreds$set_env(c(FOO = NA_character_)) expect_equal(Sys.getenv("FOO", ""), "") }) test_that("env var set to empty string is same as unset", { skip_on_os("windows") withr::local_envvar(c(GITHUB_PAT_GITHUB_ACME_COM = NA_character_)) oenv <- gitcreds$set_env(c(GITHUB_PAT_GITHUB_ACME_COM = "")) on.exit(gitcreds$set_env(oenv), add = TRUE) x <- gitcreds$gitcreds_get_cache("GITHUB_PAT_GITHUB_ACME_COM") expect_null(x) }) test_that("ack", { cred <- gitcreds$new_gitcreds( protocol = "https", host = "github.com", username = "user", password = "secret", arbitrary = "xxxxxxxxx" ) mockery::stub(gitcreds$ack, "utils::menu", 1) expect_false(gitcreds$ack("https://github.com", cred)) mockery::stub(gitcreds$ack, "utils::menu", 2) expect_true(gitcreds$ack("https://github.com", cred)) num <- 0L mockery::stub(gitcreds$ack, "utils::menu", function(...) { num <<- num + 1L if (num == 1L) 3 else 1 }) mm <- NULL withCallingHandlers( ret <- gitcreds$ack("https://github.com", cred), message = function(m) mm <<- m ) expect_equal(num, 2) expect_false(ret) expect_true(any(grepl("Current password", mm$message))) }) gc_test_that("check_for_git", { expect_silent(gitcreds$check_for_git()) mockery::stub(gitcreds$check_for_git, "system2", function(command, ...) { base::system2(basename(tempfile()), ...) }) expect_error( gitcreds$check_for_git(), class = "gitcreds_nogit_error" ) }) gc_test_that("get_os", os = "macos", helpers = "osxkeychain", { mockery::stub(gitcreds$get_os, "Sys.info", c(sysname = "foobar")) expect_equal(gitcreds$get_os(), "unknown") }) test_that("get_url_username", { expect_equal(gitcreds$get_url_username("https://user1@host.com"), "user1") expect_null(gitcreds$get_url_username("https://host.com")) }) test_that("throw", { err <- gitcreds$new_error("myerror", message = "boo") expect_error(gitcreds$throw(err), class = "myerror") wrn <- gitcreds$new_warning("mywarning", message = "ouch") expect_warning(gitcreds$throw(wrn), class = "mywarning") msg <- structure( list(message = "hey"), class = c("mymessage", "message", "condition") ) expect_message(gitcreds$throw(msg), class = "mymessage") cnd <- structure( list(message = "this"), class = c("mycondition", "condition") ) expect_condition(gitcreds$throw(cnd), class = "mycondition") }) test_that("null_file", { expect_silent(writeLines(letters, gitcreds$null_file())) expect_equal(readLines(gitcreds$null_file()), character()) }) test_that("msg", { # testthat is catching all messages, so we need to change the # class to test this mockery::stub(gitcreds$msg, "simpleMessage", function(message, call = NULL) { structure( list(message = message, call = call), class = c("mymessage", "condition") ) }) # not interactive, we have a sink, goes to stderr out <- capture.output(gitcreds$msg("foo", "bar"), type = "message") expect_equal(out, "foobar") # no output if we catch it mm <- NULL out <- capture.output( tryCatch(gitcreds$msg("foo", "bar"), mymessage = function(m) mm <<- m) ) expect_equal(out, character()) expect_true(mm$message %in% c("foobar\n", "foobar\r\n")) }) test_that("default_output", { mockery::stub(gitcreds$default_output, "is_interactive", TRUE) expect_equal(gitcreds$default_output(), stdout()) expect_equal( withr::with_output_sink(tempfile(), gitcreds$default_output()), stderr() ) expect_equal( withr::with_message_sink(tempfile(), gitcreds$default_output()), stderr() ) mockery::stub(gitcreds$default_output, "is_interactive", FALSE) expect_equal(gitcreds$default_output(), stderr()) expect_equal( withr::with_output_sink(tempfile(), gitcreds$default_output()), stderr() ) expect_equal( withr::with_message_sink(tempfile(), gitcreds$default_output()), stderr() ) }) test_that("is_interactive", { withr::with_options( list(rlib_interactive = TRUE, rlang_interactive = NULL), expect_true(gitcreds$is_interactive()) ) withr::with_options( list(rlib_interactive = FALSE, rlang_interactive = NULL), expect_false(gitcreds$is_interactive()) ) withr::with_options( list(rlib_interactive = NULL, rlang_interactive = TRUE), expect_true(gitcreds$is_interactive()) ) withr::with_options( list(rlib_interactive = NULL, rlang_interactive = FALSE), expect_false(gitcreds$is_interactive()) ) withr::with_options( list(rlib_interactive = TRUE, rlang_interactive = FALSE), expect_true(gitcreds$is_interactive()) ) withr::with_options( list(rlib_interactive = FALSE, rlang_interactive = TRUE), expect_false(gitcreds$is_interactive()) ) withr::with_options( list(rlib_interactive = NULL, rlang_interactive = NULL, knitr.in.progress = TRUE), expect_false(gitcreds$is_interactive()) ) withr::with_options( list(rlib_interactive = NULL, rlang_interactive = NULL, knitr.in.progress = NULL), withr::with_envvar( c(TESTTHAT = "true"), expect_false(gitcreds$is_interactive()) ) ) mockery::stub(gitcreds$is_interactive, "base::interactive", "yes") withr::with_options( list(rlib_interactive = NULL, rlang_interactive = NULL, knitr.in.progress = NULL), withr::with_envvar( c(TESTTHAT = NA_character_), expect_equal(gitcreds$is_interactive(), "yes") ) ) }) gc_test_that("git_run", { err <- tryCatch(gitcreds$git_run("qwertyzxcvbn"), error = function(x) x) expect_s3_class(err, "git_error") expect_s3_class(err, "gitcreds_error") expect_match(conditionMessage(err), "System git failed:.*qwertyzxcvbn") }) gitcreds/tests/testthat/test-cache.R0000644000176200001440000000556014210415267017233 0ustar liggesusers gc_test_that("gitcreds_cache_envvvar", { cases <- list( c("https://github.com", "GITHUB_PAT_GITHUB_COM"), c("https://api.github.com/path/to/endpoint", "GITHUB_PAT_GITHUB_COM"), c("https://jane@github.com", "GITHUB_PAT_JANE_AT_GITHUB_COM"), c("https://another.site.github.com", "GITHUB_PAT_ANOTHER_SITE_GITHUB_COM"), c("http://foo.bar", "GITHUB_PAT_FOO_BAR") ) for (case in cases) { expect_equal(gitcreds_cache_envvar(case[[1]]), case[[2]]) } # vectorized expect_equal( gitcreds_cache_envvar(vapply(cases, "[[", character(1), 1)), vapply(cases, "[[", character(1), 2) ) # error expect_error(gitcreds_cache_envvar("foo.bar"), "Invalid URL") }) gc_test_that("gitcreds_get_cache", { # single password withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "token")) cred <- gitcreds$gitcreds_get_cache("GITHUB_PAT_GITHUB_COM") expect_s3_class(cred, "gitcreds") expect_equal(cred$password, "token") # username + password withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "user:pass")) cred <- gitcreds$gitcreds_get_cache("GITHUB_PAT_GITHUB_COM") expect_s3_class(cred, "gitcreds") expect_equal(cred$username, "user") expect_equal(cred$password, "pass") # fall back to GITHUB_PAT withr::local_envvar(c( GITHUB_PAT_GITHUB_COM = NA_character_, GITHUB_PAT = "mytoken" )) cred <- gitcreds$gitcreds_get_cache("GITHUB_PAT_GITHUB_COM") expect_s3_class(cred, "gitcreds") expect_equal(cred$password, "mytoken") # fall back to GITHUB_TOKEN withr::local_envvar(c( GITHUB_PAT_GITHUB_COM = NA_character_, GITHUB_PAT = NA_character_, GITHUB_TOKEN = "mytoken3" )) cred <- gitcreds$gitcreds_get_cache("GITHUB_PAT_GITHUB_COM") expect_s3_class(cred, "gitcreds") expect_equal(cred$password, "mytoken3") # Not set withr::local_envvar(c( GITHUB_PAT_GITHUB_COM = NA_character_, GITHUB_PAT = NA_character_, GITHUB_TOKEN = NA_character_ )) expect_null(gitcreds$gitcreds_get_cache("GITHUB_PAT_GITHUB_COM")) # Warn for invalid withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "what:is:this")) expect_warning( expect_null(gitcreds$gitcreds_get_cache("GITHUB_PAT_GITHUB_COM")), "Invalid gitcreds credentials in env var" ) # fails if it has to withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "FAIL")) expect_error( gitcreds$gitcreds_get_cache("GITHUB_PAT_GITHUB_COM"), class = "gitcreds_no_credentials" ) withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "FAIL:gitcreds_no_helper")) expect_error( gitcreds$gitcreds_get_cache("GITHUB_PAT_GITHUB_COM"), class = "gitcreds_no_helper" ) }) gc_test_that("gitcreds_set_cache", { # : is escaped gitcreds$gitcreds_set_cache("GITHUB_PAT_GITHUB_COM", list("x:y" = "a:b")) cred <- gitcreds$gitcreds_get_cache("GITHUB_PAT_GITHUB_COM") expect_s3_class(cred, "gitcreds") expect_equal(cred$username, "x:y") expect_equal(cred$password, "a:b") }) gitcreds/tests/testthat.R0000644000176200001440000000012314210415267015201 0ustar liggesuserslibrary(testthat) library(gitcreds) test_check("gitcreds", reporter = "progress") gitcreds/vignettes/0000755000176200001440000000000014306341667014100 5ustar liggesusersgitcreds/vignettes/helper-survey.Rmd0000644000176200001440000011277314214106402017351 0ustar liggesusers--- title: "git credential helpers" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{git credential helpers} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} editor_options: markdown: wrap: sentence canonical: true --- # General remarks - For `git credential approve` git (2.28.0 macOS) does not even call the credential helper if no username is supplied: ``` {.sh} ❯ export GIT_TRACE=true ❯ (echo url=https://github.com; echo password=secret; echo ) | git credential approve 10:43:36.712290 git.c:444 trace: built-in: git credential approve ❯ ``` - The `credential.helper` key has a multi-set value, so if you add a new value, the old values are still kept. From git 2.9 specifying an empty string removes the previously defined helpers. # Credential helper survey We do this with an eye of supporting usernames and multiple users. ## `cache` - Docs: - This helper is not included in the default git installer on Windows . :( - This is how we can set up `cache` for a particular repository: ❯ mkdir foo ❯ cd foo ❯ git init . 11:43:28.618841 git.c:444 trace: built-in: git init . Initialized empty Git repository in /private/tmp/foo/.git/ ❯ git config --add credential.helper "" 11:43:50.682962 git.c:444 trace: built-in: git config --add credential.helper '' ❯ git config --add credential.helper cache 11:43:54.577707 git.c:444 trace: built-in: git config --add credential.helper cache ❯ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true [credential] helper = helper = cache - Now let's add credentials: ❯ (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve 11:45:16.813913 git.c:444 trace: built-in: git credential approve 11:45:16.814431 run-command.c:663 trace: run_command: 'git credential-cache store' 11:45:16.823008 git.c:704 trace: exec: git-credential-cache store 11:45:16.823637 run-command.c:663 trace: run_command: git-credential-cache store 11:45:16.842902 run-command.c:663 trace: run_command: git-credential-cache--daemon /Users/gaborcsardi/.cache/git/credential/socket ❯ (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve 11:45:28.927712 git.c:444 trace: built-in: git credential approve 11:45:28.928108 run-command.c:663 trace: run_command: 'git credential-cache store' 11:45:28.937087 git.c:704 trace: exec: git-credential-cache store 11:45:28.937695 run-command.c:663 trace: run_command: git-credential-cache store - Query with username works correctly: ❯ (echo url=https://token@github.com; echo ) | git credential fill 11:46:40.689122 git.c:444 trace: built-in: git credential fill 11:46:40.689638 run-command.c:663 trace: run_command: 'git credential-cache get' 11:46:40.696784 git.c:704 trace: exec: git-credential-cache get 11:46:40.697333 run-command.c:663 trace: run_command: git-credential-cache get protocol=https host=github.com username=token password=secret ❯ (echo url=https://token2@github.com; echo ) | git credential fill 11:46:43.767002 git.c:444 trace: built-in: git credential fill 11:46:43.767676 run-command.c:663 trace: run_command: 'git credential-cache get' 11:46:43.778637 git.c:704 trace: exec: git-credential-cache get 11:46:43.779201 run-command.c:663 trace: run_command: git-credential-cache get protocol=https host=github.com username=token2 password=secret2 - Query without username works, and returns *some* credential: ❯ (echo url=https://github.com; echo ) | git credential fill 11:45:58.200272 git.c:444 trace: built-in: git credential fill 11:45:58.200667 run-command.c:663 trace: run_command: 'git credential-cache get' 11:45:58.208372 git.c:704 trace: exec: git-credential-cache get 11:45:58.208919 run-command.c:663 trace: run_command: git-credential-cache get protocol=https host=github.com username=token password=secret ❯ (echo url=https://token@github.com; echo ) | git credential reject 11:47:03.921697 git.c:444 trace: built-in: git credential reject 11:47:03.922530 run-command.c:663 trace: run_command: 'git credential-cache erase' 11:47:03.935858 git.c:704 trace: exec: git-credential-cache erase 11:47:03.936400 run-command.c:663 trace: run_command: git-credential-cache erase ❯ (echo url=https://github.com; echo ) | git credential fill 11:47:10.018877 git.c:444 trace: built-in: git credential fill 11:47:10.019386 run-command.c:663 trace: run_command: 'git credential-cache get' 11:47:10.027990 git.c:704 trace: exec: git-credential-cache get 11:47:10.028572 run-command.c:663 trace: run_command: git-credential-cache get protocol=https host=github.com username=token2 password=secret2 ## `store` - Docs: - Configure for a repo: ❯ mkdir foo ❯ cd foo ❯ git init . 11:53:48.042569 git.c:444 trace: built-in: git init . Initialized empty Git repository in /private/tmp/foo/.git/ ❯ git config --add credential.helper "" 11:53:52.949914 git.c:444 trace: built-in: git config --add credential.helper '' ❯ git config --add credential.helper store 11:53:56.682348 git.c:444 trace: built-in: git config --add credential.helper store ❯ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true [credential] helper = helper = store - Add credentials: ❯ (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve 11:54:44.184929 git.c:444 trace: built-in: git credential approve 11:54:44.185729 run-command.c:663 trace: run_command: 'git credential-store store' 11:54:44.197920 git.c:704 trace: exec: git-credential-store store 11:54:44.198471 run-command.c:663 trace: run_command: git-credential-store store /tmp/foo master ❯ (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve 11:54:48.452942 git.c:444 trace: built-in: git credential approve 11:54:48.453399 run-command.c:663 trace: run_command: 'git credential-store store' 11:54:48.463535 git.c:704 trace: exec: git-credential-store store 11:54:48.464004 run-command.c:663 trace: run_command: git-credential-store store - Query with username: ❯ (echo url=https://token@github.com; echo ) | git credential fill 11:55:21.191654 git.c:444 trace: built-in: git credential fill 11:55:21.192357 run-command.c:663 trace: run_command: 'git credential-store get' 11:55:21.204279 git.c:704 trace: exec: git-credential-store get 11:55:21.205063 run-command.c:663 trace: run_command: git-credential-store get protocol=https host=github.com username=token password=secret ❯ (echo url=https://token2@github.com; echo ) | git credential fill 11:55:24.194096 git.c:444 trace: built-in: git credential fill 11:55:24.194654 run-command.c:663 trace: run_command: 'git credential-store get' 11:55:24.207028 git.c:704 trace: exec: git-credential-store get 11:55:24.207643 run-command.c:663 trace: run_command: git-credential-store get protocol=https host=github.com username=token2 password=secret2 - Query without username returns *some* credentials, apparently not the ones that were set first: ❯ (echo url=https://github.com; echo ) | git credential fill 11:56:12.394594 git.c:444 trace: built-in: git credential fill 11:56:12.394949 run-command.c:663 trace: run_command: 'git credential-store get' 11:56:12.403303 git.c:704 trace: exec: git-credential-store get 11:56:12.403863 run-command.c:663 trace: run_command: git-credential-store get protocol=https host=github.com username=token2 password=secret2 ❯ (echo url=https://token2@github.com; echo ) | git credential reject 11:56:24.065910 git.c:444 trace: built-in: git credential reject 11:56:24.066314 run-command.c:663 trace: run_command: 'git credential-store erase' 11:56:24.074851 git.c:704 trace: exec: git-credential-store erase 11:56:24.076875 run-command.c:663 trace: run_command: git-credential-store erase ❯ (echo url=https://github.com; echo ) | git credential fill 11:56:26.438444 git.c:444 trace: built-in: git credential fill 11:56:26.438839 run-command.c:663 trace: run_command: 'git credential-store get' 11:56:26.446181 git.c:704 trace: exec: git-credential-store get 11:56:26.446721 run-command.c:663 trace: run_command: git-credential-store get protocol=https host=github.com username=token password=secret ## `osxkeychain` - Some docs: - This is the default helper on macOS currently (git 2.28.0). - This is how it stores a credential: ``` {.sh} Name: github.com Kind: Internet password Account: token Where: https://github.com ``` - It installs as a git subcommand, so it is possible to call its internal api directly: ❯ git credential-osxkeychain 11:50:56.325499 git.c:704 trace: exec: git-credential-osxkeychain 11:50:56.325783 run-command.c:663 trace: run_command: git-credential-osxkeychain usage: git credential-osxkeychain - As always, needs `username` when setting the credential. - No need to supply `username` to get *some* token that matches the host. This is in a clean keychain. First we set two credentials: ``` {.sh} ❯ (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve 10:48:47.187164 git.c:444 trace: built-in: git credential approve 10:48:47.187691 run-command.c:663 trace: run_command: 'git credential-osxkeychain store' 10:48:47.197964 git.c:704 trace: exec: git-credential-osxkeychain store 10:48:47.198518 run-command.c:663 trace: run_command: git-credential-osxkeychain store ❯ (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve 10:48:55.299933 git.c:444 trace: built-in: git credential approve 10:48:55.300282 run-command.c:663 trace: run_command: 'git credential-osxkeychain store' 10:48:55.308568 git.c:704 trace: exec: git-credential-osxkeychain store 10:48:55.309276 run-command.c:663 trace: run_command: git-credential-osxkeychain store ``` If we don't supply a username, then we'll just get one of them: ``` {.sh} ❯ (echo url=https://github.com; echo ) | git credential fill 10:49:17.371636 git.c:444 trace: built-in: git credential fill 10:49:17.372021 run-command.c:663 trace: run_command: 'git credential-osxkeychain get' 10:49:17.378688 git.c:704 trace: exec: git-credential-osxkeychain get 10:49:17.379164 run-command.c:663 trace: run_command: git-credential-osxkeychain get protocol=https host=github.com username=token password=secret ``` If we supply the username, then we'll get the correct one: ``` {.sh} ❯ (echo url=https://token2@github.com; echo ) | git credential fill 10:49:28.613779 git.c:444 trace: built-in: git credential fill 10:49:28.614108 run-command.c:663 trace: run_command: 'git credential-osxkeychain get' 10:49:28.621440 git.c:704 trace: exec: git-credential-osxkeychain get 10:49:28.621979 run-command.c:663 trace: run_command: git-credential-osxkeychain get protocol=https host=github.com username=token2 password=secret2 ``` To check that without a username we get an arbitrary one, let's remove `token`: ``` {.sh} ❯ (echo url=https://token@github.com; echo ) | git credential reject 10:49:58.584332 git.c:444 trace: built-in: git credential reject 10:49:58.586880 run-command.c:663 trace: run_command: 'git credential-osxkeychain erase' 10:49:58.598463 git.c:704 trace: exec: git-credential-osxkeychain erase 10:49:58.599214 run-command.c:663 trace: run_command: git-credential-osxkeychain erase ```sh ❯ (echo url=https://github.com; echo ) | git credential fill 10:50:07.468385 git.c:444 trace: built-in: git credential fill 10:50:07.468728 run-command.c:663 trace: run_command: 'git credential-osxkeychain get' 10:50:07.478398 git.c:704 trace: exec: git-credential-osxkeychain get 10:50:07.478832 run-command.c:663 trace: run_command: git-credential-osxkeychain get protocol=https host=github.com username=token2 password=secret2 ``` Now let's re-add token to make sure that `osxkeychain` does not prefer `token`: ``` {.sh} ❯ (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve 10:58:52.302066 git.c:444 trace: built-in: git credential approve 10:58:52.311063 run-command.c:663 trace: run_command: 'git credential-osxkeychain store' 10:58:52.321633 git.c:704 trace: exec: git-credential-osxkeychain store 10:58:52.322108 run-command.c:663 trace: run_command: git-credential-osxkeychain store ❯ (echo url=https://github.com; echo ) | git credential fill 10:58:57.316418 git.c:444 trace: built-in: git credential fill 10:58:57.317630 run-command.c:663 trace: run_command: 'git credential-osxkeychain get' 10:58:57.330142 git.c:704 trace: exec: git-credential-osxkeychain get 10:58:57.330697 run-command.c:663 trace: run_command: git-credential-osxkeychain get protocol=https host=github.com username=token2 password=secret2 ``` So it seems that `osxkeychain` will just find an arbitrary one, or the one that was added first. ## `manager-core` (on macOS), before version 2.0.246-beta - Not installed by default (git 2.28.0). - Install from brew, according to the instructions: - If updates your global git config, adding these lines: ``` {.sh} [credential] helper = "" [credential "https://dev.azure.com"] useHttpPath = true [credential] helper = manager-core ``` The `helper = ""` line deletes previous handlers. The system helper is kept as `osxkeychain`. - It installs as a git subcommand, and you can call its internal API directly: ❯ git credential-manager-core 11:51:56.434300 git.c:704 trace: exec: git-credential-manager-core 11:51:56.434496 run-command.c:663 trace: run_command: git-credential-manager-core Missing command. usage: git-credential-manager-core Available commands: erase get store configure [--system] unconfigure [--system] --version, version --help, -h, -? - It is not compatible with the `osxkeychain` helper, because it uses different keys in the keychain. - It supports different *providers*. Providers are auto-detected by default. GitHub has its own provider, detected via the `github.com` URL. The provider can be configured user a git config key or an environment variable: - It does not currently supports namepaces (like `manager`). - This is how it stores a credential: Name: git:https://github.com Kind: application password Account: token Where: git:https://github.com - No need to supply `username` to get *some* credential: ❯ (echo url=https://github.com; echo ) | git credential fill 11:24:47.750966 git.c:444 trace: built-in: git credential fill 11:24:47.753268 run-command.c:663 trace: run_command: 'git credential-manager-core get' 11:24:47.762249 git.c:704 trace: exec: git-credential-manager-core get 11:24:47.762917 run-command.c:663 trace: run_command: git-credential-manager-core get protocol=https host=github.com username=token password=secret - If there are multiple credentials, a random one (or the one set first?) is returned: ❯ (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve 11:25:41.553761 git.c:444 trace: built-in: git credential approve 11:25:41.554242 run-command.c:663 trace: run_command: 'git credential-manager-core store' 11:25:41.565748 git.c:704 trace: exec: git-credential-manager-core store 11:25:41.566218 run-command.c:663 trace: run_command: git-credential-manager-core store - In fact the username is completely ignored when getting credentials, at least for the GitHub provider: ❯ (echo url=https://token2@github.com; echo ) | git credential fill 11:29:49.274574 git.c:444 trace: built-in: git credential fill 11:29:49.275020 run-command.c:663 trace: run_command: 'git credential-manager-core get' 11:29:49.283563 git.c:704 trace: exec: git-credential-manager-core get 11:29:49.284236 run-command.c:663 trace: run_command: git-credential-manager-core get protocol=https host=github.com username=token password=secret This is because the username is not stored as part of the URL by the GitHub provider. Related issue: `manager` has a similar problem, it is linked from this GitHub issue. - If you set the provider to Generic, then usernames work as expected. In this case, this is stored in the keychain: Name: git:https://token@github.com/ Kind: application password Account: token Where: git:https://token@github.com/ These credentials are not compatible with the ones set by the GitHub provider. ❯ export GCM_PROVIDER=generic ❯ (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve 11:34:15.998644 git.c:444 trace: built-in: git credential approve 11:34:15.998992 run-command.c:663 trace: run_command: 'git credential-manager-core store' 11:34:16.008178 git.c:704 trace: exec: git-credential-manager-core store 11:34:16.008834 run-command.c:663 trace: run_command: git-credential-manager-core store ❯ (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve 11:35:52.629963 git.c:444 trace: built-in: git credential approve 11:35:52.637966 run-command.c:663 trace: run_command: 'git credential-manager-core store' 11:35:52.648058 git.c:704 trace: exec: git-credential-manager-core store 11:35:52.648514 run-command.c:663 trace: run_command: git-credential-manager-core store ❯ (echo url=https://token@github.com; echo ) | git credential fill 11:35:58.336428 git.c:444 trace: built-in: git credential fill 11:35:58.336881 run-command.c:663 trace: run_command: 'git credential-manager-core get' 11:35:58.345187 git.c:704 trace: exec: git-credential-manager-core get 11:35:58.345729 run-command.c:663 trace: run_command: git-credential-manager-core get protocol=https host=github.com username=token password=secret ❯ (echo url=https://token2@github.com; echo ) | git credential fill 11:36:02.550339 git.c:444 trace: built-in: git credential fill 11:36:02.550695 run-command.c:663 trace: run_command: 'git credential-manager-core get' 11:36:02.557777 git.c:704 trace: exec: git-credential-manager-core get 11:36:02.558359 run-command.c:663 trace: run_command: git-credential-manager-core get protocol=https host=github.com username=token2 password=secret2 ## `manager-core` (macOS), 2.0.246-beta or later - This version adds better support for multiple credentials for the same host. - It does not save the `username` as part of the service any more, for the generic provider. - It does save the `username` as `account` and it uses it when searching for credentials, both for the `generic` and the `github` provider. ## `manager` (GitHub authority) - This was Git Credential Manager for Windows, and it is the default helper on Windows for git 2.28.0. In git 2.29.0 it is not the default any more, and it is deprecated in favor of `manager-core`. It is still installed, though. - Docs: - It is installed as a git subcommand, so it can be called directly: ❯ $env:GIT_TRACE="true" ❯ git credential-manager 12:15:45.554298 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 12:15:45.556249 git.c:704 trace: exec: git-credential-manager 12:15:45.556249 run-command.c:663 trace: run_command: git-credential-manager usage: git-credential-manager.exe [approve|clear|config|delete|deploy|erase|fill|get|install|reject|remove|store|uninstall|version] [] fatal: Unable to open help documentation. - Setting credentials. The `username` must be provided, and it is stored in the credential, but the GitHub provider overwrites credentials with the same host name, even if they have a different `username`. I.e. after this, there will be only one credential: ❯ % { echo url=https://github.com; echo username=token; echo password=secret; echo "" } | git credential approve 13:16:58.118611 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:16:58.120617 git.c:444 trace: built-in: git credential approve 13:16:58.122612 run-command.c:663 trace: run_command: 'git credential-manager store' 13:16:58.170625 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:16:58.173087 git.c:704 trace: exec: git-credential-manager store 13:16:58.173087 run-command.c:663 trace: run_command: git-credential-manager store ❯ % { echo url=https://github.com; echo username=token2; echo password=secret2; echo "" } | git credential approve 13:17:30.925970 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:17:30.928134 git.c:444 trace: built-in: git credential approve 13:17:30.928987 run-command.c:663 trace: run_command: 'git credential-manager store' 13:17:30.968981 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:17:30.970974 git.c:704 trace: exec: git-credential-manager store 13:17:30.970974 run-command.c:663 trace: run_command: git-credential-manager store - It stores credentials in the following format: Internet or network address: git:https://github.com User name: token Password: **** Persistence: Local computer \ - It has multiple authorities, (like providers for `manager-core`). The GitHub authority is used for `github.com` URLs by default. The authority can be set with a config option or an env var. - We need to set the `GCM_VALIDATE` env var to `false` , otherwise it tries to validate the GitHub token every time we query it. ❯ $env:GCM_VALIDATE="false" - No `username` is needed for getting *the* credential, for the GitHub provider. ❯ % { echo url=https://github.com; echo "" } | git credential fill 13:24:43.831089 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:24:43.833666 git.c:444 trace: built-in: git credential fill 13:24:43.834092 run-command.c:663 trace: run_command: 'git credential-manager get' 13:24:43.872087 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:24:43.874092 git.c:704 trace: exec: git-credential-manager get 13:24:43.874092 run-command.c:663 trace: run_command: git-credential-manager get protocol=https host=github.com path= username=token2 password=secret2 In fact the `username` is ignored for the GitHub provider: ❯ % { echo url=https://token@github.com; echo "" } | git credential fill 13:25:06.029084 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:25:06.031084 git.c:444 trace: built-in: git credential fill 13:25:06.032081 run-command.c:663 trace: run_command: 'git credential-manager get' 13:25:06.069085 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:25:06.070086 git.c:704 trace: exec: git-credential-manager get 13:25:06.070086 run-command.c:663 trace: run_command: git-credential-manager get protocol=https host=github.com path= username=token2 password=secret2 - Supports namespaces, the default namespace is `git`. The namespace is included in the name of the credential: `namespace:URL`. - There is a pull request to include the `username` in the URL as well: ## `manager` (Basic authority) - `Basic` authority can be configured with an env var or config option: ❯ $env:GCM_AUTHORITY="Basic" - It is compatible with the credentials set by the `GitHub` authority: ❯ % { echo url=https://github.com; echo "" } | git credential fill 13:27:17.344396 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:27:17.346397 git.c:444 trace: built-in: git credential fill 13:27:17.347388 run-command.c:663 trace: run_command: 'git credential-manager get' 13:27:17.386388 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:27:17.388386 git.c:704 trace: exec: git-credential-manager get 13:27:17.388386 run-command.c:663 trace: run_command: git-credential-manager get protocol=https host=github.com path= username=token2 password=secret2 - But it supports usernames as well. Note that since the username is stored in the URL, it does not return some arbitrary credentials if we query the URL without a username. We need to tell GCM here not to ask for a password, but just fall back to the next credential helper. ❯ $env:GCM_INTERACTIVE="Never" ❯ % { echo url=https://token2@github.com; echo "" } | git -c credential.helper="! echo protocol=dummy; echo host=dummy; echo username=dummy; echo password=dummy" credential fill 13:31:16.682545 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:31:16.684473 git.c:444 trace: built-in: git credential fill 13:31:16.685471 run-command.c:663 trace: run_command: 'git credential-manager get' 13:31:16.725486 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:31:16.727472 git.c:704 trace: exec: git-credential-manager get 13:31:16.727472 run-command.c:663 trace: run_command: git-credential-manager get Logon failed, use ctrl+c to cancel basic credential prompt. 13:31:16.948468 run-command.c:663 trace: run_command: ' echo protocol=dummy; echo host=dummy; echo username=dummy; echo password=dummy get' protocol=dummy host=dummy username=dummy password=dummy get - When setting credentials, usernames are included in the URL: ❯ % { echo url=https://github.com; echo username=token; echo password=secret; echo "" } | git credential approve 13:34:37.937934 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:34:37.940820 git.c:444 trace: built-in: git credential approve 13:34:37.940995 run-command.c:663 trace: run_command: 'git credential-manager store' 13:34:37.983372 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:34:37.984737 git.c:704 trace: exec: git-credential-manager store 13:34:37.984737 run-command.c:663 trace: run_command: git-credential-manager store will create this record in the credential store: Internet or network address: git:https://token@github.com User name: token Password: **** Persistence: Local computer and then this can be queried with an explicit username: ❯ % { echo url=https://token@github.com; echo "" } | git -c credential.helper="! echo protocol=dummy; echo host=dummy; echo username=dummy; echo password=dummy" credential fill 13:36:42.789268 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:36:42.791270 git.c:444 trace: built-in: git credential fill 13:36:42.792266 run-command.c:663 trace: run_command: 'git credential-manager get' 13:36:42.830475 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:36:42.832279 git.c:704 trace: exec: git-credential-manager get 13:36:42.832279 run-command.c:663 trace: run_command: git-credential-manager get protocol=https host=github.com path= username=token password=secret ## `manager-core` (on Windows, before version 2.0.246-beta) - Not installed by default (git 2.28.0), but this is supposed to be the future canonical implementation. See above for the macOS version. - It works similarly to `manager` , I haven't seen any differences in behavior (version was 2.0.194.40577). Instead of authorities, we need to set up providers: . The `Generic` provider works the same way as the `Basic` authority in `manager`. - Unfortunately setting `GCM_AUTHORITY` will make `manager-core` break, so it is not possible to use both `manager` and `manager-core` if you need to set this environment variable. ## `manager-core` (on Windows, from 2.0.246-beta) - This is now the version that is the default helper in git 2.29.0 and later. `manager` is still installed, but the default system config sets `manager-core`. - It supports multiple users better: - When storing a credential, it first checks if there is a target name without a username. If there is, then it uses the target name without the username. It stores the username in the `username` field of the credential, still. so it is not lost. - When looking up a credential without a username, it will return the first credential it can find, even if the target name contains a username, and there is another credential without a username in the target name. - When looking up a credential with a username, it may return any credential with a matching username, and a target name that matches the host. The username inside the target name does not actually matter. # Recommendations for multiple accounts ## macOS 1. Use the `osxkeychain` helper. 2. FIrst remove all your current credentials for the host you are targeting. E.g. for GitHub, search for "Internet Passwords" for github.com, or use `gitcreds::gitcreds_list()` and the oskeyring package to remove them. You can also use the oskeyring package to back up the tokens and passwords. 3. Then add the credential that you want to use for "generic access". This is the credential that will be used for URLs without user names. The user name for this credential does not matter, but you can choose something descriptive, e.g. "PersonalAccessToken", "token", or "generic". 4. Configure git to use this username by default. E.g. if you chose "generic", then run git config --global credential.username generic 5. Add all the other credentials, with appropriate user names. These are the user names that you need to put in the URLs for the repositories or operations you want to use them for. (GitHub does not actually use the user names if the password is a PAT.) ## Windows, with git 2.29.0 or later and manager-core 1. We suggest that you update to the latest git version, but at least 2.29.0, and use the `manager-core` helper which is now default. If you installed `manager-core` separately from git, we suggest that you remove it, because it might cause confusion as to which helper is actually used. 2. Remove all current credentials first, for the host you are targeting. You can do this in 'Credential Manager' or `gitcreds::gitcreds_list()` to find them and the oskeyring package to remove them. You can also use the oskeyring packaeg to back up the tokens and passwords. 3. Then add the credential that you want to use for "generic access". This is the credential that will be used for URLs without user names. The user name for this credential does not matter, but you can choose something descriptive, e.g. "PersonalAccessToken", "token", or "generic". 4. Configure git to use this username by default. E.g. if you chose "generic", then run git config --global credential.username generic 5. Add all the other credentials, with appropriate user names. These are the user names that you need to put in the URLs for the repositories or operations you want to use them for. (GitHub does not actually use the user names if the password is a PAT.) ## Windows with older git versions ### At most one github.com credential If you only need to manage a single github.com credential, together with possibly multiple credentials to other hosts (including GitHub Enterprise hosts), then you can use the default `manager` helper, and get away with the default auto-detected GCM authority setting. In this case, you can add you github.com credential with an arbitrary user name, and for each other host you can add configure a default user name, and/or include user names in the URLs to these hosts. This is how to set a default user name for a host: git config --global credential.https://example.com.username myusername ### Multiple GitHub credentials If you need to manage multiple github.com credentials, then you can still use the `manager` helper, but you need to change the GCM authority by setting an option or an environment variable, see Once is merged, you won't need to do this. (At least in recent git versions, that contain a GCM build with the fix.) This is how to change the config for this: git config --global credential.authority Basic You can also change it only for github.com: git config --global credential.github.com.authority Basic Then you can configure a default user name, this will be used for URLs without a user name: git config --global credential.username generic Now you can add you credentials, the default one with the "generic" user name, and all the others with their specific user and host names. gitcreds/vignettes/package.Rmd0000644000176200001440000001527214210415267016136 0ustar liggesusers--- title: "gitcreds for package authors" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{gitcreds for package authors} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} editor_options: markdown: wrap: sentence canonical: true --- ## Introduction If you have a package that queries the GitHub API, or uses git with remote git repositories, then most likely you need to let your users specify their GitHub or git credentials. There are several benefits of using gitcreds to do this: - (Re)use the same credentials as command line git, R and the RStudio IDE., etc. Users can set their GitHub token once and use it everywhere. - Users can use the same credentials for multiple R packages. - gitcreds has a cache that makes credential lookup very fast. - Typically more secure than storing passwords and tokens in `.Renviron` files. - gitcreds supports multiple users and multiple hosts. - If git or credential helpers are not available, e.g. typically on a Linux server, then gitcreds can still use environment variables, and it still supports multiple users and hosts. ## The simple API The simplest way to use gitcreds is to call `gitcreds_get()` from your package to query credentials, possibly with a custom URL. For setting new credentials, you can point your users to `gitcreds_set()`. ### Errors from the simple API If you are using the simple API, gitcreds may throw the following classed errors and your package might want to handle: - `gitcreds_nogit_error` if git it not available on the system. - `gitcreds_no_credentials` if git did not find any credentials for the specified URL. The URL is stored in the error, under `url`. - `git_error` if a git command returned some error. The following information is stored in the error object: - `args` the command line arguments to git, - `stdout` standard output, - `stderr` standard error, - `status` the exit status of the git process. - `gitcreds_not_interactive_error` if `gitcreds_set()` is called in non-interactive mode. - `gitcreds_abort_replace_error` if the user aborted replacing the existing credentials. ## The low level API Should you need more flexibility, you can use the `gitcreds_approve()`, `gitcreds_fill()` and `gitcreds_reject()` functions, to add/update, query and remove credentials. We suggest you use the dummy credential helper (see below) for `gitcreds_fill()`, to avoid git password dialog boxes if a credential is not available. E.g. the low level API makes it possible to implement an alternative to `gitcreds_set()` , with a different user interface, or a version that also works in non-interactive sessions. ### The dummy credential helper In a typical setup, if git does not find credentials for the requested host after querying all defined credential helpers, it'll ask for a password in a dialog box, or a terminal prompt. It is often best to avoid these, and deal with the situation within R. gitcreds has a dummy credential helper, that always supplies dummy credentials. By default `gitcreds_fill()` adds this dummy helper to the list of configured credential helpers, and code calling `gitcreds_fill()` can check if git returned the dummy credentials, meaning that no real credentials were found. This is how the dummy credentials look: ``` {.r} gitcreds_fill(list(url="https://impossible.com")) #> [1] "protocol=dummy" "host=dummy" #> [3] "username=dummy" "password=dummy get" ``` It is best to look for `protocol=dummy` as the first line of the git output. ### Errors from the low level API - `git_error` if a git command returned some error. The following information is stored in the error object: - `args` the command line arguments to git, - `stdout` standard output, - `stderr` standard error, - `status` the exit status of the git process. ## Testing If your package uses gitcreds, either directly, or through another package, then you might want to test your package for the the various possible states of the user's git installation and credential store. gitcreds has some facilities to help you with this. If you want to test your package for a specific output from gitcreds, you can temporarily set the environment variable that gitcreds uses as a cache to the desired value. Use the `gitcreds_cache_envvar()` function to see which environment variable you need to set for a url: ```{r} gitcreds::gitcreds_cache_envvar("https://github.com") ``` It is easiest to use the withr package to temporarily change this environment variable in a test case: ```{r} library(testthat) test_that("bad credentials from git", { withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "bad")) # Test code that calls gitcreds_get(), potentially downstream. # gitcreds_get() will return `bad` as the password. # Illustration: expect_equal( gitcreds::gitcreds_get("https://github.com")$password, "bad" ) }) ``` If you want gitcreds to return a specific credential record, then you can specify the fields of the record in the environment variable, separated by colons. For example: ```{r} library(testthat) test_that("another GitHub user", { cred <- paste0( "protocol:https:", "host:github.com:", "username:user1:", "password:secret" ) withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = cred)) # Your test code comes here. This is just an illustration: print(gitcreds::gitcreds_get()) expect_equal(gitcreds::gitcreds_get()$username, "user1") }) ``` If you want gitcreds to fail for a specific host, set the corresponding environment variable to `"FAIL"`: ```{r} library(testthat) test_that("no credentials from git", { withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "FAIL")) # The test code that calls gitcreds_get() comes here. # It will fail with error "gitcreds_no_credentials" expect_error( gitcreds::gitcreds_get("https://github.com"), class = "gitcreds_no_credentials" ) }) ``` If you want gitcreds to fail with a specific error, then include the error class after a `"FAIL:"` prefix, in the environment variable. See the list of possible error classes above. For example: ```{r} library(testthat) test_that("no git installation", { withr::local_envvar(c( GITHUB_PAT_GITHUB_COM = "FAIL:gitcreds_nogit_error" )) # Test code that calls gitcreds_get() comes here. # Illustration: expect_error( gitcreds::gitcreds_get("https://github.com"), class = "gitcreds_nogit_error" ) }) ``` It is not currently possible to simulate the additional data in the error object, e.g. the standard output of a failed git command. If you need this for a test case, then your test case can call `gitcreds_get()` directly and you can use the mockery package to make gitcreds fail with the desired error object. gitcreds/R/0000755000176200001440000000000014214106402012251 5ustar liggesusersgitcreds/R/print.R0000644000176200001440000000057614210415267013550 0ustar liggesusers #' @export print.gitcreds <- function(x, header = TRUE, ...) { cat(format(x, header = header, ...), sep = "\n") invisible(x) } #' @export format.gitcreds <- function(x, header = TRUE, ...) { nms <- names(x) vls <- unlist(x, use.names = FALSE) vls[nms == "password"] <- "<-- hidden -->" c( if (header) "", paste0(" ", format(nms), ": ", vls) ) } gitcreds/R/git-auth.R0000644000176200001440000005356114214106402014130 0ustar liggesusers gitcreds_get <- NULL gitcreds_set <- NULL gitcreds_delete <- NULL gitcreds_list_helpers <- NULL gitcreds_cache_envvar <- NULL gitcreds_fill <- NULL gitcreds_approve <- NULL gitcreds_reject <- NULL gitcreds_parse_output <- NULL gitcreds <- local({ # ------------------------------------------------------------------------ # Public API # ------------------------------------------------------------------------ gitcreds_get <<- function(url = "https://github.com", use_cache = TRUE, set_cache = TRUE) { stopifnot( is_string(url), has_no_newline(url), is_flag(use_cache), is_flag(set_cache) ) cache_ev <- gitcreds_cache_envvar(url) if (use_cache && !is.null(ans <- gitcreds_get_cache(cache_ev))) { return(ans) } check_for_git() out <- gitcreds_fill(list(url = url), dummy = TRUE) creds <- gitcreds_parse_output(out, url) if (set_cache) { gitcreds_set_cache(cache_ev, creds) } creds } gitcreds_set <<- function(url = "https://github.com") { if (!is_interactive()) { throw(new_error( "gitcreds_not_interactive_error", message = "`gitcreds_set()` only works in interactive sessions" )) } stopifnot(is_string(url), has_no_newline(url)) check_for_git() current <- tryCatch( gitcreds_get(url, use_cache = FALSE, set_cache = FALSE), gitcreds_no_credentials = function(e) NULL ) if (!is.null(current)) { gitcreds_set_replace(url, current) } else { gitcreds_set_new(url) } msg("-> Removing credentials from cache...") gitcreds_delete_cache(gitcreds_cache_envvar(url)) msg("-> Done.") invisible() } #' Replace credentials with new ones #' #' It only works interactively, because of `menu()` in `ack()` and #' `readline()`. #' #' We need to set a username, it is compulsory for git credential. #' 1. If there was one in the url, then we use that. #' 2. Otherwise if git has a username configured for this URL, we use that. #' 3. Otherwise we use the username in the credentials we are replacing. #' #' @param url URL. #' @param current Must not be `NULL`, and it must contain a #' `gitcreds` object. (Well, a named list, really.) #' @noRd #' @return Nothing. gitcreds_set_replace <- function(url, current) { # Potentially take username from the credential we are replacing current_username <- current$username # Keep warning until there is a credential to replace. # In case there are multiple credentials for the same URL. while (!is.null(current)) { if (!ack(url, current, "Replace")) { throw(new_error("gitcreds_abort_replace_error")) } msg("\n-> Removing current credentials...") gitcreds_reject(current) current <- tryCatch( gitcreds_get(url, use_cache = FALSE, set_cache = FALSE), gitcreds_no_credentials = function(e) NULL ) if (!is.null(current)) msg("\n!! Found more matching credentials!") } msg("") pat <- readline("? Enter new password or token: ") username <- get_url_username(url) %||% gitcreds_username(url) %||% current_username msg("-> Adding new credentials...") gitcreds_approve(list(url = url, username = username, password = pat)) invisible() } #' Set new credentials #' #' This should not replace or remove any old credentials, but of course #' we cannot be sure, because credential helpers pretty much do what they #' want. #' #' We need to set a username, it is compulsory for git credential. #' 1. If there was one in the url, then we use that. #' 2. Otherwise if git has a username configured for this URL, we use that. #' 3. Otherwise we use a default username. #' #' @param url URL. #' @noRd #' @return Nothing. gitcreds_set_new <- function(url) { msg("\n") pat <- readline("? Enter password or token: ") username <- get_url_username(url) %||% gitcreds_username(url) %||% default_username() msg("-> Adding new credentials...") gitcreds_approve(list(url = url, username = username, password = pat)) invisible() } gitcreds_delete <<- function(url = "https://github.com") { if (!is_interactive()) { throw(new_error( "gitcreds_not_interactive_error", message = "`gitcreds_delete()` only works in interactive sessions" )) } stopifnot(is_string(url)) check_for_git() current <- tryCatch( gitcreds_get(url, use_cache = FALSE, set_cache = FALSE), gitcreds_no_credentials = function(e) NULL ) if (is.null(current)) { return(invisible(FALSE)) } if (!ack(url, current, "Delete")) { throw(new_error("gitcreds_abort_delete_error")) } msg("-> Removing current credentials...") gitcreds_reject(current) msg("-> Removing credentials from cache...") gitcreds_delete_cache(gitcreds_cache_envvar(url)) msg("-> Done.") invisible(TRUE) } gitcreds_list_helpers <<- function() { check_for_git() out <- git_run(c("config", "--get-all", "credential.helper")) clear <- rev(which(out == "")) if (length(clear)) out <- out[-(1:clear[1])] out } gitcreds_cache_envvar <<- function(url) { pcs <- parse_url(url) bad <- is.na(pcs$protocol) | is.na(pcs$host) if (any(bad)) { stop("Invalid URL(s): ", paste(url[bad], collapse = ", ")) } proto <- sub("^https?_$", "", paste0(pcs$protocol, "_")) user <- ifelse(pcs$username != "", paste0(pcs$username, "_AT_"), "") host0 <- sub("^api[.]github[.]com$", "github.com", pcs$host) host1 <- gsub("[.:]+", "_", host0) host <- gsub("[^a-zA-Z0-9_-]", "x", host1) slug1 <- paste0(proto, user, host) # fix the user name ambiguity, not that it happens often... slug2 <- ifelse(grepl("^AT_", slug1), paste0("AT_", slug1), slug1) # env vars cannot start with a number slug3 <- ifelse(grepl("^[0-9]", slug2), paste0("AT_", slug2), slug2) paste0("GITHUB_PAT_", toupper(slug3)) } gitcreds_get_cache <- function(ev) { val <- Sys.getenv(ev, NA_character_) if (is.na(val) && ev == "GITHUB_PAT_GITHUB_COM") { val <- Sys.getenv("GITHUB_PAT", NA_character_) } if (is.na(val) && ev == "GITHUB_PAT_GITHUB_COM") { val <- Sys.getenv("GITHUB_TOKEN", NA_character_) } if (is.na(val) || val == "") { return(NULL) } if (val == "FAIL" || grepl("^FAIL:", val)) { class <- strsplit(val, ":", fixed = TRUE)[[1]][2] if (is.na(class)) class <- "gitcreds_no_credentials" throw(new_error(class)) } unesc <- function(x) { gsub("\\\\(.)", "\\1", x) } # split on `:` that is not preceded by a `\` spval <- strsplit(val, "(? Your current credentials for ", squote(url), ":\n") msg(paste0(format(current, header = FALSE), collapse = "\n"), "\n") choices <- c( "Abort update with error, and keep the existing credentials", paste(what, "these credentials"), if (has_password(current)) "See the password / token" ) repeat { ch <- utils::menu(title = "-> What would you like to do?", choices) if (ch == 1) return(FALSE) if (ch == 2) return(TRUE) msg("\nCurrent password: ", current$password, "\n\n") } } #' Whether a `gitcreds` credential has a non-empty `password` #' #' This is usually `TRUE`. #' #' @param creds `gitcreds` #' @noRd #' @return `TRUE` is there is a `password` has_password <- function(creds) { is_string(creds$password) && creds$password != "" } #' Create a string that can be passed as standard input to `git credential` #' commands #' #' @param args Usually a `gitcreds` object, but can be a named list in #' general. This is a format: https://git-scm.com/docs/git-credential#IOFMT #' @noRd #' @return String. create_gitcreds_input <- function(args) { paste0( paste0(names(args), "=", args, collapse = "\n"), "\n\n" ) } #' Environment to set for all `git credential` commands. #' @noRd #' @return Named character vector. gitcreds_env <- function() { # Avoid interactivity and validation with some common credential helpers c( GCM_INTERACTIVE = "Never", GCM_MODAL_PROMPT = "false", GCM_VALIDATE = "false" ) } #' Check if `git` is installed and can run #' #' If not installed, a `gitcreds_nogit_error` is thrown. #' #' @noRd #' @return Nothing check_for_git <- function() { # This is simpler than Sys.which(), and also less fragile has_git <- tryCatch({ suppressWarnings(system2( "git", "--version", stdout = TRUE, stderr = null_file() )) TRUE }, error = function(e) FALSE) if (!has_git) throw(new_error("gitcreds_nogit_error")) } #' Query the `username` to use for `git config credential` #' #' @details #' The current working directory matters for this command, as you can #' configure `username` in a local `.git/config` file (via #' `git config --local`). #' #' @param url URL to query the username for, or `NULL`. If not `NULL`, #' then we first try to query an URL-specific username. See #' https://git-scm.com/docs/gitcredentials for more about URL-specific #' credential config #' @noRd #' @return A string with the username, or `NULL` if no default was found. gitcreds_username <- function(url = NULL) { gitcreds_username_for_url(url) %||% gitcreds_username_generic() } gitcreds_username_for_url <- function(url) { if (is.null(url)) return(NULL) tryCatch( git_run(c( "config", "--get-urlmatch", "credential.username", shQuote(url) )), git_error = function(err) { if (err$status == 1) NULL else throw(err) } ) } gitcreds_username_generic <- function() { tryCatch( git_run(c("config", "credential.username")), git_error = function(err) { if (err$status == 1) NULL else throw(err) } ) } #' User name to use when creating a credential, if there is nothing better #' #' These user names are typical for some git tools, e.g. #' [Git Credential Manager for Windows](http://microsoft.github.io/Git-Credential-Manager-for-Windows/) #' (`manager`) and #' [Git Credential Manager Core](https://github.com/Microsoft/Git-Credential-Manager-Core) #' (`manager-core`). #' #' @noRd #' @return Character string default_username <- function() { "PersonalAccessToken" } new_gitcreds <- function(...) { structure(list(...), class = "gitcreds") } # ------------------------------------------------------------------------ # Errors # ------------------------------------------------------------------------ gitcred_errors <- function() { c( git_error = "System git failed", gitcreds_nogit_error = "Could not find system git", gitcreds_not_interactive_error = "gitcreds needs an interactive session", gitcreds_abort_replace_error = "User aborted updating credentials", gitcreds_abort_delete_error = "User aborted deleting credentials", gitcreds_no_credentials = "Could not find any credentials", gitcreds_no_helper = "No credential helper is set", gitcreds_multiple_helpers = "Multiple credential helpers, only using the first", gitcreds_unknown_helper = "Unknown credential helper, cannot list credentials" ) } new_error <- function(class, ..., message = "", call. = TRUE, domain = NULL) { if (message == "") message <- gitcred_errors()[[class]] message <- .makeMessage(message, domain = domain) cond <- list(message = message, ...) if (call.) cond$call <- sys.call(-1) class(cond) <- c(class, "gitcreds_error", "error", "condition") cond } new_git_error <- function(class, ..., stderr) { cond <- new_error(class, ..., stderr = stderr) cond$message <- paste0(cond$message, ": ", stderr) cond } new_warning <- function(class, ..., message = "", call. = TRUE, domain = NULL) { if (message == "") message <- gitcred_errors()[[class]] message <- .makeMessage(message, domain = domain) cond <- list(message = message, ...) if (call.) cond$call <- sys.call(-1) class(cond) <- c(class, "gitcreds_warning", "warning", "condition") cond } throw <- function(cond) { cond if ("error" %in% class(cond)) { stop(cond) } else if ("warning" %in% class(cond)) { warning(cond) } else if ("message" %in% class(cond)) { message(cond) } else { signalCondition(cond) } } # ------------------------------------------------------------------------ # Genetic helpers # ------------------------------------------------------------------------ #' Set/remove env var and return the old values #' #' @param envs Named character vector or list of env vars to set. `NA` #' values will un-set an env var. #' @noRd #' @return Character vector, the old values of the supplied environment #' variables, `NA` for the ones that were not set. set_env <- function(envs) { current <- Sys.getenv(names(envs), NA_character_, names = TRUE) na <- is.na(envs) if (any(na)) { Sys.unsetenv(names(envs)[na]) } if (any(!na)) { do.call("Sys.setenv", as.list(envs[!na])) } invisible(current) } #' Get the user name from a `protocol://username@host/path` URL. #' #' @param url URL #' @noRd #' @return String or `NULL` if `url` does not have a username. get_url_username <- function(url) { nm <- parse_url(url)$username if (nm == "") NULL else nm } #' Parse URL #' #' It does not parse query parameters, as we don't deal with them here. #' The port number is included in the host name, if present. #' #' @param url Character vector of one or more URLs. #' @noRd #' @return Data frame with string columns: `protocol`, `username`, #' `password`, `host`, `path`. parse_url <- function(url) { re_url <- paste0( "^(?[a-zA-Z0-9]+)://", "(?:(?[^@/:]+)(?::(?[^@/]+))?@)?", "(?[^/]+)", "(?.*)$" # don't worry about query params here... ) mch <- re_match(url, re_url) mch[, setdiff(colnames(mch), c(".match", ".text")), drop = FALSE] } is_string <- function(x) { is.character(x) && length(x) == 1 && !is.na(x) } is_flag <- function(x) { is.logical(x) && length(x) == 1 && !is.na(x) } has_no_newline <- function(url) { ! grepl("\n", url, fixed = TRUE) } # From the rematch2 package re_match <- function(text, pattern, perl = TRUE, ...) { stopifnot(is.character(pattern), length(pattern) == 1, !is.na(pattern)) text <- as.character(text) match <- regexpr(pattern, text, perl = perl, ...) start <- as.vector(match) length <- attr(match, "match.length") end <- start + length - 1L matchstr <- substring(text, start, end) matchstr[ start == -1 ] <- NA_character_ res <- data.frame( stringsAsFactors = FALSE, .text = text, .match = matchstr ) if (!is.null(attr(match, "capture.start"))) { gstart <- attr(match, "capture.start") glength <- attr(match, "capture.length") gend <- gstart + glength - 1L groupstr <- substring(text, gstart, gend) groupstr[ gstart == -1 ] <- NA_character_ dim(groupstr) <- dim(gstart) res <- cbind(groupstr, res, stringsAsFactors = FALSE) } names(res) <- c(attr(match, "capture.names"), ".text", ".match") res } null_file <- function() { if (get_os() == "windows") "nul:" else "/dev/null" } get_os <- function() { if (.Platform$OS.type == "windows") { "windows" } else if (Sys.info()[["sysname"]] == "Darwin") { "macos" } else if (Sys.info()[["sysname"]] == "Linux") { "linux" } else { "unknown" } } `%||%` <- function(l, r) if (is.null(l)) r else l #' Like [message()], but print to standard output in interactive #' sessions #' #' To avoid red output in RStudio, RGui, and R.app. #' #' @inheritParams message #' @noRd #' @return Nothing msg <- function(..., domain = NULL, appendLF = TRUE) { cnd <- .makeMessage(..., domain = domain, appendLF = appendLF) withRestarts(muffleMessage = function() NULL, { signalCondition(simpleMessage(cnd)) output <- default_output() cat(cnd, file = output, sep = "") }) invisible() } #' Where to print messages to #' #' If the session is not interactive, then it potentially matters #' whether we print to stdout or stderr, so we print to stderr. #' #' The same applies when there is a sink for stdout or stderr. #' #' @noRd #' @return The connection to print to. default_output <- function() { if (is_interactive() && no_active_sink()) stdout() else stderr() } no_active_sink <- function() { # See ?sink.number for the explanation sink.number("output") == 0 && sink.number("message") == 2 } #' Smarter `interactive()` #' #' @noRd #' @return Logical scalar. is_interactive <- function() { opt <- getOption("rlib_interactive") opt2 <- getOption("rlang_interactive") if (isTRUE(opt)) { TRUE } else if (identical(opt, FALSE)) { FALSE } else if (isTRUE(opt2)) { TRUE } else if (identical(opt2, FALSE)) { FALSE } else if (tolower(getOption("knitr.in.progress", "false")) == "true") { FALSE } else if (identical(Sys.getenv("TESTTHAT"), "true")) { FALSE } else { base::interactive() } } #' Squote wrapper to avoid smart quotes #' #' @inheritParams sQuote #' @inherit sQuote return #' @noRd #' @return Character vector. squote <- function(x) { old <- options(useFancyQuotes = FALSE) on.exit(options(old), add = TRUE) sQuote(x) } #' Read all of a file #' #' @param path File to read. #' @param ... Passed to [readChar()]. #' @noRd #' @return String. read_file <- function(path, ...) { readChar(path, nchars = file.info(path)$size, ...) } environment() }) gitcreds/R/aaa-doc.R0000644000176200001440000003757214214106402013677 0ustar liggesusers # ------------------------------------------------------------------------ # Public API # ------------------------------------------------------------------------ #' Query and set git credentials #' #' This manual page is for _users_ of packages that depend on gitcreds #' for managing tokens or passwords to GitHub or other git repositories. #' If you are a package author and want to import gitcreds for this #' functionality, see `vignette("package", package = "gitcreds")`. #' Otherwise please start at 'Basics' below. #' #' # Basics #' #' `gitcreds_get()` queries git credentials. It is typically used by package #' code that needs to authenticate to GitHub or another git repository. #' The end user might call `gitcreds_get()` directly to check that the #' credentials are properly set up. #' #' `gitcreds_set()` adds or updates git credentials in the credential store. #' It is typically called by the user, and it only works in interactive #' sessions. It always asks for acknowledgement before it overwrites #' existing credentials. #' #' `gitcreds_delete()` deletes git credentials from the credential store. #' It is typically called by the user, and it only works in interactive #' sessions. It always asks for acknowledgement. #' #' `gitcreds_list_helpers()` lists the active credential helpers. #' #' ## git versions #' #' These functions use the `git credential` system command to query and set #' git credentials. They need an external git installation. You can #' download git from https://git-scm.com/downloads. A recent version is #' best, but at least git 2.9 is suggested. #' #' gitcreds should work out of the box on macOS with git versions 2.9.2 #' or later, and on Windows with git versions 2.12.1 or later, using the #' default git settings. On Windows, for git versions from 2.9.2 up until #' 2.12.1 you probably need to set the default credential helper to #' `wincred`. It is usually simpler to update git to a recent version. #' #' To see your current git version run `git --version` from your shell. #' Or from R: #' #' ```r #' system("git --version") #' ``` #' #' If you need to avoid installing git, see 'Environment variables' below. #' #' ## GitHub #' #' ### New setup #' #' To set up password-less authentication to GitHub: #' 1. Create a personal access token (PAT). See #' https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token. #' 2. Call `gitcreds_set()` and give this token as the password. #' 3. Run `gitcreds_get(use_cache = FALSE)` to check that the new #' PAT is set up. To see the token, you can run #' `gitcreds_get(use_cache = FALSE)$password`. #' #' ### Migrating from the `GITHUB_PAT` environment variable #' #' If you already have a GitHub token, and use the `GITHUB_PAT` or #' `GITHUB_TOKEN` environment variable in your `.Renviron` file or #' elsewhere, no changes are neccessary. gitcreds will automatically use #' this variable. #' #' However, we still suggest that you add your token to the git credential #' store with `gitcreds_set()` and remove `GITHUB_PAT` from your #' `.Renviron` file. The credential store is more secure than storing #' tokens in files, and command line git also uses the credential store #' for password-less authentication. #' #' # Advanced topics #' #' ## Cached credentials #' #' Because querying the git credential store might not be very fast, #' `gitcreds_get()` caches credentials in environment variables by default. #' Credentials for different URLs are stored in different environment #' variables. The name of the environment variable is calculated with #' [gitcreds_cache_envvar()]. #' #' To remove the cache, remove this environment variable with #' [Sys.unsetenv()]. #' #' ## Environment variables #' #' If you want to avoid installing git, or using the credential store for #' some reason, you can supply credentials in environment variables, e.g. #' via the `.Renviron` file. Use [gitcreds_cache_envvar()] to query the #' environment variable you need to set for a URL: #' #' 1. You can set this environment variable to the token or password itself. #' 2. If you also need a user name, then use the `user:password` form, i.e. #' separate them with a colon. (If your user name or passwrd has `:` #' characters, then you need to escape them with a preceding backslash.) #' #' ## Proxies #' #' git should pick up the proxy configuration from the `http_proxy`, #' `https_proxy`, and `all_proxy` environment variables. To override #' these, you can set the `http.proxy` git configuration key. #' More info here: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy #' and here: https://github.com/microsoft/Git-Credential-Manager-Core/blob/master/docs/netconfig.md #' #' ## Credential helpers #' #' git credential helpers are an extensible, configurable mechanism to #' store credentials. Different git installations have different credentials #' helpers. On Windows the default helper stores credentials in the system #' credential store. On macOS, it stores them in the macOS Keychain. #' Other helpers cache credentials in a server process or in a file on the #' file system. #' #' gitcreds only works if a credential helper is configured. For the current #' git version (2.29.0), this is the case by default on Windows and macOS #' (for git from HomeBrew), but most Linux distributions do not set up a #' default credential helper. #' #' You can use `gitcreds_list_helpers()` to see the _active_ credential #' helper(s) for a repository. Make sure you set the working directory #' to the git tree before calling `gitcreds_list_helpers()`. #' #' ## The current working directory #' #' git allows repository specific configuration, via the `.git/config` file. #' The `config` file might specify a different credential helper, a #' different user name, etc. This means that `gitcreds_get()` etc. will #' potentially work differently depending on the current working #' directory. This is especially relevant for package code that changes #' the working directory temporarily. #' #' ## Non-GitHub accounts #' #' Non-GitHub URLs work mostly the same way as GitHub URLs. #' `gitcreds_get()` and `gitcreds_set()` default to GitHub, so you'll need #' to explicitly set their `url` argument. #' #' Some credential helpers, e.g. Git Credential Manager for Windows #' (`manager`) and Git Credential Manager Core (`manager-core`) work #' slightly differently for GitHub and non-GitHub URLs, see their #' documentation for details. #' #' ## Multiple accounts #' #' The various credential helpers support having multiple accounts on the #' same server in different ways. Here are our recommendations. #' #' ### macOS #' #' 1. Use the (currently default) `osxkeychain` credential helper. #' 2. In Keychain Access, remove all your current credentials for the #' host(s) you are targeting. E.g. for GitHub, search for github.com #' Internet Passwords. #' 3. Then add the credential that you want to use for "generic access". #' This is the credential that will be used for URLs without user #' names. The user name for this credential does not matter, but you #' can choose something descriptive, e.g. "token", or "generic". #' 4. Configure git to use this username by default. E.g. if you chose #' "generic", then run #' #' git config --global credential.username generic #' #' 5. Add all the other credentials, with appropriate user names. These #' are the user names that you need to put in the URLs for the #' repositories or operations you want to use them for. (GitHub does #' not actually use the user names if the password is a PAT, but they #' are used to look up the correct token in the credential store.) #' #' ### Windows with git 2.29.0 or later #' #' 1. We suggest that you update to the latest git version, but at #' least 2.29.0, and use the `manager-core` helper which is now default. #' If you installed `manager-core` separately from git, we suggest that #' you remove it, because it might cause confusion as to which helper is #' actually used. #' 2. Remove all current credentials first, for the host you are targeting. #' You can do this in 'Credential Manager' or `gitcreds::gitcreds_list()` #' to find them and 'Credential Manager' or the oskeyring package to #' remove them. You can also use the oskeyring package to back up the #' tokens and passwords. #' 3. Then add the credential that you want to use for "generic access". #' This is the credential that will be used for URLs without user names. #' The user name for this credential does not matter, but you can choose #' something descriptive, e.g. "PersonalAccessToken", "token", or #' "generic". #' 4. Configure git to use this username by default. E.g. if you chose #' "generic", then run #' #' git config --global credential.username generic #' #' 5. Add all the other credentials, with appropriate user names. #' These are the user names that you need to put in the URLs for the #' repositories or operations you want to use them for. (GitHub does #' not actually use the user names if the password is a PAT, but they #' are used to look up the correct token from the credential store.) #' #' ### Windows with older git versions, 2.28.0 and before #' #' #### A single GitHub account #' #' If you only need to manage a single github.com credential, together with #' possibly multiple credentials to other hosts (including GitHub #' Enterprise hosts), then you can use the default `manager` helper, and #' get away with the default auto-detected GCM authority setting. #' #' In this case, you can add your github.com credential with an arbitrary #' user name, and for each other host you can configure a default user #' name, and/or include user names in the URLs to these hosts. This is how #' to set a default user name for a host called `https://example.com`: #' #' ``` #' git config --global credential.https://example.com.username myusername #' ``` #' #' #### Multiple GitHub credentials #' #' If you need to manage multiple github.com credentials, then you can #' still use the `manager` helper, but you need to change the GCM authority #' by setting an option or an environment variable, see #' #' #' This is how to change GCM authority in the config: #' #' ``` #' git config --global credential.authority Basic #' ``` #' #' You can also change it only for github.com: #' #' ``` #' git config --global credential.github.com.authority Basic #' ``` #' #' Then you can configure a default user name, this will be used for URLs #' without a user name: #' #' ``` #' git config --global credential.username generic #' ``` #' #' Now you can add you credentials, the default one with the "generic" user #' name, and all the others with their specific user and host names. #' #' Alternatively, you can install a newer version of Git Credential Manager #' Core (GCM Core), at least version 2.0.252-beta, and use the #' `manager-core` helper. You'll potentially need to delete the older #' `manager-core` helper that came with git itself. With the newer version #' of GCM Core, you can use the same method as for newer git versions, see #' above. #' #' ## Multiple credential helpers #' #' It is possible to configure multiple credential helpers. If multiple #' helpers are configured for a repository, then `gitcreds_get()` will #' go over them until a credential is found. `gitcreds_set()` will try to #' set the new credentials in _every_ configured credential helper. #' #' You can use [gitcreds_list_helpers()] to list all configured helpers. #' #' @param url URL to get, set or delete credentials for. It may contain a #' user name, which is typically (but not always) used by the credential #' helpers. It may also contain a path, which is typically (but not always) #' ignored by the credential helpers. #' @param use_cache Whether to try to use the environment variable cache #' before turning to git to look up the credentials for `url`. #' See [gitcreds_cache_envvar()]. #' @param set_cache Whether to set the environment variable cache after #' receiving the credentials from git. See [gitcreds_cache_envvar()]. #' #' @return `gitcreds_get()` returns a `gitcreds` object, a named list #' of strings, the fields returned by the git credential handler. #' Typically the fields are `protocol`, `host`, `username`, `password`. #' Some credential helpers support path-dependent credentials and also #' return a `path` field. #' #' `gitcreds_set()` returns nothing. #' #' `gitcreds_delete()` returns `FALSE` if it did not find find any #' credentials to delete, and thus it did not call `git credential reject`. #' Otherwise it returns `TRUE`. #' #' `gitcreds_get()` errors if git is not installed, no credential helpers #' are configured or no credentials are found. `gitcreds_set()` errors if #' git is not installed, or setting the new credentials fails. #' `gitcreds_delete()` errors if git is not installed or the git calls fail. #' See `vignette("package", package = "gitcreds")` if you want to handle #' these errors. #' #' @aliases gitcreds #' @export #' @examples #' \dontrun{ #' gitcreds_get() #' gitcreds_get("https://github.com") #' gitcreds_get("https://myuser@github.com/myorg/myrepo") #' } gitcreds_get <- NULL #' @export #' @rdname gitcreds_get gitcreds_set <- NULL #' @export #' @rdname gitcreds_get gitcreds_delete <- NULL #' @return `gitcreds_list_helpers()` returns a character vector, #' corresponding to the `credential.helper` git configuration key. #' Usually it contains a single credential helper, but it is possible to #' configure multiple helpers. #' #' @rdname gitcreds_get #' @export gitcreds_list_helpers <- NULL #' Environment variable to cache the password for a URL #' #' `gitcreds_get()` caches credentials in environment variables. #' `gitcreds_cache_envvar()` calculates the environment variaable name #' that is used as the cache, for a URL. #' #' @param url Character vector of URLs, they may contain user names #' and paths as well. See details below. #' @return Character vector of environment variables. #' #' @seealso [gitcreds_get()]. #' #' @export #' @examples #' gitcreds_cache_envvar("https://github.com") #' gitcreds_cache_envvar("https://api.github.com/path/to/endpoint") #' gitcreds_cache_envvar("https://jane@github.com") #' gitcreds_cache_envvar("https://another.site.github.com") gitcreds_cache_envvar <- NULL # ------------------------------------------------------------------------ # Raw git credential API # ------------------------------------------------------------------------ #' Access the low level credential API #' #' These function are primarily for package authors, who want more #' control over the user interface, so they want to avoid calling #' [gitcreds_get()] and [gitcreds_set()] directly. #' #' `gitcreds_fill()` calls `git credential fill` to query git #' credentials. #' #' @param input Named list to pass to `git credential fill`. #' @param args Extra args, used _before_ `fill`, to allow #' `git -c ... fill`. #' @param dummy Whether to append a dummy credential helper to the #' list of credential helpers. #' @return The standard output of the `git` command, line by line. #' #' @seealso [gitcreds_parse_output()] to parse the output of #' `gitcreds_fill()`. #' #' @rdname gitcreds-api #' @export gitcreds_fill <- NULL #' @details `gitcreds_approve()` calls `git credential approve` #' to add new credentials. #' #' @param creds `gitcreds` object (named list) to add or remove. #' #' @rdname gitcreds-api #' @export gitcreds_approve <- NULL #' `gitcreds_reject()` calls `git credential reject` to remove #' credentials. #' #' @rdname gitcreds-api #' @export gitcreds_reject <- NULL #' Parse standard output from `git credential fill` #' #' @details #' For dummy credentials (i.e. the lack of credentials), it throws an #' error of class `gitcreds_no_credentials`. #' #' @param txt Character vector, standard output lines from #' `git credential fill`. #' @param url URL we queried, to be able to create a better error message. #' @return `gitcreds` object. #' #' @export gitcreds_parse_output <- NULL gitcreds/R/list-creds.R0000644000176200001440000002334714212610163014460 0ustar liggesusers #' List all credentials stored by a git credential helper #' #' This function is meant to be used interactively, to help you when #' configuring credential helpers. It is especially useful if you have #' multiple accounts on a host. #' #' Note that this function does not use the credential helper itself, #' so it does not have to be installed. But it may also give false #' results, so interpret the results with caution, and also use the tool #' provided by your OS, to look at the credentials: 'Keychain Access' #' on macOS and 'Credential Manager' on Windows. #' #' Only a small number of credential helpers are supported currently. #' Here is a brief description of each. #' #' ## `osxkeychain` on macOS #' #' This is the default credential helper on macOS. #' #' It has some peculiarities: #' * If you don't specify a username in the URL, then it will return the #' _oldest_ credentials that match the specified host name, with an #' arbitrary user name. #' * If the user name is specified in the URL, then it is used to look up #' the credentials. #' #' To change or delete the listed credentials, see the oskeyring package #' or the 'Keychain Access' macOS app. #' #' ## `manager`, on Windows #' #' This is Git Credential Manager for Windows, see #' https://github.com/microsoft/Git-Credential-Manager-for-Windows #' #' It is currently the default helper on Windows, included in the git #' installer. #' #' It has some oddities, especially with multiple GitHub users: #' * The `github` authority (which is used by default for `github.com` #' URLs) cannot handle multiple users. It always sets the `target_name` #' of the Windows credential to `git:` where `` does not #' contain the user name. Since `target_name` is a primary key, it is #' not possible to add multiple GitHub users with the default #' configuration. #' * To support multiple users, switch to the `Basic` authority, e.g. by #' setting the `GCM_AUTHORITY` env var to `Basic`. Then the user name #' will be included in `target_name`, and everything works fine. #' * For this helper `gitcreds_list()` lists all records with a matching #' host name. #' #' ## `manager-core` on Windows #' #' This is Git Credential Manager Core, see #' https://github.com/microsoft/Git-Credential-Manager-Core #' #' On Windows it behaves almost the same way as `manager`, with some #' differences: #' * Instead of _authorities_, it has providers. `github.com` URLs use the #' `github` provider by default. For better support for multiple GitHub #' accounts, switch to the `generic` provider by setting the #' `GCM_PROVIDER` env var to `generic`. #' * `gitcreds_list()` will list all credentials with a matching host, #' irrespectively of the user name in the input URL. #' #' ## `manager-core`, _before_ version 2.0.246-beta, on macOS #' #' This is Git Credential Manager Core, see #' https://github.com/microsoft/Git-Credential-Manager-Core #' #' This helper has some peculiarities w.r.t. user names: #' * If the "github" provider is used (which is the default for #' `github.com` URLs), then it completely ignores user names, even if #' they are explicitly specified in the query. #' * For other providers, the user name (if specified) is saved in the #' Keychain item. #' * For this helper, `gitcreds_list()` always lists all records that #' match the _host_, even if the user name does not match, because it #' is impossible to tell if the user name would be used in a proper #' git credential lookup. #' #' To change or delete the listed credentials, see the oskeyring package #' or the 'Keychain Access' macOS app. #' #' ## `manager-core`, version 2.0.246-beta or newer, on macOS #' #' This is a newer version of Git Credential Manager Core, that supports #' multiple users better: #' * if a user name is provided, then it saves it in the credential store, #' and it uses this user name for looking up credentials, even for the #' `github` provider. #' * `gitcreds_list()` always lists all records that match the host, even #' if the user name does not match. #' * Credentials that were created by an older version of `manager-core`, #' with the `generic` provider, do not work with the newer version of #' `manager-core`, because the format of the Keychain item is different. #' #' @param credential_helper Credential helper to use. If this is `NULL`, #' then the configured credential helper is used. If multiple credential #' helpers are configured, then the first one is used, with a warning. #' @param url URL to list credentials for. If `NULL` then the credentials #' are listed for all URLs. Note that for a host the results might be #' different if you specify or omit this argument. `gitcreds_list()` #' uses heuristics when the `url` is not specified. If is always best to #' specify the URL. #' @param protocol Protocol to list credentials for. If `NULL` and `url` #' includes a protocol then that is used. Otherwise `"https"` is used. #' @return A list of `oskeyring_macos_item` objects. See #' [oskeyring::macos_item()]. #' @export gitcreds_list <- function(url = "https://github.com", credential_helper = NULL, protocol = NULL) { stopifnot( is.null(credential_helper) || gitcreds$is_string(credential_helper), is.null(url) || gitcreds$is_string(url), is.null(protocol) || gitcreds$is_string(protocol) ) credential_helper <- credential_helper %||% gitcreds_list_helpers() if (length(credential_helper) == 0) { gitcreds$throw(gitcreds$new_error("gitcreds_no_helper")) } if (length(credential_helper) > 1) { gitcreds$throw(gitcreds$new_warning("gitcreds_multiple_helpers")) credential_helper <- credential_helper[[1]] } host <- NULL if (!is.null(url)) { purl <- gitcreds$parse_url(url) if (!is.na(purl$host)) host <- purl$host if (!is.na(purl$protocol)) protocol <- purl$protocol } protocol <- protocol %||% "https" switch( credential_helper, "osxkeychain" = gitcreds_list_osxkeychain(url, host, protocol), "manager" = gitcreds_list_manager(url, host, protocol), "manager-core" = gitcreds_list_manager_core(url, host, protocol), gitcreds$throw(gitcreds$new_error( "gitcreds_unknown_helper", credential_helper = credential_helper, message = sprintf( "Unknown credential helper: `%s`, cannot list credentials", credential_helper ) )) ) } #' This is how an item, added by git-credential-osxkeychain looks like: #' * protocol is always present #' * server is the same as label, but we ignore this when querying a #' specific host because the credential helper will ignore it as well #' * security_domain is never present, similarly ignored for specific hosts. similar reasons #' @noRd gitcreds_list_osxkeychain <- function(url, host, protocol) { if (!requireNamespace("oskeyring", quietly=TRUE)) { stop("Listing `osxkeychain` credentials needs the `oskeyring` package") } attr <- list() if (!is.null(host)) { attr$server <- host attr$label <- host } attr$protocol <- protocol its <- oskeyring::macos_item_search( "internet_password", attributes = attr ) Filter(is_osxkeychain_item, its) } is_osxkeychain_item <- function(it) { !is.null(it$attributes$label) && !is.null(it$attributes$server) && it$attributes$server == it$attributes$label && is.null(it$attributes$security_domain) } gitcreds_list_manager_core <- function(url, host, protocol) { os <- gitcreds$get_os() if (os == "macos") { gitcreds_list_manager_core_macos(url, host, protocol) } else if (os == "windows") { gitcreds_list_manager_core_win(url, host, protocol) } else { stop("Unsupported OS for `manager-core`") } } gitcreds_list_manager_core_macos <- function(url, host, protocol) { if (!requireNamespace("oskeyring", quietly=TRUE)) { stop("Listing `manager-core` credentials needs the `oskeyring` package") } # We can't filter, need to list all of them, because the 'service' # might include the user name, if credential.provider is "github" # (or "auto" and the host is github.com). its <- oskeyring::macos_item_search("generic_password") its <- Filter( function(it) is_manager_core_macos_item(it, protocol, host), its ) its } is_manager_core_macos_item <- function(it, protocol, host) { if (is.null(it$attributes$service)) return(FALSE) if (!grepl("^git:", it$attributes$service)) return(FALSE) if (is.null(host)) return(TRUE) iturl <- sub("^git:", "", it$attributes$service) piturl <- gitcreds$parse_url(iturl) !is.na(piturl$host) && piturl$host == host && !is.na(piturl$protocol) && piturl$protocol == protocol } gitcreds_list_manager_core_win <- function(url, host, protocol) { if (!requireNamespace("oskeyring", quietly=TRUE)) { stop("Listing `manager-core` credentials needs the `oskeyring` package") } its <- oskeyring::windows_item_enumerate(filter = "git:*") its <- Filter( function(it) is_manager_core_win_item(it, protocol, host), its ) its } is_manager_core_win_item <- function(it, protocol, host) { if (it$type != "generic") return(FALSE) if (!grepl("^git:", it$target_name)) return(FALSE) iturl <- sub("^git:", "", it$target_name) if (is.null(host)) return(TRUE) piturl <- gitcreds$parse_url(iturl) !is.na(piturl$host) && piturl$host == host && !is.na(piturl$protocol) && piturl$protocol == protocol } gitcreds_list_manager <- function(url, host, protocol) { if (!requireNamespace("oskeyring", quietly=TRUE)) { stop("Listing `manager` credentials needs the `oskeyring` package") } its <- oskeyring::windows_item_enumerate(filter = "git:*") its <- Filter( function(it) is_manager_item(it, protocol, host), its ) its } # this is the same, apparently is_manager_item <- is_manager_core_win_item `%||%` <- function(l, r) if (is.null(l)) r else l gitcreds/R/gitcreds-package.R0000644000176200001440000000017214214106402015571 0ustar liggesusers#' @keywords internal #' @aliases gitcreds-package "_PACKAGE" ## usethis namespace: start ## usethis namespace: end NULL gitcreds/NEWS.md0000644000176200001440000000076214306341640013162 0ustar liggesusers# gitcreds 0.1.2 * No user visible changes. # gitcreds 0.1.1 * gitcreds now works with older git versions on Windows. It should work without any configuration for git 2.12.1 or later, and with minimal configuration for git 2.9.2 - git 2.12.0. See `?gitcreds_get` for more. * git errors have now better error messages, they include the output from git as well. * The `git-auth.R` file is now standalone, and can be embedded into other packages. # gitcreds 0.1.0 First public release. gitcreds/MD50000644000176200001440000000377114306343457012407 0ustar liggesusersf02f16d47c6e65fc7169e7043423308a *DESCRIPTION fd6302a7bb0d29c2fba79b66644039bc *LICENSE 0d1ea6e7936dc778f0cee3be19b228a7 *NAMESPACE 4372e2972c799d13dd80105bd9ddd99c *NEWS.md 34f41d5b1025d1de441229576250a0d5 *R/aaa-doc.R 74b612795fdae4e2c968bd51c3796b4c *R/git-auth.R e65f88dfed8af485f4976c9208b64d10 *R/gitcreds-package.R e6f5f55141f902a99a3bc4e1b4227653 *R/list-creds.R adea6e86ded7781d381613bce42e5ed2 *R/print.R 9e50cb7692c6cd54e562045a01a62991 *README.md 214d99f2ce7320bac0b4689408e5ed38 *build/vignette.rds 16212b6040392b2ad64c5cfdaedafda7 *inst/doc/helper-survey.Rmd 6318244ff51355f2bd980f7a3c6da23c *inst/doc/helper-survey.html ffac8e37f7bfefbb2236d127a5ffce12 *inst/doc/package.R 57002e403689800e519540a658e3e250 *inst/doc/package.Rmd 66879e3ebd62dcce31979a69dfd92767 *inst/doc/package.html a87d430322280298623b65872004fef1 *man/gitcreds-api.Rd 175b6e951e796080850ffe0d0c71fde7 *man/gitcreds-package.Rd 274dd2af005fe2cddf756df0e4bee2c9 *man/gitcreds_cache_envvar.Rd a7634ae5334b5b25c186312397581567 *man/gitcreds_get.Rd 2cf4ef2ec3e0552a439b431cf8be8586 *man/gitcreds_list.Rd a6abf1260f6251a46da7e22254f5eefb *man/gitcreds_parse_output.Rd 234f6f02e2439b2c986f7085feb0c8b4 *tests/testthat.R 2bf7f813a74988914ce9ab3f6d00ecf0 *tests/testthat/helper.R 4b2802c9b5f3eaba36de388864a63b18 *tests/testthat/test-cache.R cf4f42a8e1d7f8501d9a17babe076bf2 *tests/testthat/test-format.R 0c205ec717014141f5cc985fdcff8be8 *tests/testthat/test-gitcreds-delete.R 05f8f2cd5f3a9a8d2ea8904751c513c1 *tests/testthat/test-gitcreds-get.R 3760a8fdff3bf4115f96ea616267e7be *tests/testthat/test-gitcreds-list.R a0d9b913778560ccbdff541035f58434 *tests/testthat/test-gitcreds-set.R 16a563fda5251d1c881e715a7ee285b7 *tests/testthat/test-list-helpers.R bcf77e821279be780bf5e6086bcfd012 *tests/testthat/test-standalone.R 86a85ada85ceb769ca0f7a0bdb9b1c6a *tests/testthat/test-username.R 111150f53b11a3f807d9fa3fbdc71253 *tests/testthat/test-utils.R 16212b6040392b2ad64c5cfdaedafda7 *vignettes/helper-survey.Rmd 57002e403689800e519540a658e3e250 *vignettes/package.Rmd gitcreds/inst/0000755000176200001440000000000014306341667013045 5ustar liggesusersgitcreds/inst/doc/0000755000176200001440000000000014306341667013612 5ustar liggesusersgitcreds/inst/doc/helper-survey.Rmd0000644000176200001440000011277314214106402017063 0ustar liggesusers--- title: "git credential helpers" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{git credential helpers} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} editor_options: markdown: wrap: sentence canonical: true --- # General remarks - For `git credential approve` git (2.28.0 macOS) does not even call the credential helper if no username is supplied: ``` {.sh} ❯ export GIT_TRACE=true ❯ (echo url=https://github.com; echo password=secret; echo ) | git credential approve 10:43:36.712290 git.c:444 trace: built-in: git credential approve ❯ ``` - The `credential.helper` key has a multi-set value, so if you add a new value, the old values are still kept. From git 2.9 specifying an empty string removes the previously defined helpers. # Credential helper survey We do this with an eye of supporting usernames and multiple users. ## `cache` - Docs: - This helper is not included in the default git installer on Windows . :( - This is how we can set up `cache` for a particular repository: ❯ mkdir foo ❯ cd foo ❯ git init . 11:43:28.618841 git.c:444 trace: built-in: git init . Initialized empty Git repository in /private/tmp/foo/.git/ ❯ git config --add credential.helper "" 11:43:50.682962 git.c:444 trace: built-in: git config --add credential.helper '' ❯ git config --add credential.helper cache 11:43:54.577707 git.c:444 trace: built-in: git config --add credential.helper cache ❯ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true [credential] helper = helper = cache - Now let's add credentials: ❯ (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve 11:45:16.813913 git.c:444 trace: built-in: git credential approve 11:45:16.814431 run-command.c:663 trace: run_command: 'git credential-cache store' 11:45:16.823008 git.c:704 trace: exec: git-credential-cache store 11:45:16.823637 run-command.c:663 trace: run_command: git-credential-cache store 11:45:16.842902 run-command.c:663 trace: run_command: git-credential-cache--daemon /Users/gaborcsardi/.cache/git/credential/socket ❯ (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve 11:45:28.927712 git.c:444 trace: built-in: git credential approve 11:45:28.928108 run-command.c:663 trace: run_command: 'git credential-cache store' 11:45:28.937087 git.c:704 trace: exec: git-credential-cache store 11:45:28.937695 run-command.c:663 trace: run_command: git-credential-cache store - Query with username works correctly: ❯ (echo url=https://token@github.com; echo ) | git credential fill 11:46:40.689122 git.c:444 trace: built-in: git credential fill 11:46:40.689638 run-command.c:663 trace: run_command: 'git credential-cache get' 11:46:40.696784 git.c:704 trace: exec: git-credential-cache get 11:46:40.697333 run-command.c:663 trace: run_command: git-credential-cache get protocol=https host=github.com username=token password=secret ❯ (echo url=https://token2@github.com; echo ) | git credential fill 11:46:43.767002 git.c:444 trace: built-in: git credential fill 11:46:43.767676 run-command.c:663 trace: run_command: 'git credential-cache get' 11:46:43.778637 git.c:704 trace: exec: git-credential-cache get 11:46:43.779201 run-command.c:663 trace: run_command: git-credential-cache get protocol=https host=github.com username=token2 password=secret2 - Query without username works, and returns *some* credential: ❯ (echo url=https://github.com; echo ) | git credential fill 11:45:58.200272 git.c:444 trace: built-in: git credential fill 11:45:58.200667 run-command.c:663 trace: run_command: 'git credential-cache get' 11:45:58.208372 git.c:704 trace: exec: git-credential-cache get 11:45:58.208919 run-command.c:663 trace: run_command: git-credential-cache get protocol=https host=github.com username=token password=secret ❯ (echo url=https://token@github.com; echo ) | git credential reject 11:47:03.921697 git.c:444 trace: built-in: git credential reject 11:47:03.922530 run-command.c:663 trace: run_command: 'git credential-cache erase' 11:47:03.935858 git.c:704 trace: exec: git-credential-cache erase 11:47:03.936400 run-command.c:663 trace: run_command: git-credential-cache erase ❯ (echo url=https://github.com; echo ) | git credential fill 11:47:10.018877 git.c:444 trace: built-in: git credential fill 11:47:10.019386 run-command.c:663 trace: run_command: 'git credential-cache get' 11:47:10.027990 git.c:704 trace: exec: git-credential-cache get 11:47:10.028572 run-command.c:663 trace: run_command: git-credential-cache get protocol=https host=github.com username=token2 password=secret2 ## `store` - Docs: - Configure for a repo: ❯ mkdir foo ❯ cd foo ❯ git init . 11:53:48.042569 git.c:444 trace: built-in: git init . Initialized empty Git repository in /private/tmp/foo/.git/ ❯ git config --add credential.helper "" 11:53:52.949914 git.c:444 trace: built-in: git config --add credential.helper '' ❯ git config --add credential.helper store 11:53:56.682348 git.c:444 trace: built-in: git config --add credential.helper store ❯ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true [credential] helper = helper = store - Add credentials: ❯ (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve 11:54:44.184929 git.c:444 trace: built-in: git credential approve 11:54:44.185729 run-command.c:663 trace: run_command: 'git credential-store store' 11:54:44.197920 git.c:704 trace: exec: git-credential-store store 11:54:44.198471 run-command.c:663 trace: run_command: git-credential-store store /tmp/foo master ❯ (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve 11:54:48.452942 git.c:444 trace: built-in: git credential approve 11:54:48.453399 run-command.c:663 trace: run_command: 'git credential-store store' 11:54:48.463535 git.c:704 trace: exec: git-credential-store store 11:54:48.464004 run-command.c:663 trace: run_command: git-credential-store store - Query with username: ❯ (echo url=https://token@github.com; echo ) | git credential fill 11:55:21.191654 git.c:444 trace: built-in: git credential fill 11:55:21.192357 run-command.c:663 trace: run_command: 'git credential-store get' 11:55:21.204279 git.c:704 trace: exec: git-credential-store get 11:55:21.205063 run-command.c:663 trace: run_command: git-credential-store get protocol=https host=github.com username=token password=secret ❯ (echo url=https://token2@github.com; echo ) | git credential fill 11:55:24.194096 git.c:444 trace: built-in: git credential fill 11:55:24.194654 run-command.c:663 trace: run_command: 'git credential-store get' 11:55:24.207028 git.c:704 trace: exec: git-credential-store get 11:55:24.207643 run-command.c:663 trace: run_command: git-credential-store get protocol=https host=github.com username=token2 password=secret2 - Query without username returns *some* credentials, apparently not the ones that were set first: ❯ (echo url=https://github.com; echo ) | git credential fill 11:56:12.394594 git.c:444 trace: built-in: git credential fill 11:56:12.394949 run-command.c:663 trace: run_command: 'git credential-store get' 11:56:12.403303 git.c:704 trace: exec: git-credential-store get 11:56:12.403863 run-command.c:663 trace: run_command: git-credential-store get protocol=https host=github.com username=token2 password=secret2 ❯ (echo url=https://token2@github.com; echo ) | git credential reject 11:56:24.065910 git.c:444 trace: built-in: git credential reject 11:56:24.066314 run-command.c:663 trace: run_command: 'git credential-store erase' 11:56:24.074851 git.c:704 trace: exec: git-credential-store erase 11:56:24.076875 run-command.c:663 trace: run_command: git-credential-store erase ❯ (echo url=https://github.com; echo ) | git credential fill 11:56:26.438444 git.c:444 trace: built-in: git credential fill 11:56:26.438839 run-command.c:663 trace: run_command: 'git credential-store get' 11:56:26.446181 git.c:704 trace: exec: git-credential-store get 11:56:26.446721 run-command.c:663 trace: run_command: git-credential-store get protocol=https host=github.com username=token password=secret ## `osxkeychain` - Some docs: - This is the default helper on macOS currently (git 2.28.0). - This is how it stores a credential: ``` {.sh} Name: github.com Kind: Internet password Account: token Where: https://github.com ``` - It installs as a git subcommand, so it is possible to call its internal api directly: ❯ git credential-osxkeychain 11:50:56.325499 git.c:704 trace: exec: git-credential-osxkeychain 11:50:56.325783 run-command.c:663 trace: run_command: git-credential-osxkeychain usage: git credential-osxkeychain - As always, needs `username` when setting the credential. - No need to supply `username` to get *some* token that matches the host. This is in a clean keychain. First we set two credentials: ``` {.sh} ❯ (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve 10:48:47.187164 git.c:444 trace: built-in: git credential approve 10:48:47.187691 run-command.c:663 trace: run_command: 'git credential-osxkeychain store' 10:48:47.197964 git.c:704 trace: exec: git-credential-osxkeychain store 10:48:47.198518 run-command.c:663 trace: run_command: git-credential-osxkeychain store ❯ (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve 10:48:55.299933 git.c:444 trace: built-in: git credential approve 10:48:55.300282 run-command.c:663 trace: run_command: 'git credential-osxkeychain store' 10:48:55.308568 git.c:704 trace: exec: git-credential-osxkeychain store 10:48:55.309276 run-command.c:663 trace: run_command: git-credential-osxkeychain store ``` If we don't supply a username, then we'll just get one of them: ``` {.sh} ❯ (echo url=https://github.com; echo ) | git credential fill 10:49:17.371636 git.c:444 trace: built-in: git credential fill 10:49:17.372021 run-command.c:663 trace: run_command: 'git credential-osxkeychain get' 10:49:17.378688 git.c:704 trace: exec: git-credential-osxkeychain get 10:49:17.379164 run-command.c:663 trace: run_command: git-credential-osxkeychain get protocol=https host=github.com username=token password=secret ``` If we supply the username, then we'll get the correct one: ``` {.sh} ❯ (echo url=https://token2@github.com; echo ) | git credential fill 10:49:28.613779 git.c:444 trace: built-in: git credential fill 10:49:28.614108 run-command.c:663 trace: run_command: 'git credential-osxkeychain get' 10:49:28.621440 git.c:704 trace: exec: git-credential-osxkeychain get 10:49:28.621979 run-command.c:663 trace: run_command: git-credential-osxkeychain get protocol=https host=github.com username=token2 password=secret2 ``` To check that without a username we get an arbitrary one, let's remove `token`: ``` {.sh} ❯ (echo url=https://token@github.com; echo ) | git credential reject 10:49:58.584332 git.c:444 trace: built-in: git credential reject 10:49:58.586880 run-command.c:663 trace: run_command: 'git credential-osxkeychain erase' 10:49:58.598463 git.c:704 trace: exec: git-credential-osxkeychain erase 10:49:58.599214 run-command.c:663 trace: run_command: git-credential-osxkeychain erase ```sh ❯ (echo url=https://github.com; echo ) | git credential fill 10:50:07.468385 git.c:444 trace: built-in: git credential fill 10:50:07.468728 run-command.c:663 trace: run_command: 'git credential-osxkeychain get' 10:50:07.478398 git.c:704 trace: exec: git-credential-osxkeychain get 10:50:07.478832 run-command.c:663 trace: run_command: git-credential-osxkeychain get protocol=https host=github.com username=token2 password=secret2 ``` Now let's re-add token to make sure that `osxkeychain` does not prefer `token`: ``` {.sh} ❯ (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve 10:58:52.302066 git.c:444 trace: built-in: git credential approve 10:58:52.311063 run-command.c:663 trace: run_command: 'git credential-osxkeychain store' 10:58:52.321633 git.c:704 trace: exec: git-credential-osxkeychain store 10:58:52.322108 run-command.c:663 trace: run_command: git-credential-osxkeychain store ❯ (echo url=https://github.com; echo ) | git credential fill 10:58:57.316418 git.c:444 trace: built-in: git credential fill 10:58:57.317630 run-command.c:663 trace: run_command: 'git credential-osxkeychain get' 10:58:57.330142 git.c:704 trace: exec: git-credential-osxkeychain get 10:58:57.330697 run-command.c:663 trace: run_command: git-credential-osxkeychain get protocol=https host=github.com username=token2 password=secret2 ``` So it seems that `osxkeychain` will just find an arbitrary one, or the one that was added first. ## `manager-core` (on macOS), before version 2.0.246-beta - Not installed by default (git 2.28.0). - Install from brew, according to the instructions: - If updates your global git config, adding these lines: ``` {.sh} [credential] helper = "" [credential "https://dev.azure.com"] useHttpPath = true [credential] helper = manager-core ``` The `helper = ""` line deletes previous handlers. The system helper is kept as `osxkeychain`. - It installs as a git subcommand, and you can call its internal API directly: ❯ git credential-manager-core 11:51:56.434300 git.c:704 trace: exec: git-credential-manager-core 11:51:56.434496 run-command.c:663 trace: run_command: git-credential-manager-core Missing command. usage: git-credential-manager-core Available commands: erase get store configure [--system] unconfigure [--system] --version, version --help, -h, -? - It is not compatible with the `osxkeychain` helper, because it uses different keys in the keychain. - It supports different *providers*. Providers are auto-detected by default. GitHub has its own provider, detected via the `github.com` URL. The provider can be configured user a git config key or an environment variable: - It does not currently supports namepaces (like `manager`). - This is how it stores a credential: Name: git:https://github.com Kind: application password Account: token Where: git:https://github.com - No need to supply `username` to get *some* credential: ❯ (echo url=https://github.com; echo ) | git credential fill 11:24:47.750966 git.c:444 trace: built-in: git credential fill 11:24:47.753268 run-command.c:663 trace: run_command: 'git credential-manager-core get' 11:24:47.762249 git.c:704 trace: exec: git-credential-manager-core get 11:24:47.762917 run-command.c:663 trace: run_command: git-credential-manager-core get protocol=https host=github.com username=token password=secret - If there are multiple credentials, a random one (or the one set first?) is returned: ❯ (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve 11:25:41.553761 git.c:444 trace: built-in: git credential approve 11:25:41.554242 run-command.c:663 trace: run_command: 'git credential-manager-core store' 11:25:41.565748 git.c:704 trace: exec: git-credential-manager-core store 11:25:41.566218 run-command.c:663 trace: run_command: git-credential-manager-core store - In fact the username is completely ignored when getting credentials, at least for the GitHub provider: ❯ (echo url=https://token2@github.com; echo ) | git credential fill 11:29:49.274574 git.c:444 trace: built-in: git credential fill 11:29:49.275020 run-command.c:663 trace: run_command: 'git credential-manager-core get' 11:29:49.283563 git.c:704 trace: exec: git-credential-manager-core get 11:29:49.284236 run-command.c:663 trace: run_command: git-credential-manager-core get protocol=https host=github.com username=token password=secret This is because the username is not stored as part of the URL by the GitHub provider. Related issue: `manager` has a similar problem, it is linked from this GitHub issue. - If you set the provider to Generic, then usernames work as expected. In this case, this is stored in the keychain: Name: git:https://token@github.com/ Kind: application password Account: token Where: git:https://token@github.com/ These credentials are not compatible with the ones set by the GitHub provider. ❯ export GCM_PROVIDER=generic ❯ (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve 11:34:15.998644 git.c:444 trace: built-in: git credential approve 11:34:15.998992 run-command.c:663 trace: run_command: 'git credential-manager-core store' 11:34:16.008178 git.c:704 trace: exec: git-credential-manager-core store 11:34:16.008834 run-command.c:663 trace: run_command: git-credential-manager-core store ❯ (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve 11:35:52.629963 git.c:444 trace: built-in: git credential approve 11:35:52.637966 run-command.c:663 trace: run_command: 'git credential-manager-core store' 11:35:52.648058 git.c:704 trace: exec: git-credential-manager-core store 11:35:52.648514 run-command.c:663 trace: run_command: git-credential-manager-core store ❯ (echo url=https://token@github.com; echo ) | git credential fill 11:35:58.336428 git.c:444 trace: built-in: git credential fill 11:35:58.336881 run-command.c:663 trace: run_command: 'git credential-manager-core get' 11:35:58.345187 git.c:704 trace: exec: git-credential-manager-core get 11:35:58.345729 run-command.c:663 trace: run_command: git-credential-manager-core get protocol=https host=github.com username=token password=secret ❯ (echo url=https://token2@github.com; echo ) | git credential fill 11:36:02.550339 git.c:444 trace: built-in: git credential fill 11:36:02.550695 run-command.c:663 trace: run_command: 'git credential-manager-core get' 11:36:02.557777 git.c:704 trace: exec: git-credential-manager-core get 11:36:02.558359 run-command.c:663 trace: run_command: git-credential-manager-core get protocol=https host=github.com username=token2 password=secret2 ## `manager-core` (macOS), 2.0.246-beta or later - This version adds better support for multiple credentials for the same host. - It does not save the `username` as part of the service any more, for the generic provider. - It does save the `username` as `account` and it uses it when searching for credentials, both for the `generic` and the `github` provider. ## `manager` (GitHub authority) - This was Git Credential Manager for Windows, and it is the default helper on Windows for git 2.28.0. In git 2.29.0 it is not the default any more, and it is deprecated in favor of `manager-core`. It is still installed, though. - Docs: - It is installed as a git subcommand, so it can be called directly: ❯ $env:GIT_TRACE="true" ❯ git credential-manager 12:15:45.554298 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 12:15:45.556249 git.c:704 trace: exec: git-credential-manager 12:15:45.556249 run-command.c:663 trace: run_command: git-credential-manager usage: git-credential-manager.exe [approve|clear|config|delete|deploy|erase|fill|get|install|reject|remove|store|uninstall|version] [] fatal: Unable to open help documentation. - Setting credentials. The `username` must be provided, and it is stored in the credential, but the GitHub provider overwrites credentials with the same host name, even if they have a different `username`. I.e. after this, there will be only one credential: ❯ % { echo url=https://github.com; echo username=token; echo password=secret; echo "" } | git credential approve 13:16:58.118611 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:16:58.120617 git.c:444 trace: built-in: git credential approve 13:16:58.122612 run-command.c:663 trace: run_command: 'git credential-manager store' 13:16:58.170625 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:16:58.173087 git.c:704 trace: exec: git-credential-manager store 13:16:58.173087 run-command.c:663 trace: run_command: git-credential-manager store ❯ % { echo url=https://github.com; echo username=token2; echo password=secret2; echo "" } | git credential approve 13:17:30.925970 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:17:30.928134 git.c:444 trace: built-in: git credential approve 13:17:30.928987 run-command.c:663 trace: run_command: 'git credential-manager store' 13:17:30.968981 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:17:30.970974 git.c:704 trace: exec: git-credential-manager store 13:17:30.970974 run-command.c:663 trace: run_command: git-credential-manager store - It stores credentials in the following format: Internet or network address: git:https://github.com User name: token Password: **** Persistence: Local computer \ - It has multiple authorities, (like providers for `manager-core`). The GitHub authority is used for `github.com` URLs by default. The authority can be set with a config option or an env var. - We need to set the `GCM_VALIDATE` env var to `false` , otherwise it tries to validate the GitHub token every time we query it. ❯ $env:GCM_VALIDATE="false" - No `username` is needed for getting *the* credential, for the GitHub provider. ❯ % { echo url=https://github.com; echo "" } | git credential fill 13:24:43.831089 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:24:43.833666 git.c:444 trace: built-in: git credential fill 13:24:43.834092 run-command.c:663 trace: run_command: 'git credential-manager get' 13:24:43.872087 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:24:43.874092 git.c:704 trace: exec: git-credential-manager get 13:24:43.874092 run-command.c:663 trace: run_command: git-credential-manager get protocol=https host=github.com path= username=token2 password=secret2 In fact the `username` is ignored for the GitHub provider: ❯ % { echo url=https://token@github.com; echo "" } | git credential fill 13:25:06.029084 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:25:06.031084 git.c:444 trace: built-in: git credential fill 13:25:06.032081 run-command.c:663 trace: run_command: 'git credential-manager get' 13:25:06.069085 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:25:06.070086 git.c:704 trace: exec: git-credential-manager get 13:25:06.070086 run-command.c:663 trace: run_command: git-credential-manager get protocol=https host=github.com path= username=token2 password=secret2 - Supports namespaces, the default namespace is `git`. The namespace is included in the name of the credential: `namespace:URL`. - There is a pull request to include the `username` in the URL as well: ## `manager` (Basic authority) - `Basic` authority can be configured with an env var or config option: ❯ $env:GCM_AUTHORITY="Basic" - It is compatible with the credentials set by the `GitHub` authority: ❯ % { echo url=https://github.com; echo "" } | git credential fill 13:27:17.344396 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:27:17.346397 git.c:444 trace: built-in: git credential fill 13:27:17.347388 run-command.c:663 trace: run_command: 'git credential-manager get' 13:27:17.386388 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:27:17.388386 git.c:704 trace: exec: git-credential-manager get 13:27:17.388386 run-command.c:663 trace: run_command: git-credential-manager get protocol=https host=github.com path= username=token2 password=secret2 - But it supports usernames as well. Note that since the username is stored in the URL, it does not return some arbitrary credentials if we query the URL without a username. We need to tell GCM here not to ask for a password, but just fall back to the next credential helper. ❯ $env:GCM_INTERACTIVE="Never" ❯ % { echo url=https://token2@github.com; echo "" } | git -c credential.helper="! echo protocol=dummy; echo host=dummy; echo username=dummy; echo password=dummy" credential fill 13:31:16.682545 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:31:16.684473 git.c:444 trace: built-in: git credential fill 13:31:16.685471 run-command.c:663 trace: run_command: 'git credential-manager get' 13:31:16.725486 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:31:16.727472 git.c:704 trace: exec: git-credential-manager get 13:31:16.727472 run-command.c:663 trace: run_command: git-credential-manager get Logon failed, use ctrl+c to cancel basic credential prompt. 13:31:16.948468 run-command.c:663 trace: run_command: ' echo protocol=dummy; echo host=dummy; echo username=dummy; echo password=dummy get' protocol=dummy host=dummy username=dummy password=dummy get - When setting credentials, usernames are included in the URL: ❯ % { echo url=https://github.com; echo username=token; echo password=secret; echo "" } | git credential approve 13:34:37.937934 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:34:37.940820 git.c:444 trace: built-in: git credential approve 13:34:37.940995 run-command.c:663 trace: run_command: 'git credential-manager store' 13:34:37.983372 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:34:37.984737 git.c:704 trace: exec: git-credential-manager store 13:34:37.984737 run-command.c:663 trace: run_command: git-credential-manager store will create this record in the credential store: Internet or network address: git:https://token@github.com User name: token Password: **** Persistence: Local computer and then this can be queried with an explicit username: ❯ % { echo url=https://token@github.com; echo "" } | git -c credential.helper="! echo protocol=dummy; echo host=dummy; echo username=dummy; echo password=dummy" credential fill 13:36:42.789268 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin 13:36:42.791270 git.c:444 trace: built-in: git credential fill 13:36:42.792266 run-command.c:663 trace: run_command: 'git credential-manager get' 13:36:42.830475 exec-cmd.c:237 trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core 13:36:42.832279 git.c:704 trace: exec: git-credential-manager get 13:36:42.832279 run-command.c:663 trace: run_command: git-credential-manager get protocol=https host=github.com path= username=token password=secret ## `manager-core` (on Windows, before version 2.0.246-beta) - Not installed by default (git 2.28.0), but this is supposed to be the future canonical implementation. See above for the macOS version. - It works similarly to `manager` , I haven't seen any differences in behavior (version was 2.0.194.40577). Instead of authorities, we need to set up providers: . The `Generic` provider works the same way as the `Basic` authority in `manager`. - Unfortunately setting `GCM_AUTHORITY` will make `manager-core` break, so it is not possible to use both `manager` and `manager-core` if you need to set this environment variable. ## `manager-core` (on Windows, from 2.0.246-beta) - This is now the version that is the default helper in git 2.29.0 and later. `manager` is still installed, but the default system config sets `manager-core`. - It supports multiple users better: - When storing a credential, it first checks if there is a target name without a username. If there is, then it uses the target name without the username. It stores the username in the `username` field of the credential, still. so it is not lost. - When looking up a credential without a username, it will return the first credential it can find, even if the target name contains a username, and there is another credential without a username in the target name. - When looking up a credential with a username, it may return any credential with a matching username, and a target name that matches the host. The username inside the target name does not actually matter. # Recommendations for multiple accounts ## macOS 1. Use the `osxkeychain` helper. 2. FIrst remove all your current credentials for the host you are targeting. E.g. for GitHub, search for "Internet Passwords" for github.com, or use `gitcreds::gitcreds_list()` and the oskeyring package to remove them. You can also use the oskeyring package to back up the tokens and passwords. 3. Then add the credential that you want to use for "generic access". This is the credential that will be used for URLs without user names. The user name for this credential does not matter, but you can choose something descriptive, e.g. "PersonalAccessToken", "token", or "generic". 4. Configure git to use this username by default. E.g. if you chose "generic", then run git config --global credential.username generic 5. Add all the other credentials, with appropriate user names. These are the user names that you need to put in the URLs for the repositories or operations you want to use them for. (GitHub does not actually use the user names if the password is a PAT.) ## Windows, with git 2.29.0 or later and manager-core 1. We suggest that you update to the latest git version, but at least 2.29.0, and use the `manager-core` helper which is now default. If you installed `manager-core` separately from git, we suggest that you remove it, because it might cause confusion as to which helper is actually used. 2. Remove all current credentials first, for the host you are targeting. You can do this in 'Credential Manager' or `gitcreds::gitcreds_list()` to find them and the oskeyring package to remove them. You can also use the oskeyring packaeg to back up the tokens and passwords. 3. Then add the credential that you want to use for "generic access". This is the credential that will be used for URLs without user names. The user name for this credential does not matter, but you can choose something descriptive, e.g. "PersonalAccessToken", "token", or "generic". 4. Configure git to use this username by default. E.g. if you chose "generic", then run git config --global credential.username generic 5. Add all the other credentials, with appropriate user names. These are the user names that you need to put in the URLs for the repositories or operations you want to use them for. (GitHub does not actually use the user names if the password is a PAT.) ## Windows with older git versions ### At most one github.com credential If you only need to manage a single github.com credential, together with possibly multiple credentials to other hosts (including GitHub Enterprise hosts), then you can use the default `manager` helper, and get away with the default auto-detected GCM authority setting. In this case, you can add you github.com credential with an arbitrary user name, and for each other host you can add configure a default user name, and/or include user names in the URLs to these hosts. This is how to set a default user name for a host: git config --global credential.https://example.com.username myusername ### Multiple GitHub credentials If you need to manage multiple github.com credentials, then you can still use the `manager` helper, but you need to change the GCM authority by setting an option or an environment variable, see Once is merged, you won't need to do this. (At least in recent git versions, that contain a GCM build with the fix.) This is how to change the config for this: git config --global credential.authority Basic You can also change it only for github.com: git config --global credential.github.com.authority Basic Then you can configure a default user name, this will be used for URLs without a user name: git config --global credential.username generic Now you can add you credentials, the default one with the "generic" user name, and all the others with their specific user and host names. gitcreds/inst/doc/package.Rmd0000644000176200001440000001527214210415267015650 0ustar liggesusers--- title: "gitcreds for package authors" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{gitcreds for package authors} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} editor_options: markdown: wrap: sentence canonical: true --- ## Introduction If you have a package that queries the GitHub API, or uses git with remote git repositories, then most likely you need to let your users specify their GitHub or git credentials. There are several benefits of using gitcreds to do this: - (Re)use the same credentials as command line git, R and the RStudio IDE., etc. Users can set their GitHub token once and use it everywhere. - Users can use the same credentials for multiple R packages. - gitcreds has a cache that makes credential lookup very fast. - Typically more secure than storing passwords and tokens in `.Renviron` files. - gitcreds supports multiple users and multiple hosts. - If git or credential helpers are not available, e.g. typically on a Linux server, then gitcreds can still use environment variables, and it still supports multiple users and hosts. ## The simple API The simplest way to use gitcreds is to call `gitcreds_get()` from your package to query credentials, possibly with a custom URL. For setting new credentials, you can point your users to `gitcreds_set()`. ### Errors from the simple API If you are using the simple API, gitcreds may throw the following classed errors and your package might want to handle: - `gitcreds_nogit_error` if git it not available on the system. - `gitcreds_no_credentials` if git did not find any credentials for the specified URL. The URL is stored in the error, under `url`. - `git_error` if a git command returned some error. The following information is stored in the error object: - `args` the command line arguments to git, - `stdout` standard output, - `stderr` standard error, - `status` the exit status of the git process. - `gitcreds_not_interactive_error` if `gitcreds_set()` is called in non-interactive mode. - `gitcreds_abort_replace_error` if the user aborted replacing the existing credentials. ## The low level API Should you need more flexibility, you can use the `gitcreds_approve()`, `gitcreds_fill()` and `gitcreds_reject()` functions, to add/update, query and remove credentials. We suggest you use the dummy credential helper (see below) for `gitcreds_fill()`, to avoid git password dialog boxes if a credential is not available. E.g. the low level API makes it possible to implement an alternative to `gitcreds_set()` , with a different user interface, or a version that also works in non-interactive sessions. ### The dummy credential helper In a typical setup, if git does not find credentials for the requested host after querying all defined credential helpers, it'll ask for a password in a dialog box, or a terminal prompt. It is often best to avoid these, and deal with the situation within R. gitcreds has a dummy credential helper, that always supplies dummy credentials. By default `gitcreds_fill()` adds this dummy helper to the list of configured credential helpers, and code calling `gitcreds_fill()` can check if git returned the dummy credentials, meaning that no real credentials were found. This is how the dummy credentials look: ``` {.r} gitcreds_fill(list(url="https://impossible.com")) #> [1] "protocol=dummy" "host=dummy" #> [3] "username=dummy" "password=dummy get" ``` It is best to look for `protocol=dummy` as the first line of the git output. ### Errors from the low level API - `git_error` if a git command returned some error. The following information is stored in the error object: - `args` the command line arguments to git, - `stdout` standard output, - `stderr` standard error, - `status` the exit status of the git process. ## Testing If your package uses gitcreds, either directly, or through another package, then you might want to test your package for the the various possible states of the user's git installation and credential store. gitcreds has some facilities to help you with this. If you want to test your package for a specific output from gitcreds, you can temporarily set the environment variable that gitcreds uses as a cache to the desired value. Use the `gitcreds_cache_envvar()` function to see which environment variable you need to set for a url: ```{r} gitcreds::gitcreds_cache_envvar("https://github.com") ``` It is easiest to use the withr package to temporarily change this environment variable in a test case: ```{r} library(testthat) test_that("bad credentials from git", { withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "bad")) # Test code that calls gitcreds_get(), potentially downstream. # gitcreds_get() will return `bad` as the password. # Illustration: expect_equal( gitcreds::gitcreds_get("https://github.com")$password, "bad" ) }) ``` If you want gitcreds to return a specific credential record, then you can specify the fields of the record in the environment variable, separated by colons. For example: ```{r} library(testthat) test_that("another GitHub user", { cred <- paste0( "protocol:https:", "host:github.com:", "username:user1:", "password:secret" ) withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = cred)) # Your test code comes here. This is just an illustration: print(gitcreds::gitcreds_get()) expect_equal(gitcreds::gitcreds_get()$username, "user1") }) ``` If you want gitcreds to fail for a specific host, set the corresponding environment variable to `"FAIL"`: ```{r} library(testthat) test_that("no credentials from git", { withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "FAIL")) # The test code that calls gitcreds_get() comes here. # It will fail with error "gitcreds_no_credentials" expect_error( gitcreds::gitcreds_get("https://github.com"), class = "gitcreds_no_credentials" ) }) ``` If you want gitcreds to fail with a specific error, then include the error class after a `"FAIL:"` prefix, in the environment variable. See the list of possible error classes above. For example: ```{r} library(testthat) test_that("no git installation", { withr::local_envvar(c( GITHUB_PAT_GITHUB_COM = "FAIL:gitcreds_nogit_error" )) # Test code that calls gitcreds_get() comes here. # Illustration: expect_error( gitcreds::gitcreds_get("https://github.com"), class = "gitcreds_nogit_error" ) }) ``` It is not currently possible to simulate the additional data in the error object, e.g. the standard output of a failed git command. If you need this for a test case, then your test case can call `gitcreds_get()` directly and you can use the mockery package to make gitcreds fail with the desired error object. gitcreds/inst/doc/package.html0000644000176200001440000005600214306341667016076 0ustar liggesusers gitcreds for package authors

gitcreds for package authors

Introduction

If you have a package that queries the GitHub API, or uses git with remote git repositories, then most likely you need to let your users specify their GitHub or git credentials. There are several benefits of using gitcreds to do this:

  • (Re)use the same credentials as command line git, R and the RStudio IDE., etc. Users can set their GitHub token once and use it everywhere.

  • Users can use the same credentials for multiple R packages.

  • gitcreds has a cache that makes credential lookup very fast.

  • Typically more secure than storing passwords and tokens in .Renviron files.

  • gitcreds supports multiple users and multiple hosts.

  • If git or credential helpers are not available, e.g. typically on a Linux server, then gitcreds can still use environment variables, and it still supports multiple users and hosts.

The simple API

The simplest way to use gitcreds is to call gitcreds_get() from your package to query credentials, possibly with a custom URL. For setting new credentials, you can point your users to gitcreds_set().

Errors from the simple API

If you are using the simple API, gitcreds may throw the following classed errors and your package might want to handle:

  • gitcreds_nogit_error if git it not available on the system.

  • gitcreds_no_credentials if git did not find any credentials for the specified URL. The URL is stored in the error, under url.

  • git_error if a git command returned some error. The following information is stored in the error object:

    • args the command line arguments to git,

    • stdout standard output,

    • stderr standard error,

    • status the exit status of the git process.

  • gitcreds_not_interactive_error if gitcreds_set() is called in non-interactive mode.

  • gitcreds_abort_replace_error if the user aborted replacing the existing credentials.

The low level API

Should you need more flexibility, you can use the gitcreds_approve(), gitcreds_fill() and gitcreds_reject() functions, to add/update, query and remove credentials. We suggest you use the dummy credential helper (see below) for gitcreds_fill(), to avoid git password dialog boxes if a credential is not available.

E.g. the low level API makes it possible to implement an alternative to gitcreds_set() , with a different user interface, or a version that also works in non-interactive sessions.

The dummy credential helper

In a typical setup, if git does not find credentials for the requested host after querying all defined credential helpers, it’ll ask for a password in a dialog box, or a terminal prompt. It is often best to avoid these, and deal with the situation within R.

gitcreds has a dummy credential helper, that always supplies dummy credentials. By default gitcreds_fill() adds this dummy helper to the list of configured credential helpers, and code calling gitcreds_fill() can check if git returned the dummy credentials, meaning that no real credentials were found. This is how the dummy credentials look:

gitcreds_fill(list(url="https://impossible.com"))
#> [1] "protocol=dummy"     "host=dummy"
#> [3] "username=dummy"     "password=dummy get"

It is best to look for protocol=dummy as the first line of the git output.

Errors from the low level API

  • git_error if a git command returned some error. The following information is stored in the error object:

    • args the command line arguments to git,

    • stdout standard output,

    • stderr standard error,

    • status the exit status of the git process.

Testing

If your package uses gitcreds, either directly, or through another package, then you might want to test your package for the the various possible states of the user’s git installation and credential store. gitcreds has some facilities to help you with this.

If you want to test your package for a specific output from gitcreds, you can temporarily set the environment variable that gitcreds uses as a cache to the desired value. Use the gitcreds_cache_envvar() function to see which environment variable you need to set for a url:

gitcreds::gitcreds_cache_envvar("https://github.com")
## [1] "GITHUB_PAT_GITHUB_COM"

It is easiest to use the withr package to temporarily change this environment variable in a test case:

library(testthat)
test_that("bad credentials from git", {
  withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "bad"))
  # Test code that calls gitcreds_get(), potentially downstream.
  # gitcreds_get() will return `bad` as the password.
  # Illustration:
  expect_equal(
    gitcreds::gitcreds_get("https://github.com")$password,
    "bad"
  )
})
## Test passed 🥇

If you want gitcreds to return a specific credential record, then you can specify the fields of the record in the environment variable, separated by colons. For example:

library(testthat)
test_that("another GitHub user", {
  cred <- paste0(
    "protocol:https:",
    "host:github.com:",
    "username:user1:",
    "password:secret"
  )
  withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = cred))
  # Your test code comes here. This is just an illustration:
  print(gitcreds::gitcreds_get())
  expect_equal(gitcreds::gitcreds_get()$username, "user1")
})
## <gitcreds>
##   protocol: https
##   host    : github.com
##   username: user1
##   password: <-- hidden -->
## Test passed 🌈

If you want gitcreds to fail for a specific host, set the corresponding environment variable to "FAIL":

library(testthat)
test_that("no credentials from git", {
  withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "FAIL"))
  # The test code that calls gitcreds_get() comes here.
  # It will fail with error "gitcreds_no_credentials"
  expect_error(
    gitcreds::gitcreds_get("https://github.com"),
    class = "gitcreds_no_credentials"
  )
})
## Test passed 😸

If you want gitcreds to fail with a specific error, then include the error class after a "FAIL:" prefix, in the environment variable. See the list of possible error classes above. For example:

library(testthat)
test_that("no git installation", {
  withr::local_envvar(c(
    GITHUB_PAT_GITHUB_COM = "FAIL:gitcreds_nogit_error"
  ))
  # Test code that calls gitcreds_get() comes here.
  # Illustration:
  expect_error(
    gitcreds::gitcreds_get("https://github.com"),
    class = "gitcreds_nogit_error"
  )
})
## Test passed 🎊

It is not currently possible to simulate the additional data in the error object, e.g. the standard output of a failed git command. If you need this for a test case, then your test case can call gitcreds_get() directly and you can use the mockery package to make gitcreds fail with the desired error object.

gitcreds/inst/doc/helper-survey.html0000644000176200001440000016555014306341666017325 0ustar liggesusers git credential helpers

git credential helpers

General remarks

  • For git credential approve git (2.28.0 macOS) does not even call the credential helper if no username is supplied:

     export GIT_TRACE=true
     (echo url=https://github.com; echo password=secret; echo ) | git credential approve
    10:43:36.712290 git.c:444               trace: built-in: git credential approve
    
  • The credential.helper key has a multi-set value, so if you add a new value, the old values are still kept. From git 2.9 specifying an empty string removes the previously defined helpers.

Credential helper survey

We do this with an eye of supporting usernames and multiple users.

cache

  • Docs: https://git-scm.com/docs/git-credential-cache

  • This helper is not included in the default git installer on Windows . :(

  • This is how we can set up cache for a particular repository:

    ❯ mkdir foo
    ❯ cd foo
    ❯ git init .
    11:43:28.618841 git.c:444               trace: built-in: git init .
    Initialized empty Git repository in /private/tmp/foo/.git/
    
    ❯ git config --add credential.helper ""
    11:43:50.682962 git.c:444               trace: built-in: git config --add credential.helper ''
    
    ❯ git config --add credential.helper cache
    11:43:54.577707 git.c:444               trace: built-in: git config --add credential.helper cache
    
    ❯ cat .git/config
    [core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
        ignorecase = true
        precomposeunicode = true
    [credential]
        helper =
        helper = cache
  • Now let’s add credentials:

    ❯  (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve
    11:45:16.813913 git.c:444               trace: built-in: git credential approve
    11:45:16.814431 run-command.c:663       trace: run_command: 'git credential-cache store'
    11:45:16.823008 git.c:704               trace: exec: git-credential-cache store
    11:45:16.823637 run-command.c:663       trace: run_command: git-credential-cache store
    11:45:16.842902 run-command.c:663       trace: run_command: git-credential-cache--daemon /Users/gaborcsardi/.cache/git/credential/socket
    
    ❯  (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve
    11:45:28.927712 git.c:444               trace: built-in: git credential approve
    11:45:28.928108 run-command.c:663       trace: run_command: 'git credential-cache store'
    11:45:28.937087 git.c:704               trace: exec: git-credential-cache store
    11:45:28.937695 run-command.c:663       trace: run_command: git-credential-cache store
  • Query with username works correctly:

    ❯ (echo url=https://token@github.com; echo ) | git credential fill
    11:46:40.689122 git.c:444               trace: built-in: git credential fill
    11:46:40.689638 run-command.c:663       trace: run_command: 'git credential-cache get'
    11:46:40.696784 git.c:704               trace: exec: git-credential-cache get
    11:46:40.697333 run-command.c:663       trace: run_command: git-credential-cache get
    protocol=https
    host=github.com
    username=token
    password=secret
    
    ❯ (echo url=https://token2@github.com; echo ) | git credential fill
    11:46:43.767002 git.c:444               trace: built-in: git credential fill
    11:46:43.767676 run-command.c:663       trace: run_command: 'git credential-cache get'
    11:46:43.778637 git.c:704               trace: exec: git-credential-cache get
    11:46:43.779201 run-command.c:663       trace: run_command: git-credential-cache get
    protocol=https
    host=github.com
    username=token2
    password=secret2
  • Query without username works, and returns some credential:

    ❯ (echo url=https://github.com; echo ) | git credential fill
    11:45:58.200272 git.c:444               trace: built-in: git credential fill
    11:45:58.200667 run-command.c:663       trace: run_command: 'git credential-cache get'
    11:45:58.208372 git.c:704               trace: exec: git-credential-cache get
    11:45:58.208919 run-command.c:663       trace: run_command: git-credential-cache get
    protocol=https
    host=github.com
    username=token
    password=secret
    
    ❯ (echo url=https://token@github.com; echo ) | git credential reject
    11:47:03.921697 git.c:444               trace: built-in: git credential reject
    11:47:03.922530 run-command.c:663       trace: run_command: 'git credential-cache erase'
    11:47:03.935858 git.c:704               trace: exec: git-credential-cache erase
    11:47:03.936400 run-command.c:663       trace: run_command: git-credential-cache erase
    
    ❯ (echo url=https://github.com; echo ) | git credential fill
    11:47:10.018877 git.c:444               trace: built-in: git credential fill
    11:47:10.019386 run-command.c:663       trace: run_command: 'git credential-cache get'
    11:47:10.027990 git.c:704               trace: exec: git-credential-cache get
    11:47:10.028572 run-command.c:663       trace: run_command: git-credential-cache get
    protocol=https
    host=github.com
    username=token2
    password=secret2

store

  • Docs: https://git-scm.com/docs/git-credential-store

  • Configure for a repo:

    ❯ mkdir foo
    ❯ cd foo
    ❯ git init .
    11:53:48.042569 git.c:444               trace: built-in: git init .
    Initialized empty Git repository in /private/tmp/foo/.git/
    
    ❯ git config --add credential.helper ""
    11:53:52.949914 git.c:444               trace: built-in: git config --add credential.helper ''
    
    ❯ git config --add credential.helper store
    11:53:56.682348 git.c:444               trace: built-in: git config --add credential.helper store
    
    ❯ cat .git/config
    [core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
        ignorecase = true
        precomposeunicode = true
    [credential]
        helper =
        helper = store
  • Add credentials:

    ❯  (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve
    11:54:44.184929 git.c:444               trace: built-in: git credential approve
    11:54:44.185729 run-command.c:663       trace: run_command: 'git credential-store store'
    11:54:44.197920 git.c:704               trace: exec: git-credential-store store
    11:54:44.198471 run-command.c:663       trace: run_command: git-credential-store store
    
    /tmp/foo master
    ❯  (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve
    11:54:48.452942 git.c:444               trace: built-in: git credential approve
    11:54:48.453399 run-command.c:663       trace: run_command: 'git credential-store store'
    11:54:48.463535 git.c:704               trace: exec: git-credential-store store
    11:54:48.464004 run-command.c:663       trace: run_command: git-credential-store store
  • Query with username:

    ❯ (echo url=https://token@github.com; echo ) | git credential fill
    11:55:21.191654 git.c:444               trace: built-in: git credential fill
    11:55:21.192357 run-command.c:663       trace: run_command: 'git credential-store get'
    11:55:21.204279 git.c:704               trace: exec: git-credential-store get
    11:55:21.205063 run-command.c:663       trace: run_command: git-credential-store get
    protocol=https
    host=github.com
    username=token
    password=secret
    
    ❯ (echo url=https://token2@github.com; echo ) | git credential fill
    11:55:24.194096 git.c:444               trace: built-in: git credential fill
    11:55:24.194654 run-command.c:663       trace: run_command: 'git credential-store get'
    11:55:24.207028 git.c:704               trace: exec: git-credential-store get
    11:55:24.207643 run-command.c:663       trace: run_command: git-credential-store get
    protocol=https
    host=github.com
    username=token2
    password=secret2
  • Query without username returns some credentials, apparently not the ones that were set first:

    ❯ (echo url=https://github.com; echo ) | git credential fill
    11:56:12.394594 git.c:444               trace: built-in: git credential fill
    11:56:12.394949 run-command.c:663       trace: run_command: 'git credential-store get'
    11:56:12.403303 git.c:704               trace: exec: git-credential-store get
    11:56:12.403863 run-command.c:663       trace: run_command: git-credential-store get
    protocol=https
    host=github.com
    username=token2
    password=secret2
    
    ❯ (echo url=https://token2@github.com; echo ) | git credential reject
    11:56:24.065910 git.c:444               trace: built-in: git credential reject
    11:56:24.066314 run-command.c:663       trace: run_command: 'git credential-store erase'
    11:56:24.074851 git.c:704               trace: exec: git-credential-store erase
    11:56:24.076875 run-command.c:663       trace: run_command: git-credential-store erase
    
    ❯ (echo url=https://github.com; echo ) | git credential fill
    11:56:26.438444 git.c:444               trace: built-in: git credential fill
    11:56:26.438839 run-command.c:663       trace: run_command: 'git credential-store get'
    11:56:26.446181 git.c:704               trace: exec: git-credential-store get
    11:56:26.446721 run-command.c:663       trace: run_command: git-credential-store get
    protocol=https
    host=github.com
    username=token
    password=secret

osxkeychain

  • Some docs: https://docs.github.com/en/github/using-git/updating-credentials-from-the-osx-keychain

  • This is the default helper on macOS currently (git 2.28.0).

  • This is how it stores a credential:

    Name: github.com
    Kind: Internet password
    Account: token
    Where: https://github.com
  • It installs as a git subcommand, so it is possible to call its internal api directly:

    ❯ git credential-osxkeychain
    11:50:56.325499 git.c:704               trace: exec: git-credential-osxkeychain
    11:50:56.325783 run-command.c:663       trace: run_command: git-credential-osxkeychain
    usage: git credential-osxkeychain <get|store|erase>
  • As always, needs username when setting the credential.

  • No need to supply username to get some token that matches the host. This is in a clean keychain. First we set two credentials:

     (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve
    10:48:47.187164 git.c:444               trace: built-in: git credential approve
    10:48:47.187691 run-command.c:663       trace: run_command: 'git credential-osxkeychain store'
    10:48:47.197964 git.c:704               trace: exec: git-credential-osxkeychain store
    10:48:47.198518 run-command.c:663       trace: run_command: git-credential-osxkeychain store
    
     (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve
    10:48:55.299933 git.c:444               trace: built-in: git credential approve
    10:48:55.300282 run-command.c:663       trace: run_command: 'git credential-osxkeychain store'
    10:48:55.308568 git.c:704               trace: exec: git-credential-osxkeychain store
    10:48:55.309276 run-command.c:663       trace: run_command: git-credential-osxkeychain store

    If we don’t supply a username, then we’ll just get one of them:

     (echo url=https://github.com; echo ) | git credential fill
    10:49:17.371636 git.c:444               trace: built-in: git credential fill
    10:49:17.372021 run-command.c:663       trace: run_command: 'git credential-osxkeychain get'
    10:49:17.378688 git.c:704               trace: exec: git-credential-osxkeychain get
    10:49:17.379164 run-command.c:663       trace: run_command: git-credential-osxkeychain get
    protocol=https
    host=github.com
    username=token
    password=secret

    If we supply the username, then we’ll get the correct one:

     (echo url=https://token2@github.com; echo ) | git credential fill
    10:49:28.613779 git.c:444               trace: built-in: git credential fill
    10:49:28.614108 run-command.c:663       trace: run_command: 'git credential-osxkeychain get'
    10:49:28.621440 git.c:704               trace: exec: git-credential-osxkeychain get
    10:49:28.621979 run-command.c:663       trace: run_command: git-credential-osxkeychain get
    protocol=https
    host=github.com
    username=token2
    password=secret2

    To check that without a username we get an arbitrary one, let’s remove token:

     (echo url=https://token@github.com; echo ) | git credential reject
    10:49:58.584332 git.c:444               trace: built-in: git credential reject
    10:49:58.586880 run-command.c:663       trace: run_command: 'git credential-osxkeychain erase'
    10:49:58.598463 git.c:704               trace: exec: git-credential-osxkeychain erase
    10:49:58.599214 run-command.c:663       trace: run_command: git-credential-osxkeychain erase
    
    ```sh
     (echo url=https://github.com; echo ) | git credential fill
    10:50:07.468385 git.c:444               trace: built-in: git credential fill
    10:50:07.468728 run-command.c:663       trace: run_command: 'git credential-osxkeychain get'
    10:50:07.478398 git.c:704               trace: exec: git-credential-osxkeychain get
    10:50:07.478832 run-command.c:663       trace: run_command: git-credential-osxkeychain get
    protocol=https
    host=github.com
    username=token2
    password=secret2

    Now let’s re-add token to make sure that osxkeychain does not prefer token:

     (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve
    10:58:52.302066 git.c:444               trace: built-in: git credential approve
    10:58:52.311063 run-command.c:663       trace: run_command: 'git credential-osxkeychain store'
    10:58:52.321633 git.c:704               trace: exec: git-credential-osxkeychain store
    10:58:52.322108 run-command.c:663       trace: run_command: git-credential-osxkeychain store
    
     (echo url=https://github.com; echo ) | git credential fill
    10:58:57.316418 git.c:444               trace: built-in: git credential fill
    10:58:57.317630 run-command.c:663       trace: run_command: 'git credential-osxkeychain get'
    10:58:57.330142 git.c:704               trace: exec: git-credential-osxkeychain get
    10:58:57.330697 run-command.c:663       trace: run_command: git-credential-osxkeychain get
    protocol=https
    host=github.com
    username=token2
    password=secret2

    So it seems that osxkeychain will just find an arbitrary one, or the one that was added first.

manager-core (on macOS), before version 2.0.246-beta

  • Not installed by default (git 2.28.0).

  • Install from brew, according to the instructions: https://github.com/GitCredentialManager/git-credential-manager#macos-homebrew

  • If updates your global git config, adding these lines:

    [credential]
        helper = ""
    [credential "https://dev.azure.com"]
        useHttpPath = true
    [credential]
        helper = manager-core

    The helper = "" line deletes previous handlers. The system helper is kept as osxkeychain.

  • It installs as a git subcommand, and you can call its internal API directly:

    ❯ git credential-manager-core
    11:51:56.434300 git.c:704               trace: exec: git-credential-manager-core
    11:51:56.434496 run-command.c:663       trace: run_command: git-credential-manager-core
    Missing command.
    
    usage: git-credential-manager-core <command>
    
      Available commands:
        erase
        get
        store
    
        configure [--system]
        unconfigure [--system]
    
        --version, version
        --help, -h, -?
  • It is not compatible with the osxkeychain helper, because it uses different keys in the keychain.

  • It supports different providers. Providers are auto-detected by default. GitHub has its own provider, detected via the github.com URL. The provider can be configured user a git config key or an environment variable: https://github.com/GitCredentialManager/git-credential-manager/blob/master/docs/configuration.md

  • It does not currently supports namepaces (like manager).

  • This is how it stores a credential:

    Name: git:https://github.com
    Kind: application password
    Account: token
    Where: git:https://github.com
  • No need to supply username to get some credential:

    ❯ (echo url=https://github.com; echo ) | git credential fill
    11:24:47.750966 git.c:444               trace: built-in: git credential fill
    11:24:47.753268 run-command.c:663       trace: run_command: 'git credential-manager-core get'
    11:24:47.762249 git.c:704               trace: exec: git-credential-manager-core get
    11:24:47.762917 run-command.c:663       trace: run_command: git-credential-manager-core get
    protocol=https
    host=github.com
    username=token
    password=secret
  • If there are multiple credentials, a random one (or the one set first?) is returned:

    ❯  (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve
    11:25:41.553761 git.c:444               trace: built-in: git credential approve
    11:25:41.554242 run-command.c:663       trace: run_command: 'git credential-manager-core store'
    11:25:41.565748 git.c:704               trace: exec: git-credential-manager-core store
    11:25:41.566218 run-command.c:663       trace: run_command: git-credential-manager-core store
  • In fact the username is completely ignored when getting credentials, at least for the GitHub provider:

    ❯ (echo url=https://token2@github.com; echo ) | git credential fill
    11:29:49.274574 git.c:444               trace: built-in: git credential fill
    11:29:49.275020 run-command.c:663       trace: run_command: 'git credential-manager-core get'
    11:29:49.283563 git.c:704               trace: exec: git-credential-manager-core get
    11:29:49.284236 run-command.c:663       trace: run_command: git-credential-manager-core get
    protocol=https
    host=github.com
    username=token
    password=secret

    This is because the username is not stored as part of the URL by the GitHub provider. Related issue: https://github.com/GitCredentialManager/git-credential-manager/issues/160 manager has a similar problem, it is linked from this GitHub issue.

  • If you set the provider to Generic, then usernames work as expected. In this case, this is stored in the keychain:

    Name: git:https://token@github.com/
    Kind: application password
    Account: token
    Where: git:https://token@github.com/

    These credentials are not compatible with the ones set by the GitHub provider.

    ❯ export GCM_PROVIDER=generic
    
    ❯  (echo url=https://github.com; echo username=token; echo password=secret; echo ) | git credential approve
    11:34:15.998644 git.c:444               trace: built-in: git credential approve
    11:34:15.998992 run-command.c:663       trace: run_command: 'git credential-manager-core store'
    11:34:16.008178 git.c:704               trace: exec: git-credential-manager-core store
    11:34:16.008834 run-command.c:663       trace: run_command: git-credential-manager-core store
    
    ❯  (echo url=https://github.com; echo username=token2; echo password=secret2; echo ) | git credential approve
    11:35:52.629963 git.c:444               trace: built-in: git credential approve
    11:35:52.637966 run-command.c:663       trace: run_command: 'git credential-manager-core store'
    11:35:52.648058 git.c:704               trace: exec: git-credential-manager-core store
    11:35:52.648514 run-command.c:663       trace: run_command: git-credential-manager-core store
    
    ❯ (echo url=https://token@github.com; echo ) | git credential fill
    11:35:58.336428 git.c:444               trace: built-in: git credential fill
    11:35:58.336881 run-command.c:663       trace: run_command: 'git credential-manager-core get'
    11:35:58.345187 git.c:704               trace: exec: git-credential-manager-core get
    11:35:58.345729 run-command.c:663       trace: run_command: git-credential-manager-core get
    protocol=https
    host=github.com
    username=token
    password=secret
    
    ❯ (echo url=https://token2@github.com; echo ) | git credential fill
    11:36:02.550339 git.c:444               trace: built-in: git credential fill
    11:36:02.550695 run-command.c:663       trace: run_command: 'git credential-manager-core get'
    11:36:02.557777 git.c:704               trace: exec: git-credential-manager-core get
    11:36:02.558359 run-command.c:663       trace: run_command: git-credential-manager-core get
    protocol=https
    host=github.com
    username=token2
    password=secret2

manager-core (macOS), 2.0.246-beta or later

  • This version adds better support for multiple credentials for the same host.
  • It does not save the username as part of the service any more, for the generic provider.
  • It does save the username as account and it uses it when searching for credentials, both for the generic and the github provider.

manager (GitHub authority)

  • This was Git Credential Manager for Windows, and it is the default helper on Windows for git 2.28.0. In git 2.29.0 it is not the default any more, and it is deprecated in favor of manager-core. It is still installed, though.

  • Docs: https://github.com/Microsoft/Git-Credential-Manager-for-Windows

  • It is installed as a git subcommand, so it can be called directly:

    ❯ $env:GIT_TRACE="true"
    ❯ git credential-manager
    12:15:45.554298 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
    12:15:45.556249 git.c:704               trace: exec: git-credential-manager
    12:15:45.556249 run-command.c:663       trace: run_command: git-credential-manager
    usage: git-credential-manager.exe [approve|clear|config|delete|deploy|erase|fill|get|install|reject|remove|store|uninstall|version] [<args>]
    fatal: Unable to open help documentation.
  • Setting credentials. The username must be provided, and it is stored in the credential, but the GitHub provider overwrites credentials with the same host name, even if they have a different username. I.e. after this, there will be only one credential:

    ❯  %  { echo url=https://github.com; echo username=token; echo password=secret; echo "" } | git credential approve
    13:16:58.118611 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
    13:16:58.120617 git.c:444               trace: built-in: git credential approve
    13:16:58.122612 run-command.c:663       trace: run_command: 'git credential-manager store'
    13:16:58.170625 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core
    13:16:58.173087 git.c:704               trace: exec: git-credential-manager store
    13:16:58.173087 run-command.c:663       trace: run_command: git-credential-manager store
    
    ❯  %  { echo url=https://github.com; echo username=token2; echo password=secret2; echo "" } | git credential approve
    13:17:30.925970 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
    13:17:30.928134 git.c:444               trace: built-in: git credential approve
    13:17:30.928987 run-command.c:663       trace: run_command: 'git credential-manager store'
    13:17:30.968981 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core
    13:17:30.970974 git.c:704               trace: exec: git-credential-manager store
    13:17:30.970974 run-command.c:663       trace: run_command: git-credential-manager store
  • It stores credentials in the following format:

    Internet or network address: git:https://github.com
    User name: token
    Password: ****
    Persistence: Local computer

<!- - –>

  • It has multiple authorities, (like providers for manager-core). The GitHub authority is used for github.com URLs by default. The authority can be set with a config option or an env var.

  • We need to set the GCM_VALIDATE env var to false , otherwise it tries to validate the GitHub token every time we query it.

    ❯ $env:GCM_VALIDATE="false"
  • No username is needed for getting the credential, for the GitHub provider.

    ❯  %  { echo url=https://github.com; echo "" } | git credential fill
    13:24:43.831089 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
    13:24:43.833666 git.c:444               trace: built-in: git credential fill
    13:24:43.834092 run-command.c:663       trace: run_command: 'git credential-manager get'
    13:24:43.872087 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core
    13:24:43.874092 git.c:704               trace: exec: git-credential-manager get
    13:24:43.874092 run-command.c:663       trace: run_command: git-credential-manager get
    protocol=https
    host=github.com
    path=
    username=token2
    password=secret2

    In fact the username is ignored for the GitHub provider:

    ❯ %  { echo url=https://token@github.com; echo "" } | git credential fill
    13:25:06.029084 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
    13:25:06.031084 git.c:444               trace: built-in: git credential fill
    13:25:06.032081 run-command.c:663       trace: run_command: 'git credential-manager get'
    13:25:06.069085 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core
    13:25:06.070086 git.c:704               trace: exec: git-credential-manager get
    13:25:06.070086 run-command.c:663       trace: run_command: git-credential-manager get
    protocol=https
    host=github.com
    path=
    username=token2
    password=secret2
  • Supports namespaces, the default namespace is git. The namespace is included in the name of the credential: namespace:URL.

  • There is a pull request to include the username in the URL as well: https://github.com/microsoft/Git-Credential-Manager-for-Windows/pull/891

manager (Basic authority)

  • Basic authority can be configured with an env var or config option:

    ❯ $env:GCM_AUTHORITY="Basic"
  • It is compatible with the credentials set by the GitHub authority:

    ❯  %  { echo url=https://github.com; echo "" } | git credential fill
    13:27:17.344396 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
    13:27:17.346397 git.c:444               trace: built-in: git credential fill
    13:27:17.347388 run-command.c:663       trace: run_command: 'git credential-manager get'
    13:27:17.386388 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core
    13:27:17.388386 git.c:704               trace: exec: git-credential-manager get
    13:27:17.388386 run-command.c:663       trace: run_command: git-credential-manager get
    protocol=https
    host=github.com
    path=
    username=token2
    password=secret2
  • But it supports usernames as well. Note that since the username is stored in the URL, it does not return some arbitrary credentials if we query the URL without a username. We need to tell GCM here not to ask for a password, but just fall back to the next credential helper.

    ❯ $env:GCM_INTERACTIVE="Never"
    
    ❯ % { echo url=https://token2@github.com; echo "" } | git -c credential.helper="! echo protocol=dummy; echo host=dummy;
     echo username=dummy; echo password=dummy" credential fill
    13:31:16.682545 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
    13:31:16.684473 git.c:444               trace: built-in: git credential fill
    13:31:16.685471 run-command.c:663       trace: run_command: 'git credential-manager get'
    13:31:16.725486 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core
    13:31:16.727472 git.c:704               trace: exec: git-credential-manager get
    13:31:16.727472 run-command.c:663       trace: run_command: git-credential-manager get
    Logon failed, use ctrl+c to cancel basic credential prompt.
    13:31:16.948468 run-command.c:663       trace: run_command: ' echo protocol=dummy; echo host=dummy; echo username=dummy; echo password=dummy get'
    protocol=dummy
    host=dummy
    username=dummy
    password=dummy get
  • When setting credentials, usernames are included in the URL:

    ❯ % { echo url=https://github.com; echo username=token; echo password=secret; echo "" } | git credential approve
    13:34:37.937934 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
    13:34:37.940820 git.c:444               trace: built-in: git credential approve
    13:34:37.940995 run-command.c:663       trace: run_command: 'git credential-manager store'
    13:34:37.983372 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core
    13:34:37.984737 git.c:704               trace: exec: git-credential-manager store
    13:34:37.984737 run-command.c:663       trace: run_command: git-credential-manager store

    will create this record in the credential store:

    Internet or network address: git:https://token@github.com
    User name: token
    Password: ****
    Persistence: Local computer

    and then this can be queried with an explicit username:

    ❯  %  { echo url=https://token@github.com; echo "" } | git -c credential.helper="! echo protocol=dummy; echo host=dummy; echo username=dummy; echo password=dummy" credential fill
    13:36:42.789268 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
    13:36:42.791270 git.c:444               trace: built-in: git credential fill
    13:36:42.792266 run-command.c:663       trace: run_command: 'git credential-manager get'
    13:36:42.830475 exec-cmd.c:237          trace: resolved executable dir: C:/Program Files/Git/mingw64/libexec/git-core
    13:36:42.832279 git.c:704               trace: exec: git-credential-manager get
    13:36:42.832279 run-command.c:663       trace: run_command: git-credential-manager get
    protocol=https
    host=github.com
    path=
    username=token
    password=secret

manager-core (on Windows, before version 2.0.246-beta)

  • Not installed by default (git 2.28.0), but this is supposed to be the future canonical implementation. See above for the macOS version.

  • It works similarly to manager , I haven’t seen any differences in behavior (version was 2.0.194.40577). Instead of authorities, we need to set up providers: https://github.com/GitCredentialManager/git-credential-manager/blob/master/docs/migration.md. The Generic provider works the same way as the Basic authority in manager.

  • Unfortunately setting GCM_AUTHORITY will make manager-core break, so it is not possible to use both manager and manager-core if you need to set this environment variable.

manager-core (on Windows, from 2.0.246-beta)

  • This is now the version that is the default helper in git 2.29.0 and later. manager is still installed, but the default system config sets manager-core.
  • It supports multiple users better: https://github.com/GitCredentialManager/git-credential-manager/issues/160#issuecomment-700544889
  • When storing a credential, it first checks if there is a target name without a username. If there is, then it uses the target name without the username. It stores the username in the username field of the credential, still. so it is not lost.
  • When looking up a credential without a username, it will return the first credential it can find, even if the target name contains a username, and there is another credential without a username in the target name.
  • When looking up a credential with a username, it may return any credential with a matching username, and a target name that matches the host. The username inside the target name does not actually matter.

Recommendations for multiple accounts

macOS

  1. Use the osxkeychain helper.

  2. FIrst remove all your current credentials for the host you are targeting. E.g. for GitHub, search for “Internet Passwords” for github.com, or use gitcreds::gitcreds_list() and the oskeyring package to remove them. You can also use the oskeyring package to back up the tokens and passwords.

  3. Then add the credential that you want to use for “generic access”. This is the credential that will be used for URLs without user names. The user name for this credential does not matter, but you can choose something descriptive, e.g. “PersonalAccessToken”, “token”, or “generic”.

  4. Configure git to use this username by default. E.g. if you chose “generic”, then run

    git config --global credential.username generic
  5. Add all the other credentials, with appropriate user names. These are the user names that you need to put in the URLs for the repositories or operations you want to use them for. (GitHub does not actually use the user names if the password is a PAT.)

Windows, with git 2.29.0 or later and manager-core

  1. We suggest that you update to the latest git version, but at least 2.29.0, and use the manager-core helper which is now default. If you installed manager-core separately from git, we suggest that you remove it, because it might cause confusion as to which helper is actually used.

  2. Remove all current credentials first, for the host you are targeting. You can do this in ‘Credential Manager’ or gitcreds::gitcreds_list() to find them and the oskeyring package to remove them. You can also use the oskeyring packaeg to back up the tokens and passwords.

  3. Then add the credential that you want to use for “generic access”. This is the credential that will be used for URLs without user names. The user name for this credential does not matter, but you can choose something descriptive, e.g. “PersonalAccessToken”, “token”, or “generic”.

  4. Configure git to use this username by default. E.g. if you chose “generic”, then run

    git config –global credential.username generic

  5. Add all the other credentials, with appropriate user names. These are the user names that you need to put in the URLs for the repositories or operations you want to use them for. (GitHub does not actually use the user names if the password is a PAT.)

Windows with older git versions

At most one github.com credential

If you only need to manage a single github.com credential, together with possibly multiple credentials to other hosts (including GitHub Enterprise hosts), then you can use the default manager helper, and get away with the default auto-detected GCM authority setting.

In this case, you can add you github.com credential with an arbitrary user name, and for each other host you can add configure a default user name, and/or include user names in the URLs to these hosts. This is how to set a default user name for a host:

git config --global credential.https://example.com.username myusername

Multiple GitHub credentials

If you need to manage multiple github.com credentials, then you can still use the manager helper, but you need to change the GCM authority by setting an option or an environment variable, see https://github.com/microsoft/Git-Credential-Manager-for-Windows/blob/master/Docs/Configuration.md#authority. Once https://github.com/microsoft/Git-Credential-Manager-for-Windows/pull/891 is merged, you won’t need to do this. (At least in recent git versions, that contain a GCM build with the fix.)

This is how to change the config for this:

git config --global credential.authority Basic

You can also change it only for github.com:

git config --global credential.github.com.authority Basic

Then you can configure a default user name, this will be used for URLs without a user name:

git config --global credential.username generic

Now you can add you credentials, the default one with the “generic” user name, and all the others with their specific user and host names.

gitcreds/inst/doc/package.R0000644000176200001440000000345614306341667015340 0ustar liggesusers## ----------------------------------------------------------------------------- gitcreds::gitcreds_cache_envvar("https://github.com") ## ----------------------------------------------------------------------------- library(testthat) test_that("bad credentials from git", { withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "bad")) # Test code that calls gitcreds_get(), potentially downstream. # gitcreds_get() will return `bad` as the password. # Illustration: expect_equal( gitcreds::gitcreds_get("https://github.com")$password, "bad" ) }) ## ----------------------------------------------------------------------------- library(testthat) test_that("another GitHub user", { cred <- paste0( "protocol:https:", "host:github.com:", "username:user1:", "password:secret" ) withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = cred)) # Your test code comes here. This is just an illustration: print(gitcreds::gitcreds_get()) expect_equal(gitcreds::gitcreds_get()$username, "user1") }) ## ----------------------------------------------------------------------------- library(testthat) test_that("no credentials from git", { withr::local_envvar(c(GITHUB_PAT_GITHUB_COM = "FAIL")) # The test code that calls gitcreds_get() comes here. # It will fail with error "gitcreds_no_credentials" expect_error( gitcreds::gitcreds_get("https://github.com"), class = "gitcreds_no_credentials" ) }) ## ----------------------------------------------------------------------------- library(testthat) test_that("no git installation", { withr::local_envvar(c( GITHUB_PAT_GITHUB_COM = "FAIL:gitcreds_nogit_error" )) # Test code that calls gitcreds_get() comes here. # Illustration: expect_error( gitcreds::gitcreds_get("https://github.com"), class = "gitcreds_nogit_error" ) })