From 5fd2978328a8810e1be84ff7ba2426d131bdc317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABlle=20Salmon?= Date: Mon, 23 Mar 2026 10:35:42 +0100 Subject: [PATCH] feat: add `git_config_get()` and `git_config_global_get()` Co-Authored-By: Claude Opus 4.6 --- NAMESPACE | 3 ++ NEWS.md | 1 + R/config.R | 85 ++++++++++++++++++++++++----------- man/git_config.Rd | 40 ++++++++++++----- man/user_is_configured.Rd | 2 + tests/testthat/test-config.R | 34 ++++++++++++-- tests/testthat/test-remotes.R | 36 +++++++++++---- 7 files changed, 152 insertions(+), 49 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 85c40784..f1c504a0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -28,8 +28,11 @@ export(git_commit_id) export(git_commit_info) export(git_commit_stats) export(git_config) +export(git_config_get) export(git_config_global) +export(git_config_global_get) export(git_config_global_set) +export(git_config_local_get) export(git_config_set) export(git_conflicts) export(git_diff) diff --git a/NEWS.md b/NEWS.md index 55e606b3..9934adca 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # gert (development version) +- Add `git_config_get()`, `git_config_local_get()`, and `git_config_global_get()` to retrieve a single named config option, returning `NULL` if unset (#267). - `git_remote_set_pushurl()` gains an `add` argument to append push URLs instead of replacing them. (@robitalec, #128) - Fix `git_info()` for the case when no upstream is configured (@mpage, #263) - `git_branch_create()`: `force` now also applies to the checkout step, allowing branch creation even when local changes would be overwritten (@MichaelChirico, #177). diff --git a/R/config.R b/R/config.R index 1ffa949a..fbdb2ab6 100644 --- a/R/config.R +++ b/R/config.R @@ -4,14 +4,13 @@ #' Get or set Git options, as `git config` does on the command line. **Global** #' settings affect all of a user's Git operations (`git config --global`), #' whereas **local** settings are scoped to a specific repository (`git config -#' --local`). When both exist, local options always win. Four functions address -#' the four possible combinations of getting vs setting and global vs. local. +#' --local`). When both exist, local options always win. #' #' ```{r echo = FALSE, results = "asis"} #' dat <- data.frame( -#' local = c("`git_config()`", "`git_config_set()`"), -#' global = c("`git_config_global()`", "`git_config_global_set()`"), -#' row.names = c("get", "set") +#' local = c("`git_config()`", "`git_config_get()`", "`git_config_local_get()`", "`git_config_set()`"), +#' global = c("`git_config_global()`", "`git_config_get()`", "`git_config_global_get()`", "`git_config_global_set()`"), +#' row.names = c("get all", "get one (local+global)", "get one (local or global only)", "set") #' ) #' knitr::kable(dat, col.names = paste0("**", colnames(dat), "**")) #' ``` @@ -21,6 +20,11 @@ #' option is determined from global or local config. #' * `git_config_global()`: a `data.frame`, as for `git_config()`, except only #' for global Git options. +#' * `git_config_get()`: the value of the named option considering both local and +#' global config (local wins), or `NULL` if unset. +#' * `git_config_local_get()`: as for `git_config_get()`, but restricted to +#' local (repository-level) config only. +#' * `git_config_global_get()`: as for `git_config_get()`, but for global config only. #' * `git_config_set()`, `git_config_global_set()`: The previous value(s) of #' `name` in local or global config, respectively. If this option was #' previously unset, returns `NULL`. Returns invisibly. @@ -36,15 +40,16 @@ #' #' previous <- git_config_set("aaa.bbb", "ccc", repo = r) #' previous -#' cfg <- git_config(repo = r) -#' subset(cfg, level == "local") -#' cfg$value[cfg$name == "aaa.bbb"] +#' git_config_local_get("aaa.bbb", repo = r) #' #' previous <- git_config_set("aaa.bbb", NULL, repo = r) #' previous -#' cfg <- git_config(repo = r) -#' subset(cfg, level == "local") -#' cfg$value[cfg$name == "aaa.bbb"] +#' git_config_local_get("aaa.bbb", repo = r) +#' +#' # Get a single named option (returns NULL if unset) +#' git_config_get("aaa.bbb", repo = r) +#' git_config_set("aaa.bbb", "ccc", repo = r) +#' git_config_get("aaa.bbb", repo = r) #' #' unlink(r, recursive = TRUE) #' @@ -53,6 +58,10 @@ #' git_config_global_set("user.name", "Your Name") #' git_config_global_set("user.email", "your@email.com") #' git_config_global() +#' +#' # Get a single global option (returns NULL if unset) +#' git_config_global_get("user.name") +#' git_config_global_get("gert.nonexistent") #' } #' @export #' @family git @@ -70,10 +79,41 @@ git_config_global <- function() { .Call(R_git_config_list, NULL) } +#' @export +#' @rdname git_config +#' @param name Name of the option to get or set +git_config_get <- function(name, repo = '.') { + cfg <- git_config(repo = repo) + if (!name %in% cfg$name) { + return(NULL) + } + cfg$value[cfg$name == name] +} + +#' @export +#' @rdname git_config +git_config_local_get <- function(name, repo = '.') { + cfg <- git_config(repo = repo) + cfg <- cfg[cfg$level == "local", ] + if (!name %in% cfg$name) { + return(NULL) + } + cfg$value[cfg$name == name] +} + +#' @export +#' @rdname git_config +git_config_global_get <- function(name) { + cfg <- git_config_global() + if (!name %in% cfg$name) { + return(NULL) + } + cfg$value[cfg$name == name] +} + #' @export #' @rdname git_config #' @useDynLib gert R_git_config_set -#' @param name Name of the option to set #' @param value Value to set. Must be a string, logical, number or `NULL` (to #' unset). #' @param add if `TRUE`, append a new entry for `name` instead of replacing @@ -85,8 +125,7 @@ git_config_set <- function(name, value, add = FALSE, repo = '.') { } repo <- git_open(repo) name <- as.character(name) - orig_cfg <- git_config(repo = repo) - out <- orig_cfg$value[orig_cfg$name == name & orig_cfg$level == "local"] + out <- git_config_local_get(name, repo = repo) .Call(R_git_config_set, repo, name, value, add) if (length(out) > 0) { invisible(out) @@ -98,8 +137,7 @@ git_config_set <- function(name, value, add = FALSE, repo = '.') { #' @export #' @rdname git_config git_config_global_set <- function(name, value, add = FALSE) { - orig_cfg <- git_config_global() - out <- orig_cfg$value[orig_cfg$name == name] + out <- git_config_global_get(name) .Call(R_git_config_set, NULL, name, value, add) if (length(out) > 0) { invisible(out) @@ -141,9 +179,8 @@ configure_global_user <- function() { } global_user_is_configured <- function() { - cfg <- git_config_global() - user_name_exists <- any(cfg$name == "user.name") - user_email_exists <- any(cfg$name == "user.email") + user_name_exists <- !is.null(git_config_global_get("user.name")) + user_email_exists <- !is.null(git_config_global_get("user.email")) user_name_exists && user_email_exists } @@ -161,14 +198,10 @@ global_user_is_configured <- function() { #' `FALSE` otherwise. #' #' @export -#' @examples +#' @examplesIf interactive() #' user_is_configured() user_is_configured <- function(repo = ".") { - cfg <- tryCatch( - git_config(repo), - error = function(e) git_config_global() - ) - user_name_exists <- any(cfg$name == "user.name") - user_email_exists <- any(cfg$name == "user.email") + user_name_exists <- !is.null(git_config_get("user.name", repo = repo)) + user_email_exists <- !is.null(git_config_get("user.email", repo = repo)) user_name_exists && user_email_exists } diff --git a/man/git_config.Rd b/man/git_config.Rd index d0422eef..8361d558 100644 --- a/man/git_config.Rd +++ b/man/git_config.Rd @@ -3,6 +3,9 @@ \name{git_config} \alias{git_config} \alias{git_config_global} +\alias{git_config_get} +\alias{git_config_local_get} +\alias{git_config_global_get} \alias{git_config_set} \alias{git_config_global_set} \title{Get or set Git configuration} @@ -11,6 +14,12 @@ git_config(repo = ".") git_config_global() +git_config_get(name, repo = ".") + +git_config_local_get(name, repo = ".") + +git_config_global_get(name) + git_config_set(name, value, add = FALSE, repo = ".") git_config_global_set(name, value, add = FALSE) @@ -22,7 +31,7 @@ this search, provide the filepath protected with \code{\link[=I]{I()}}. When usi parameter, always explicitly call by name (i.e. \verb{repo = }) because future versions of gert may have additional parameters.} -\item{name}{Name of the option to set} +\item{name}{Name of the option to get or set} \item{value}{Value to set. Must be a string, logical, number or \code{NULL} (to unset).} @@ -38,6 +47,11 @@ of \code{repo}, one row per option. The \code{level} column reveals whether the option is determined from global or local config. \item \code{git_config_global()}: a \code{data.frame}, as for \code{git_config()}, except only for global Git options. +\item \code{git_config_get()}: the value of the named option considering both local and +global config (local wins), or \code{NULL} if unset. +\item \code{git_config_local_get()}: as for \code{git_config_get()}, but restricted to +local (repository-level) config only. +\item \code{git_config_global_get()}: as for \code{git_config_get()}, but for global config only. \item \code{git_config_set()}, \code{git_config_global_set()}: The previous value(s) of \code{name} in local or global config, respectively. If this option was previously unset, returns \code{NULL}. Returns invisibly. @@ -46,10 +60,11 @@ previously unset, returns \code{NULL}. Returns invisibly. \description{ Get or set Git options, as \verb{git config} does on the command line. \strong{Global} settings affect all of a user's Git operations (\verb{git config --global}), -whereas \strong{local} settings are scoped to a specific repository (\verb{git config --local}). When both exist, local options always win. Four functions address -the four possible combinations of getting vs setting and global vs. local.\tabular{lll}{ +whereas \strong{local} settings are scoped to a specific repository (\verb{git config --local}). When both exist, local options always win.\tabular{lll}{ \tab \strong{local} \tab \strong{global} \cr - get \tab \code{git_config()} \tab \code{git_config_global()} \cr + get all \tab \code{git_config()} \tab \code{git_config_global()} \cr + get one (local+global) \tab \code{git_config_get()} \tab \code{git_config_get()} \cr + get one (local or global only) \tab \code{git_config_local_get()} \tab \code{git_config_global_get()} \cr set \tab \code{git_config_set()} \tab \code{git_config_global_set()} \cr } } @@ -65,15 +80,16 @@ git_init(r) previous <- git_config_set("aaa.bbb", "ccc", repo = r) previous -cfg <- git_config(repo = r) -subset(cfg, level == "local") -cfg$value[cfg$name == "aaa.bbb"] +git_config_local_get("aaa.bbb", repo = r) previous <- git_config_set("aaa.bbb", NULL, repo = r) previous -cfg <- git_config(repo = r) -subset(cfg, level == "local") -cfg$value[cfg$name == "aaa.bbb"] +git_config_local_get("aaa.bbb", repo = r) + +# Get a single named option (returns NULL if unset) +git_config_get("aaa.bbb", repo = r) +git_config_set("aaa.bbb", "ccc", repo = r) +git_config_get("aaa.bbb", repo = r) unlink(r, recursive = TRUE) @@ -82,6 +98,10 @@ unlink(r, recursive = TRUE) git_config_global_set("user.name", "Your Name") git_config_global_set("user.email", "your@email.com") git_config_global() + +# Get a single global option (returns NULL if unset) +git_config_global_get("user.name") +git_config_global_get("gert.nonexistent") } } \seealso{ diff --git a/man/user_is_configured.Rd b/man/user_is_configured.Rd index e3ae2477..37c0ff71 100644 --- a/man/user_is_configured.Rd +++ b/man/user_is_configured.Rd @@ -19,5 +19,7 @@ configured, in order to make commits. \code{user_is_configured()} makes no distinction between local or global user config. } \examples{ +\dontshow{if (interactive()) withAutoprint(\{ # examplesIf} user_is_configured() +\dontshow{\}) # examplesIf} } diff --git a/tests/testthat/test-config.R b/tests/testthat/test-config.R index 6ac91a4c..caffcc34 100644 --- a/tests/testthat/test-config.R +++ b/tests/testthat/test-config.R @@ -1,14 +1,40 @@ +test_that("git_config_get returns value or NULL", { + repo <- git_init(tempfile("gert-tests-config")) + on.exit(unlink(repo, recursive = TRUE)) + + expect_null(git_config_get("aaa.bbb", repo = repo)) + git_config_set("aaa.bbb", "ccc", repo = repo) + expect_equal(git_config_get("aaa.bbb", repo = repo), "ccc") +}) + +test_that("git_config_global_get returns value or NULL", { + expect_null(git_config_global_get("gert.nonexistent.option.xyzzy")) + # Note: avoid setting/unsetting real global config in tests +}) + +test_that("git_config_local_get returns local value or NULL", { + repo <- git_init(tempfile("gert-tests-config")) + on.exit(unlink(repo, recursive = TRUE)) + + expect_null(git_config_local_get("aaa.bbb", repo = repo)) + git_config_set("aaa.bbb", "ccc", repo = repo) + expect_equal(git_config_local_get("aaa.bbb", repo = repo), "ccc") + # global-only option should not be visible at local level + expect_null(git_config_local_get( + "gert.nonexistent.option.xyzzy", + repo = repo + )) +}) + test_that("local, custom config roundtrip", { repo <- git_init(tempfile("gert-tests-config")) on.exit(unlink(repo, recursive = TRUE)) orig <- git_config_set("aaa.bbb", "ccc", repo = repo) expect_null(orig) - cfg <- git_config(repo) - expect_equal(cfg$value[cfg$name == "aaa.bbb"], "ccc") + expect_equal(git_config_get("aaa.bbb", repo = repo), "ccc") orig <- git_config_set("aaa.bbb", NULL, repo = repo) expect_equal(orig, "ccc") - cfg <- git_config(repo) - expect_equal(cfg$value[cfg$name == "aaa.bbb"], character()) + expect_null(git_config_get("aaa.bbb", repo = repo)) }) diff --git a/tests/testthat/test-remotes.R b/tests/testthat/test-remotes.R index db935c16..d4e99592 100644 --- a/tests/testthat/test-remotes.R +++ b/tests/testthat/test-remotes.R @@ -5,12 +5,23 @@ test_that("git_remote_set_pushurl with add = TRUE appends push URLs", { git_remote_add("https://example.com/fetch", name = "origin", repo = repo) - git_remote_set_pushurl("https://example.com/push1", remote = "origin", repo = repo) - git_remote_set_pushurl("https://example.com/push2", remote = "origin", add = TRUE, repo = repo) + git_remote_set_pushurl( + "https://example.com/push1", + remote = "origin", + repo = repo + ) + git_remote_set_pushurl( + "https://example.com/push2", + remote = "origin", + add = TRUE, + repo = repo + ) - cfg <- git_config(repo = repo) - pushurls <- cfg$value[cfg$name == "remote.origin.pushurl"] - expect_setequal(pushurls, c("https://example.com/push1", "https://example.com/push2")) + pushurls <- git_config_get("remote.origin.pushurl", repo = repo) + expect_setequal( + pushurls, + c("https://example.com/push1", "https://example.com/push2") + ) }) test_that("git_remote_set_pushurl without add replaces push URL", { @@ -20,11 +31,18 @@ test_that("git_remote_set_pushurl without add replaces push URL", { git_remote_add("https://example.com/fetch", name = "origin", repo = repo) - git_remote_set_pushurl("https://example.com/push1", remote = "origin", repo = repo) - git_remote_set_pushurl("https://example.com/push2", remote = "origin", repo = repo) + git_remote_set_pushurl( + "https://example.com/push1", + remote = "origin", + repo = repo + ) + git_remote_set_pushurl( + "https://example.com/push2", + remote = "origin", + repo = repo + ) - cfg <- git_config(repo = repo) - pushurls <- cfg$value[cfg$name == "remote.origin.pushurl"] + pushurls <- git_config_get("remote.origin.pushurl", repo = repo) expect_equal(pushurls, "https://example.com/push2") })