diff --git a/R/available_version.R b/R/available_version.R index 631370e..f533992 100644 --- a/R/available_version.R +++ b/R/available_version.R @@ -1,218 +1,276 @@ -#' Get the available version of a package -#' -#' @param desc_src Output from [package_installation_info()], or a package name -#' (for convenience/testing purposes) -#' -#' @return Either `NULL` or a list with fields `Source_Name`, `Source_Version`, -#' and possibly `Source_URL` -#' -#' @noRd -available_version <- function(desc_src) { - - if (!is_online()) { - return(NULL) - } - - # Handle cases where `x` is just a package name - for convenience/testing only - if (is.character(desc_src) && length(desc_src) == 1L) - desc_src <- package_installation_info(desc_src) - - preferred_srcs <- preferred_sources(desc_src[["Package"]]) - - # Check each of the sources specified in getOption("updateme.sources") - for (src in preferred_srcs) { - vn <- available_version_impl( - list_replace(desc_src, !!!compact(src)), - type = src[["Preferred_Source"]] - ) - if (!is.null(vn)) - return(vn) - } - - # Names: source type - # Values: required fields to be able to search this kind of repo - # ! Order is important - src_type_defaults <- list( - "repo" = c("Package", "Repository"), - "github" = c("Package", "Github_Username", "Github_Repository"), - "remote" = c(), # Not implemented yet - "bioc" = c() # Not implemented yet - ) - - for (src_type in names(src_type_defaults)) { - required_fields <- src_type_defaults[[src_type]] - - # If we've already checked this (because it's specified in one of - # getOption("updateme.sources")), skip so we don't check it again - already_checked <- FALSE - for (preferred_src in preferred_srcs) - already_checked <- list_is_subset(desc_src[required_fields], preferred_src) - if (already_checked) - next - - vn <- available_version_impl(desc_src, src_type) - if (!is.null(vn)) - return(vn) - } - - NULL -} - -available_version_impl <- function(x, type = c("repo", "github", "remote", "bioc")) { - type <- match.arg(type) - pkg <- x[["Package"]] - - if (!type %in% x[["Available_Sources"]]) - return(NULL) - - # Packages from CRAN/CRAN-like sources such as r-universe - if (identical(type, "repo")) - return(available_version_impl_repo(pkg, x[["Repository"]])) - - # Packages from GitHub - if (identical(type, "github")) - return(available_version_impl_github( - x[["Package"]], - x[["Github_Username"]], - x[["Github_Repository"]] - )) - - # TODO: Implement remote and bioc - NULL -} - -available_version_impl_repo <- function(pkg, repo = NULL) { - repos_option <- getOption("repos") - - if (is.null(repo)) { - repo <- repos_option - } else if (repo %in% names(repos_option)) { - repo <- as.list(repos_option)[[repo]] - } - - pkg_data <- available_packages(repos = repo) |> - subset(Package == pkg) |> - subset(Version == max(Version)) # E.g. if there are multiple repos - - if (nrow(pkg_data) == 0L) { - return(NULL) - } else if (nrow(pkg_data) > 1L) { - pkg_data <- pkg_data[1, , drop = FALSE] - } - - # The last bit of the repo URL can contain some extra stuff - remove this so - # we can detect whether it's the same as a repo from the `repos` global option - pattern <- pkg_data[["Repository"]] |> - sub(x = _, "/R/.+$", "") |> - sub(x = _, "/src/contrib$", "") - - repo_alias <- names(repos_option)[grepl(pattern, repos_option, fixed = TRUE)] - if (length(repo_alias) == 0L || identical(repo_alias, "")) - repo_alias <- repo - - list( - Source_Name = repo_alias, - Source_URL = repo, - Source_Version = maybe_as_version(pkg_data[["Version"]]) - ) -} - - -available_version_impl_github <- function(pkg, username, repo) { - response <- desc_from_github(username, repo, pkg) - - if (is.null(response)) - return(response) - - desc <- parse_description(response) - github_name <- desc[["Package"]] %||% pkg - - # Handle cases when the returned DESCRIPTION isn't for the correct package - if (!identical(github_name, pkg)) { - cli::cli_warn(c( - "Incorrect repo specification for package {.pkg {pkg}}", - i = "Found DESCRIPTION file for package {.pkg {github_name}}", - i = "Check the file at {.url {make_github_url(username, repo)}}" - )) - return(NULL) - } - - github_version <- desc[["Version"]] - - list( - Source_Name = "GitHub", - Source_URL = paste0("https://github.com/", username, "/", repo), - Source_Version = github_version - ) - -} - -desc_from_github <- function(username, repo, pkg = repo, use_curl = TRUE) { - file_url <- paste0( - "https://raw.githubusercontent.com/", - username, "/", repo, - "/HEAD/DESCRIPTION" - ) - - handle <- curl::new_handle() - pat <- get_github_pat() - - if (!is.null(pat)) - curl::handle_setheaders(handle, Authorization = paste("token", get_github_pat())) - - con <- curl::curl(file_url, handle = handle) - - tryCatch( - readLines(con, warn = FALSE), - error = function(e) { - msg <- e$message - - private_repo_msg <- if (is.null(get_github_pat())) - "If the repo is private, consider setting a PAT using {.fun gitcreds::gitcreds_set}" - - warning_msg <- if (grepl("404", msg)) { - c( - i = "{.val 404} error: DESCRIPTION not found", - i = private_repo_msg, - i = paste( - "Is the repo private? Perhaps you need to configure", - "an {.topic [access token](updateme::`private-repos`)}." - ) - ) - } else if (grepl("302", msg)) { - c(i = "{.val 302} response: not yet implemented") # TODO - } else if (grepl("403", msg)) { - c(i = "{.val 403} error: access forbidden") - } else { - c(i = "DESCRIPTION file not accessible: {msg}") - } - - cli::cli_warn(c( - "Failed attempting to get a package version for {.pkg {pkg}} from GitHub", - warning_msg, - i = "Error occurred accessing URL {.url {file_url}}" - )) - - NULL - } - ) -} - -get_github_pat <- function() { - # 1. check special updateme env var - updateme_github_pat <- env_var("UPDATEME_GITHUB_PAT") - if (!is.null(updateme_github_pat)) - return(updateme_github_pat) - - # 2. check w/{gitcreds} pkg - if (is_installed("gitcreds")) { - # gitcreds may error if no git installed, no creds set, etc - try(silent = TRUE, { - pat <- gitcreds::gitcreds_get()[["password"]] - return(pat) - }) - } - - # 3. Check standard env vars - env_var("GITHUB_PAT") %||% env_var("GITHUB_TOKEN") -} +#' Get the available version of a package +#' +#' @param desc_src Output from [package_installation_info()], or a package name +#' (for convenience/testing purposes) +#' +#' @return Either `NULL` or a list with fields `Source_Name`, `Source_Version`, +#' and possibly `Source_URL` +#' +#' @noRd +available_version <- function(desc_src) { + + if (!is_online()) { + return(NULL) + } + + # Handle cases where `x` is just a package name - for convenience/testing only + if (is.character(desc_src) && length(desc_src) == 1L) + desc_src <- package_installation_info(desc_src) + + preferred_srcs <- preferred_sources(desc_src[["Package"]]) + + # Check each of the sources specified in getOption("updateme.sources") + for (src in preferred_srcs) { + vn <- available_version_impl( + list_replace(desc_src, !!!compact(src)), + type = src[["Preferred_Source"]] + ) + if (!is.null(vn)) + return(vn) + } + + # Names: source type + # Values: required fields to be able to search this kind of repo + # ! Order is important + src_type_defaults <- list( + "repo" = c("Package", "Repository"), + "github" = c("Package", "Github_Username", "Github_Repository"), + "gitlab" = c("Package", "Gitlab_Username", "Gitlab_Repository"), + "remote" = c(), # Not implemented yet + "bioc" = c() # Not implemented yet + ) + + for (src_type in names(src_type_defaults)) { + required_fields <- src_type_defaults[[src_type]] + + # If we've already checked this (because it's specified in one of + # getOption("updateme.sources")), skip so we don't check it again + already_checked <- FALSE + for (preferred_src in preferred_srcs) + already_checked <- list_is_subset(desc_src[required_fields], preferred_src) + if (already_checked) + next + + vn <- available_version_impl(desc_src, src_type) + if (!is.null(vn)) + return(vn) + } + + NULL +} + +available_version_impl <- function(x, type = c("repo", "github", "gitlab", "remote", "bioc")) { + type <- match.arg(type) + pkg <- x[["Package"]] + + if (!type %in% x[["Available_Sources"]]) + return(NULL) + + # Packages from CRAN/CRAN-like sources such as r-universe + if (identical(type, "repo")) + return(available_version_impl_repo(pkg, x[["Repository"]])) + + # Packages from GitHub + if (identical(type, "github")) + return(available_version_impl_github( + x[["Package"]], + x[["Github_Username"]], + x[["Github_Repository"]] + )) + + # Packages from GitLab + if (identical(type, "gitlab")) + return(available_version_impl_gitlab( + x[["Package"]], + x[["Gitlab_Username"]], + x[["Gitlab_Repository"]] + )) + + # TODO: Implement remote and bioc + NULL +} + +available_version_impl_repo <- function(pkg, repo = NULL) { + repos_option <- getOption("repos") + + if (is.null(repo)) { + repo <- repos_option + } else if (repo %in% names(repos_option)) { + repo <- as.list(repos_option)[[repo]] + } + + pkg_data <- available_packages(repos = repo) |> + subset(Package == pkg) |> + subset(Version == max(Version)) # E.g. if there are multiple repos + + if (nrow(pkg_data) == 0L) { + return(NULL) + } else if (nrow(pkg_data) > 1L) { + pkg_data <- pkg_data[1, , drop = FALSE] + } + + # The last bit of the repo URL can contain some extra stuff - remove this so + # we can detect whether it's the same as a repo from the `repos` global option + pattern <- pkg_data[["Repository"]] |> + sub(x = _, "/R/.+$", "") |> + sub(x = _, "/src/contrib$", "") + + repo_alias <- names(repos_option)[grepl(pattern, repos_option, fixed = TRUE)] + if (length(repo_alias) == 0L || identical(repo_alias, "")) + repo_alias <- repo + + list( + Source_Name = repo_alias, + Source_URL = repo, + Source_Version = maybe_as_version(pkg_data[["Version"]]) + ) +} + +available_version_impl_github <- function(pkg, username, repo) { + available_version_impl_git(pkg, username, repo, "github") +} + +available_version_impl_gitlab <- function(pkg, username, repo) { + available_version_impl_git(pkg, username, repo, "gitlab") +} + +available_version_impl_git <- function(pkg, username, repo, type = c("github", "gitlab")) { + response <- desc_from_git(username, repo, pkg, type = type) + + if (is.null(response)) + return(response) + + desc <- parse_description(response) + returned_name <- desc[["Package"]] %||% pkg + + # Handle cases when the returned DESCRIPTION isn't for the correct package + if (!identical(returned_name, pkg)) { + cli::cli_warn(c( + "Incorrect repo specification for package {.pkg {pkg}}", + i = "Found DESCRIPTION file for package {.pkg {returned_name}}", + i = "Check the file at {.url {github_url_make(username, repo)}}" + )) + return(NULL) + } + + returned_version <- desc[["Version"]] + + list( + Source_Name = "GitHub", + Source_URL = switch(type, + github = github_url_make(username, repo), + gitlab = gitlab_url_make(username, repo) + ), + Source_Version = returned_version + ) + +} + +desc_from_git <- function(username, repo, pkg = repo, type = c("github", "gitlab")) { + type <- match.arg(type) + + pat <- get_git_pat(type) + + switch(type, + github = { + git_name <- "GitHub" + file_url <- paste0( + "https://raw.githubusercontent.com/", + username, "/", repo, "/HEAD/DESCRIPTION" + ) + auth_header <- list(Authorization = paste("token", pat)) + }, + gitlab = { + git_name <- "GitLab" + file_url <- paste0( + "https://gitlab.com/api/v4/projects/", + utils::URLencode(paste0(username, "/", repo), reserved = TRUE), + "/repository/files/DESCRIPTION/raw" + ) + auth_header <- list(`PRIVATE-TOKEN` = pat) + } + ) + + handle <- curl::new_handle() + + if (!is.null(pat)) + curl::handle_setheaders(handle, .list = auth_header) + + con <- curl::curl(file_url, handle = handle) + + tryCatch( + readLines(con, warn = FALSE), + error = function(e) { + msg <- e$message + + private_repo_msg <- if (is.null(pat)) + "If the repo is private, consider setting a PAT using {.fun gitcreds::gitcreds_set}" + + warning_msg <- if (grepl("404", msg)) { + c( + i = "{.val 404} error: DESCRIPTION not found", + i = private_repo_msg, + i = paste( + "Is the repo private? Perhaps you need to configure", + "an {.topic [access token](updateme::updateme_set_sources)}." + ) + ) + } else if (grepl("302", msg)) { + c(i = "{.val 302} response: not yet implemented") # TODO: not exactly sure what this means + } else if (grepl("403", msg)) { + c(i = "{.val 403} error: access forbidden") + } else { + c(i = "DESCRIPTION file not accessible: {msg}") + } + + cli::cli_warn(c( + "Failed attempting to get a package version for {.pkg {pkg}} from {git_name}", + warning_msg, + i = "Error occurred accessing URL {.url {file_url}}" + )) + + NULL + } + ) +} + +get_git_pat <- function(type = c("github", "gitlab")) { + type <- match.arg(type) + + switch(type, + github = { + updateme_env_var <- "UPDATEME_GITHUB_PAT" + git_name <- "GitHub" + git_url <- "https://github.com" + fallback_envvars <- c("GITHUB_PAT", "GITHUB_TOKEN") + }, + gitlab = { + updateme_env_var <- "UPDATEME_GITLAB_PAT" + git_name <- "GitLab" + git_url <- "https://gitlab.com" + fallback_envvars <- c("GITLAB_PAT", "GITLAB_TOKEN") + } + ) + + # 1. check special updateme env var + updateme_pat <- env_var(updateme_env_var) + if (!is.null(updateme_pat)) + return(updateme_pat) + + # 2. check using {gitcreds} + if (is_installed("gitcreds")) { + # gitcreds may error if no git installed, no creds set, etc + try(silent = TRUE, { + pat <- gitcreds::gitcreds_get(git_url)[["password"]] + return(pat) + }) + } + + # 3. Check standard env vars + for (envvar in fallback_envvars) { + pat <- env_var(envvar) + if (!is.null(pat)) + return(pat) + } + NULL +} diff --git a/R/package_installation_info.R b/R/package_installation_info.R index ae8fec2..144bbaa 100644 --- a/R/package_installation_info.R +++ b/R/package_installation_info.R @@ -1,70 +1,111 @@ -package_installation_info <- function(pkg, lib.loc = NULL) { - - desc <- package_description( - pkg, - lib.loc = lib.loc, - fields = c( - "Version", "URL", "Repository", "RemoteType", - "RemoteUsername", "RemoteRepo", "GithubUsername", "GithubRepo", - "RemoteUrl", "biocViews" - ) - ) - - version <- desc[["Version"]] - repo <- desc[["Repository"]] - remote_type <- desc[["RemoteType"]] - gh_username <- desc[["GithubUsername"]] %||% desc[["RemoteUsername"]] - gh_repo <- desc[["GithubRepo"]] %||% desc[["RemoteRepo"]] - remote_url <- desc[["RemoteUrl"]] - bioc_views <- desc[["biocViews"]] - pkg_urls <- desc[["URL"]] - - if (is.null(desc[["Version"]])) - return(NULL) - - # If no github info set, try getting it from the URL field - if ((is.null(gh_username) || is.null(gh_repo)) && !is.null(pkg_urls)) { - pkg_urls <- strsplit(pkg_urls, ",\\s*")[[1]] - github_url <- pkg_urls[is_valid_github_url(pkg_urls)] - if (length(github_url) > 0) { - github_url <- github_url[1] - gh_username <- github_username_from_url(github_url) - gh_repo <- github_repo_from_url(github_url) - } - } - - available_sources <- c( - if (!is.null(repo)) "repo", - if (!is.null(gh_repo) && !is.null(gh_username)) "github", - if (!is.null(remote_url)) "remote", - if (!is.null(bioc_views)) "bioc" - ) - - list( - Available_Sources = available_sources, - Package = pkg, - Version_Installed = version, - Repository = repo, - Github_Username = gh_username, - Github_Repository = gh_repo, - Remote_URL = remote_url, - Bioc_Views = bioc_views - ) - -} - - - -package_description <- function(pkg, lib.loc = NULL, fields = NULL) { - tryCatch( - pkg |> - packageDescription(lib.loc = lib.loc, fields = fields, drop = FALSE) |> - keep(\(x) !is.na(x) && x != ""), - warning = function(w) { - cli::cli_abort(c( - "No DESCRIPTION file found for {.pkg {pkg}}", - i = "Original warning: {w$message}" - )) - } - ) -} +package_installation_info <- function(pkg, lib.loc = NULL) { + + desc <- package_description( + pkg, + lib.loc = lib.loc, + fields = c( + "Version", "URL", "Repository", "RemoteType", + "RemoteUsername", "RemoteRepo", "GithubUsername", "GithubRepo", + "RemoteUrl", "biocViews" + ) + ) + + version <- desc[["Version"]] + + if (is.null(desc[["Version"]])) + return(NULL) + + repo <- desc[["Repository"]] + remote_type <- desc[["RemoteType"]] + is_github <- identical(remote_type, "github") + is_gitlab <- identical(remote_type, "gitlab") + rmt_username <- desc[["RemoteUsername"]] + rmt_repo <- desc[["RemoteRepo"]] + gl_username <- desc[["GitlabUsername"]] %||% (if (is_gitlab) rmt_username) + gl_repo <- desc[["GitlabRepo"]] %||% (if (is_gitlab) rmt_repo) + gh_username <- desc[["GithubUsername"]] %||% (if (is_github) rmt_username) + gh_repo <- desc[["GithubRepo"]] %||% (if (is_github) rmt_repo) + remote_url <- desc[["RemoteUrl"]] + bioc_views <- desc[["biocViews"]] + pkg_urls <- desc[["URL"]] + + url_repo_details <- repo_details_from_urls(pkg_urls) + + # If no github info set, try getting it from the URL field + if ((is.null(gh_username) || is.null(gh_repo)) && !is.null(pkg_urls)) { + gh_username <- url_repo_details[["Github_Username"]] + gh_repo <- url_repo_details[["Github_Repository"]] + } + + # If no gitlab info set, try getting it from the URL field + if ((is.null(gl_username) || is.null(gl_repo)) && !is.null(pkg_urls)) { + gl_username <- url_repo_details[["Gitlab_Username"]] + gl_repo <- url_repo_details[["Gitlab_Repository"]] + } + + available_sources <- c( + if (!is.null(repo)) "repo", + if (!is.null(gh_repo) && !is.null(gh_username)) "github", + if (!is.null(gl_repo) && !is.null(gl_username)) "gitlab", + if (!is.null(remote_url)) "remote", + if (!is.null(bioc_views)) "bioc" + ) + + list( + Available_Sources = available_sources, + Package = pkg, + Version_Installed = version, + Repository = repo, + Github_Username = gh_username, + Github_Repository = gh_repo, + Gitlab_Username = gl_username, + Gitlab_Repository = gl_repo, + Remote_URL = remote_url, + Bioc_Views = bioc_views + ) + +} + +repo_details_from_urls <- function(x) { + if (is.null(x)) + return(NULL) + + pkg_urls <- strsplit(x, ",\\s*")[[1]] + + out <- list( + Github_Username = NULL, + Github_Repository = NULL, + Gitlab_Username = NULL, + Gitlab_Repository = NULL + ) + + github_urls <- pkg_urls[is_github_url(pkg_urls)] + if (length(github_urls) > 0L) { + github_url <- github_urls[1] + out[["Github_Username"]] <- github_username_from_url(github_url) + out[["Github_Repository"]] <- github_repo_from_url(github_url) + } + + gitlab_urls <- pkg_urls[is_gitlab_url(pkg_urls)] + if (length(gitlab_urls) > 0L) { + gitlab_url <- gitlab_urls[1] + out[["Gitlab_Username"]] <- gitlab_username_from_url(gitlab_url) + out[["Gitlab_Repository"]] <- gitlab_repo_from_url(gitlab_url) + } + + out +} + +package_description <- function(pkg, lib.loc = NULL, fields = NULL) { + tryCatch( + pkg |> + packageDescription(lib.loc = lib.loc, fields = fields, drop = FALSE) |> + keep(\(x) !is.na(x) && x != ""), + warning = function(w) { + cli::cli_abort(c( + "No DESCRIPTION file found for {.pkg {pkg}}", + i = "Original warning: {w$message}" + )) + } + ) +} diff --git a/R/updateme_set_sources.R b/R/updateme_set_sources.R index ed6208d..91d8ad5 100644 --- a/R/updateme_set_sources.R +++ b/R/updateme_set_sources.R @@ -1,188 +1,178 @@ -#' Configure {updateme} lookup of new package versions -#' -#' This function is a helper for setting the `"updateme.sources"` -#' global option. It provides a user-friendly interface and validation of the -#' options you set. -#' -#' @param ... Named or unnamed arguments. Values should be either: -#' -#' - `"github"`: new versions will be found from the package development -#' version on GitHub, if a repo can be identified using the package -#' `DESCRIPTION` -#' -#' - One of `names(getOption("repo"))`: latest versions will be taken from -#' this source, if available -#' -#' - A URL pointing to a GitHub repo, e.g. -#' `"https://github.com/wurli/updateme"`: the latest version *for this -#' particular package* will be taken from this project -#' -#' If arguments are named, names should indicate package which the option -#' should apply to. If unnamed, the option will apply to all packages. See -#' examples for more information. -#' -#' @return The result of setting -#' `options(updateme.sources = )` -#' @export -#' -#' @examples -#' if (FALSE) { -#' -#' # If you want to check non-standard repos for new versions of packages, -#' # you'll first have to set the repos global option. Note that each -#' # option must be named for compatibility with {updateme} -#' options(repos = c( -#' -#' # Your default repos, e.g. c(CRAN = "https://cloud.r-project.org") -#' getOption("repos"), -#' -#' # The tidyverse r-universe, including dev versions of tidyverse packages -#' tidyverse = "https://tidyverse.r-universe.dev", -#' -#' # The r-lib r-universe, including dev versions of infrastructure packages -#' # like {cli}, {rlang}, etc -#' rlib = "https://r-lib.r-universe.dev" -#' )) -#' -#' # 1. New versions will first be looked up in the tidyverse r-universe -#' # 2. If not found, they will be looked up from your usual CRAN mirror -#' # 3. {bslib} will always be first looked up from GitHub -#' # 4. {cli} will always be first looked up from the r-lib r-universe -#' updateme_sources_set( -#' "tidyverse", -#' "CRAN", -#' bslib = "https://github.com/rstudio/bslib", # Name is optional here -#' cli = "rlib" -#' ) -#' } -#' -# TODO: Add .append arg? -updateme_sources_set <- function(...) { - options(updateme.sources = updateme_sources_set_impl(...)) -} - -updateme_sources_set_impl <- function(...) { - # dots_list() prevents names being NULL - dots <- dots_list(...) - out <- imap(dots, updateme_sources_validate) - sources <- map(out, function(x) x[["Source_Name"]]) - packages <- map_chr(out, function(x) x[["Package"]] %||% "") - set_names(sources, packages) -} - -preferred_sources <- function(pkg) { - opt <- updateme_sources_get() - - if (length(opt) == 0L) - return(NULL) - - c( - opt |> keep(function(x) identical(x[["Package"]], pkg)), - opt |> keep(function(x) is.null(x[["Package"]])) - ) -} - -updateme_sources_validate <- function(src, pkg = NULL, throw = cli::cli_abort) { - - handle_no_sources <- function() { - if (!is.null(throw)) { - repos <- names(getOption("repos")) - throw(call = caller_call(6), c( - "Invalid package source {.val {src}}.", - "i" = "Package sources must be either:", - " " = "1. {.val github}, to check the version on GitHub if possible", - " " = '2. One of {.code names(getOption("repos"))}', - " " = "3. The URL of a specific GitHub repository, e.g. {.url https://github.com/wurli/updateme}" - )) - } - NULL - } - - src_ok <- is.character(src) || is.null(src) - pkg_ok <- is.character(pkg) || is.null(pkg) - - if (!src_ok || !pkg_ok) { - return(handle_no_sources()) - } - - if (pkg == "") - pkg <- NULL - - out <- list( - Preferred_Source = NULL, - Package = pkg, - Source_Name = src, - Repository = NULL, - Github_Username = NULL, - Github_Repository = NULL, - Remote_URL = NULL, - Bioc_Views = NULL - ) - - if (is_valid_repo(src)) { - out[["Preferred_Source"]] <- "repo" - out[["Repository"]] <- src - return(out) - } - - if (identical(src, "github")) { - out[["Preferred_Source"]] <- "github" - return(out) - } - - if (is_valid_github_url(src)) { - out[["Preferred_Source"]] <- "github" - out[["Github_Username"]] <- github_username_from_url(src) - out[["Github_Repository"]] <- github_repo_from_url(src) - out[["Package"]] <- out[["Package"]] %||% out[["Github_Repository"]] - return(out) - } - - handle_no_sources() - -} - -updateme_sources_get <- function(check = FALSE) { - opt <- as.list(getOption("updateme.sources")) - - if (length(opt) == 0L) - return(NULL) - - names(opt) <- names(opt) %||% rep("", length(opt)) - - opt |> - imap(updateme_sources_validate, throw = if (check) cli::cli_abort) |> - compact() |> - unname() -} - -is_valid_repo <- function(x) { - x %in% names(getOption("repos")) -} - -is_valid_github_url <- function(x) { - grepl("^\\s*https://github\\.com/[a-zA-Z0-9-]+/[a-zA-Z0-9_-]+\\s*$", x) -} - -check_github_url <- function(x) { - if (!is_valid_github_url(x)) - cli::cli_abort(c( - "Incorrectly formed GitHub URL {.url {x}}", - i = "URLs should have format {.val https://github.com/username/repo}" - )) - invisible(NULL) -} - -github_username_from_url <- function(x) { - check_github_url(x) - sub("\\s*https://github\\.com/([a-zA-Z0-9-]+)/[a-zA-Z0-9_-]+\\s*$", "\\1", x) -} - -github_repo_from_url <- function(x) { - check_github_url(x) - sub("\\s*https://github\\.com/[a-zA-Z0-9-]+/([a-zA-Z0-9_-]+)\\s*$", "\\1", x) -} - -make_github_url <- function(username, repo) { - paste0("https://github.com/", username, "/", repo) -} +#' Configure {updateme} lookup of new package versions +#' +#' This function is a helper for setting the `"updateme.sources"` +#' global option. It provides a user-friendly interface and validation of the +#' options you set. +#' +#' @param ... Named or unnamed arguments. Values should be either: +#' +#' - `"github"`: new versions will be found from the package development +#' version on GitHub, if a repo can be identified using the package +#' `DESCRIPTION` +#' +#' - One of `names(getOption("repo"))`: latest versions will be taken from +#' this source, if available +#' +#' - A URL pointing to a GitHub repo, e.g. +#' `"https://github.com/wurli/updateme"`: the latest version *for this +#' particular package* will be taken from this project +#' +#' If arguments are named, names should indicate package which the option +#' should apply to. If unnamed, the option will apply to all packages. See +#' examples for more information. +#' +#' @section Private Repositories: +#' {updateme} supports packages stored in private repositories on GitHub and +#' GitLab. To get upstream package version from either, you should only +#' have to configure a personal access token (PAT). +#' +#' * For GitHub packages, {updateme} checks, in order: +#' * The `UPDATEME_GITHUB_PAT` environmental variable +#' * Any personal access tokens configured using [gitcreds::gitcreds_set()] +#' * The `GITHUB_PAT` environmental variable +#' * The `GITHUB_TOKEN` environmental variable +#' * For GitLab packages, {updateme} checks, in order: +#' * The `UPDATEME_GITLAB_PAT` environmental variable +#' * Any personal access tokens configured using [gitcreds::gitcreds_set()] +#' * The `GITLAB_PAT` environmental variable +#' * The `GITLAB_TOKEN` environmental variable +#' +#' +#' @return The result of setting +#' `options(updateme.sources = )` +#' @export +#' +#' @examples +#' if (FALSE) { +#' +#' # If you want to check non-standard repos for new versions of packages, +#' # you'll first have to set the repos global option. Note that each +#' # option must be named for compatibility with {updateme} +#' options(repos = c( +#' +#' # Your default repos, e.g. c(CRAN = "https://cloud.r-project.org") +#' getOption("repos"), +#' +#' # The tidyverse r-universe, including dev versions of tidyverse packages +#' tidyverse = "https://tidyverse.r-universe.dev", +#' +#' # The r-lib r-universe, including dev versions of infrastructure packages +#' # like {cli}, {rlang}, etc +#' rlib = "https://r-lib.r-universe.dev" +#' )) +#' +#' # 1. New versions will first be looked up in the tidyverse r-universe +#' # 2. If not found, they will be looked up from your usual CRAN mirror +#' # 3. {bslib} will always be first looked up from GitHub +#' # 4. {cli} will always be first looked up from the r-lib r-universe +#' updateme_sources_set( +#' "tidyverse", +#' "CRAN", +#' bslib = "https://github.com/rstudio/bslib", # Name is optional here +#' cli = "rlib" +#' ) +#' } +#' +# TODO: Add .append arg? +updateme_sources_set <- function(...) { + options(updateme.sources = updateme_sources_set_impl(...)) +} + +updateme_sources_set_impl <- function(...) { + # dots_list() prevents names being NULL + dots <- dots_list(...) + out <- imap(dots, updateme_sources_validate) + sources <- map(out, function(x) x[["Source_Name"]]) + packages <- map_chr(out, function(x) x[["Package"]] %||% "") + set_names(sources, packages) +} + +preferred_sources <- function(pkg) { + opt <- updateme_sources_get() + + if (length(opt) == 0L) + return(NULL) + + c( + opt |> keep(function(x) identical(x[["Package"]], pkg)), + opt |> keep(function(x) is.null(x[["Package"]])) + ) +} + +updateme_sources_validate <- function(src, pkg = NULL, throw = cli::cli_abort) { + + handle_no_sources <- function() { + if (!is.null(throw)) { + repos <- names(getOption("repos")) + throw(call = caller_call(6), c( + "Invalid package source {.val {src}}.", + "i" = "Package sources must be either:", + " " = "1. {.val github}, to check the version on GitHub if possible", + " " = '2. One of {.code names(getOption("repos"))}', + " " = "3. The URL of a specific GitHub repository, e.g. {.url https://github.com/wurli/updateme}" + )) + } + NULL + } + + src_ok <- is.character(src) || is.null(src) + pkg_ok <- is.character(pkg) || is.null(pkg) + + if (!src_ok || !pkg_ok) { + return(handle_no_sources()) + } + + if (pkg == "") + pkg <- NULL + + out <- list( + Preferred_Source = NULL, + Package = pkg, + Source_Name = src, + Repository = NULL, + Github_Username = NULL, + Github_Repository = NULL, + Remote_URL = NULL, + Bioc_Views = NULL + ) + + if (is_valid_repo(src)) { + out[["Preferred_Source"]] <- "repo" + out[["Repository"]] <- src + return(out) + } + + if (identical(src, "github")) { + out[["Preferred_Source"]] <- "github" + return(out) + } + + if (is_github_url(src)) { + out[["Preferred_Source"]] <- "github" + out[["Github_Username"]] <- github_username_from_url(src) + out[["Github_Repository"]] <- github_repo_from_url(src) + out[["Package"]] <- out[["Package"]] %||% out[["Github_Repository"]] + return(out) + } + + handle_no_sources() + +} + +updateme_sources_get <- function(check = FALSE) { + opt <- as.list(getOption("updateme.sources")) + + if (length(opt) == 0L) + return(NULL) + + names(opt) <- names(opt) %||% rep("", length(opt)) + + opt |> + imap(updateme_sources_validate, throw = if (check) cli::cli_abort) |> + compact() |> + unname() +} + +is_valid_repo <- function(x) { + x %in% names(getOption("repos")) +} diff --git a/R/url-stuff.R b/R/url-stuff.R new file mode 100644 index 0000000..f406968 --- /dev/null +++ b/R/url-stuff.R @@ -0,0 +1,113 @@ +# GitHub ----------------------------------------------------------------------- +# paste( +# "https://docs.github.com/en/enterprise-cloud@latest/admin/", +# "identity-and-access-management/iam-configuration-reference/", +# "username-considerations-for-external-authentication#about-username-normalization +# ) +# https://stackoverflow.com/a/59082561/5633063 +# Usernames only alphanumeric and dashes +# Repo names only alphanumeric, dashes, dots, and underscores +.github_username_pattern <- "[a-zA-Z0-9-]+" +.github_repo_pattern <- "[a-zA-Z0-9_.-]+" + +is_github_url <- function(x) { + grepl(x = x, paste0( + "^\\s*https://github\\.com/", + .github_username_pattern, "/", .github_repo_pattern, + "\\s*$" + )) +} + +github_url_check <- function(x) { + check_repo_url(x, is_github_url, "GitHub", "https://github.com/username/repo") +} + +github_username_from_url <- function(x) { + github_url_check(x) + pat <- paste0( + "\\s*https://github\\.com/", + "(", .github_username_pattern, ")", # Capture group + "/", .github_repo_pattern, + "\\s*$" + ) + sub(pat, "\\1", x) +} + +github_repo_from_url <- function(x) { + github_url_check(x) + pat <- paste0( + "\\s*https://github\\.com/", + .github_username_pattern, "/", + "(", .github_repo_pattern, ")", # Capture group + "\\s*$" + ) + sub(pat, "\\1", x) +} + +github_url_make <- function(username, repo) { + paste0("https://github.com/", username, "/", repo) +} + +# Gitlab ----------------------------------------------------------------------- +# https://docs.gitlab.com/ee/user/reserved_names.html +# " +# Project names can contain only letters, digits, emoji, +# underscores, dots, pluses, dashes, or spaces. +# Group names can contain only letters, digits, emoji, +# underscores, dots, parentheses (), dashes, or spaces. +# " +# So, basically anything. I don't want to write a regex to capture every +# emoji ever, so just allow anything besides ], [, /, \, {, } +.gitlab_username_pattern <- r"([^][\/{}()]+)" +.gitlab_repo_pattern <- r"([^][\/{}]+)" + +# print(is_gitlab_url("https://gitlab.com/some (crazy.org name\U270C/ some crazy Pr0j.name \U270C")) +is_gitlab_url <- function(x) { + grepl(x = x, paste0( + "^\\s*https://gitlab\\.com/", + .gitlab_username_pattern, "/", .gitlab_repo_pattern, + "\\s*$" + )) +} + + +gitlab_url_check <- function(x) { + check_repo_url(x, is_gitlab_url, "GitLab", "https://gitlab.com/username/repo") +} + +gitlab_username_from_url <- function(x) { + gitlab_url_check(x) + pat <- paste0( + "\\s*https://gitlab\\.com/", + "(", .gitlab_username_pattern, ")", # Capture group + "/", .gitlab_repo_pattern, + "\\s*$" + ) + sub(pat, "\\1", x) +} + +gitlab_repo_from_url <- function(x) { + gitlab_url_check(x) + pat <- paste0( + "\\s*https://gitlab\\.com/", + .gitlab_username_pattern, "/", + "(", .gitlab_repo_pattern, ")", # Capture group + "\\s*$" + ) + sub(pat, "\\1", x) +} + + +gitlab_url_make <- function(username, repo) { + paste0("https://gitlab.com/", username, "/", repo) +} + +# Utils ------------------------------------------------------------------------ +check_repo_url <- function(x, checker, name, correct) { + if (!checker(x)) + cli::cli_abort(c( + "Incorrectly formed {name} URL {.url {x}}", + i = "URLs should have format {.val {correct}}" + )) + invisible(NULL) +} diff --git a/R/zzz.R b/R/zzz.R index f8795b3..aec8c67 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -9,8 +9,8 @@ available_packages, cache = updateme_cache ) - available_version_impl_github <<- memoise::memoise( - available_version_impl_github, cache = updateme_cache + available_version_impl_git <<- memoise::memoise( + available_version_impl_git, cache = updateme_cache ) env <- get("attach")(env(), name = ".updateme") diff --git a/README.md b/README.md index 29f04a6..3993f58 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ it gets loaded: ## Installation -Installation can be done from GitHub using {pak}: +{updateme} is not yet on CRAN, but you can install it from GitHub using {pak}: ``` r # install.packages("pak") pak::pak("wurli/updateme") @@ -21,8 +21,26 @@ pak::pak("wurli/updateme") ## Usage To use {updateme}, simply call `library(updateme)` before loading other -packages. To use {updateme} everywhere, you can do this in your -`.Rprofile` startup script. +packages. You may find you'd like to have {updateme} available all the time; +in this case, consider adding this snippet to your `.Rprofile`: + +``` r +# If {updateme} isn't installed... +if (!requireNamespace("updateme", quietly = TRUE)) { + + # If {pak} isn't installed... + if (!requireNamepsace("pak", quietly = TRUE)) { + + # Install {pak} from CRAN + install.packages("pak") + } + + # Use {pak} to install {updateme} from GitHub + pak::pak("wurli/updateme") +} + +library(updateme) +``` ## Configuration @@ -31,15 +49,17 @@ packages. To use {updateme} everywhere, you can do this in your By default, new versions of packages will be looked up from the location where they seem to have been installed from. If you installed from CRAN (e.g. using `install.packages()`), {updateme} will check CRAN for a newer -version, and similarly with packages installed from GitHub. +version, and similarly with packages installed from GitHub or GitLab. -You can configure where the latest versions of packages are looked up from -using `updateme_sources_set()`: +If this doesn't work for you, you can override this behaviour using +`updateme_sources_set()`: ``` r +# ~~ Set the `repos` global option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # If you want to check non-standard repos for new versions of packages, # you'll first have to set the repos global option. Note that each -# option must be named for compatibility with {updateme} +# option must be named for compatibility with {updateme}. +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ options(repos = c( # Your default repos, e.g. c(CRAN = "https://cloud.r-project.org") @@ -53,10 +73,12 @@ options(repos = c( rlib = "https://r-lib.r-universe.dev" )) +# ~~ Set the `updateme.sources` global option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # 1. New versions will first be looked up in the tidyverse r-universe # 2. If not found, they will be looked up from your usual CRAN mirror # 3. {bslib} will always be first looked up from GitHub # 4. {cli} will always be first looked up from the r-lib r-universe +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ updateme_sources_set( "tidyverse", "CRAN", @@ -65,8 +87,8 @@ updateme_sources_set( ) ``` -Packages installed from Bioconductor, or from other remotes such as GitLab, -Bitbucket, etc, are not yet supported. +Packages installed from Bioconductor, or from other remotes such as Bitbucket +are not yet supported. ### Caching diff --git a/man/updateme-package.Rd b/man/updateme-package.Rd index e052749..b64397e 100644 --- a/man/updateme-package.Rd +++ b/man/updateme-package.Rd @@ -6,6 +6,19 @@ \alias{updateme-package} \title{updateme: Informative Messages About Outdated Packages} \description{ -More about what it does (maybe more than one line) Use four spaces when indenting paragraphs within the Description. +When a package is loaded, the source repository is checked for new versions. A message is show in the console indicating whether the package is out of date. +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://github.com/wurli/updateme} + \item \url{https://wurli.github.io/updateme/} + \item Report bugs at \url{https://github.com/wurli/updateme/issues} +} + +} +\author{ +\strong{Maintainer}: Jacob Scott \email{jscott2718@gmail.com} + } \keyword{internal} diff --git a/man/updateme_sources_set.Rd b/man/updateme_sources_set.Rd index 7127e77..573212e 100644 --- a/man/updateme_sources_set.Rd +++ b/man/updateme_sources_set.Rd @@ -32,6 +32,29 @@ This function is a helper for setting the \code{"updateme.sources"} global option. It provides a user-friendly interface and validation of the options you set. } +\section{Private Repositories}{ + +{updateme} supports packages stored in private repositories on GitHub and +GitLab. To get upstream package version from either, you should only +have to configure a personal access token (PAT). +\itemize{ +\item For GitHub packages, {updateme} checks, in order: +\itemize{ +\item The \code{UPDATEME_GITHUB_PAT} environmental variable +\item Any personal access tokens configured using \code{\link[gitcreds:gitcreds_get]{gitcreds::gitcreds_set()}} +\item The \code{GITHUB_PAT} environmental variable +\item The \code{GITHUB_TOKEN} environmental variable +} +\item For GitLab packages, {updateme} checks, in order: +\itemize{ +\item The \code{UPDATEME_GITLAB_PAT} environmental variable +\item Any personal access tokens configured using \code{\link[gitcreds:gitcreds_get]{gitcreds::gitcreds_set()}} +\item The \code{GITLAB_PAT} environmental variable +\item The \code{GITLAB_TOKEN} environmental variable +} +} +} + \examples{ if (FALSE) { diff --git a/tests/testthat/test-available_version.R b/tests/testthat/test-available_version.R deleted file mode 100644 index 5dd0e68..0000000 --- a/tests/testthat/test-available_version.R +++ /dev/null @@ -1,44 +0,0 @@ -test_that("Description files can be read from GitHub public repos", { - - skip_if_offline() - - desc <- desc_from_github("wurli", "updateme.testpkg") - - expect_identical(desc, c( - "Package: updateme.testpkg", - "Title: For 'updateme' Testing", - "Version: 0.2.0", - "Authors@R: person(\"Jacob\", \"Scott\", email = \"jscott2718@gmail.com\", role = c(\"aut\", \"cre\"))", - "Description: This package is used in the {updateme} test suite.", - "License: MIT + file LICENSE", "Encoding: UTF-8", "Roxygen: list(markdown = TRUE)", - "RoxygenNote: 7.2.3" - )) - -}) - -test_that("Description files can be read from GitHub private repos", { - - skip_if_offline() - - # NOTE: This PAT is 'fine-grained' and gives read-only access for a single - # repo containing an empty package {updateme.testpkg.private}. - withr::local_envvar(list( - UPDATEME_GITHUB_PAT = paste0( - "github_pat_11AEFKREY0bCtmUEn37oc7_MhIhcuvThFPzQ9Nmd74Bf", - "kydKkvUgKx2NvYw9DEYtw7WUJ7VPW6UnvQVppv" - ) - )) - - desc <- desc_from_github("wurli", "updateme.testpkg.private") - - expect_identical(desc, c( - "Package: updateme.testpkg.private", - "Title: For 'updateme' Testing", - "Version: 0.2.0", - "Authors@R: person(\"Jacob\", \"Scott\", email = \"jscott2718@gmail.com\", role = c(\"aut\", \"cre\"))", - "Description: This package is used in the {updateme} test suite.", - "License: MIT + file LICENSE", "Encoding: UTF-8", "Roxygen: list(markdown = TRUE)", - "RoxygenNote: 7.2.3" - )) - -}) diff --git a/tests/testthat/test-available_version_git.R b/tests/testthat/test-available_version_git.R new file mode 100644 index 0000000..54b229d --- /dev/null +++ b/tests/testthat/test-available_version_git.R @@ -0,0 +1,86 @@ +test_that("Description files can be read from GitHub public repos", { + + skip_if_offline() + + desc <- desc_from_git("wurli", "updateme.testpkg") + + expect_identical(desc, c( + "Package: updateme.testpkg", + "Title: For 'updateme' Testing", + "Version: 0.2.0", + "Authors@R: person(\"Jacob\", \"Scott\", email = \"jscott2718@gmail.com\", role = c(\"aut\", \"cre\"))", + "Description: This package is used in the {updateme} test suite.", + "License: MIT + file LICENSE", "Encoding: UTF-8", "Roxygen: list(markdown = TRUE)", + "RoxygenNote: 7.2.3" + )) + +}) + +test_that("Description files can be read from GitHub private repos", { + + skip_if_offline() + + # NOTE: This PAT is 'fine-grained' and gives read-only access for a single + # repo containing an empty package {updateme.testpkg.private}. + withr::local_envvar(list( + UPDATEME_GITHUB_PAT = paste0( + "github_pat_11AEFKREY0bCtmUEn37oc7_MhIhcuvThFPzQ9Nmd74Bf", + "kydKkvUgKx2NvYw9DEYtw7WUJ7VPW6UnvQVppv" + ) + )) + + desc <- desc_from_git("wurli", "updateme.testpkg.private", type = "github") + + expect_identical(desc, c( + "Package: updateme.testpkg.private", + "Title: For 'updateme' Testing", + "Version: 0.2.0", + "Authors@R: person(\"Jacob\", \"Scott\", email = \"jscott2718@gmail.com\", role = c(\"aut\", \"cre\"))", + "Description: This package is used in the {updateme} test suite.", + "License: MIT + file LICENSE", "Encoding: UTF-8", "Roxygen: list(markdown = TRUE)", + "RoxygenNote: 7.2.3" + )) + +}) + +test_that("Description files can be read from GitLab public repos", { + + skip_if_offline() + + desc <- desc_from_git("wurl1", "updateme.testpkg", type = "gitlab") + + expect_identical(desc, c( + "Package: updateme.testpkg", + "Title: For 'updateme' Testing", + "Version: 0.2.0", + "Authors@R: person(\"Jacob\", \"Scott\", email = \"jscott2718@gmail.com\", role = c(\"aut\", \"cre\"))", + "Description: This package is used in the {updateme} test suite.", + "License: MIT + file LICENSE", "Encoding: UTF-8", "Roxygen: list(markdown = TRUE)", + "RoxygenNote: 7.2.3" + )) + +}) + +test_that("Description files can be read from GitLab private repos", { + + skip_if_offline() + + # NOTE: This PAT is 'fine-grained' and gives read-only access for a single + # repo containing an empty package {updateme.testpkg.private}. + withr::local_envvar(list( + UPDATEME_GITLAB_PAT = "glpat-idE4upwYGmHjsiH-YYzw" + )) + + desc <- desc_from_git("wurli", "updateme.testpkg.private") + + expect_identical(desc, c( + "Package: updateme.testpkg.private", + "Title: For 'updateme' Testing", + "Version: 0.2.0", + "Authors@R: person(\"Jacob\", \"Scott\", email = \"jscott2718@gmail.com\", role = c(\"aut\", \"cre\"))", + "Description: This package is used in the {updateme} test suite.", + "License: MIT + file LICENSE", "Encoding: UTF-8", "Roxygen: list(markdown = TRUE)", + "RoxygenNote: 7.2.3" + )) + +}) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-list_is_subset.R similarity index 100% rename from tests/testthat/test-utils.R rename to tests/testthat/test-list_is_subset.R diff --git a/tests/testthat/test-package_installation_info.R b/tests/testthat/test-package_installation_info.R index 8d7b568..d30cff8 100644 --- a/tests/testthat/test-package_installation_info.R +++ b/tests/testthat/test-package_installation_info.R @@ -25,6 +25,8 @@ test_that("package_installation_info works with no available sources", { Repository = NULL, Github_Username = NULL, Github_Repository = NULL, + Gitlab_Username = NULL, + Gitlab_Repository = NULL, Remote_URL = NULL, Bioc_Views = NULL )) @@ -59,6 +61,8 @@ test_that("package_installation_info works with repos packages", { Repository = "CRAN", Github_Username = NULL, Github_Repository = NULL, + Gitlab_Username = NULL, + Gitlab_Repository = NULL, Remote_URL = NULL, Bioc_Views = NULL )) @@ -93,6 +97,8 @@ test_that("package_installation_info works with desc URL field", { Repository = NULL, Github_Username = "foofyfooson", Github_Repository = "foo", + Gitlab_Username = NULL, + Gitlab_Repository = NULL, Remote_URL = NULL, Bioc_Views = NULL )) @@ -128,6 +134,83 @@ test_that("package_installation_info works with desc GithubRepo/GithubUsername f Repository = NULL, Github_Username = "foofyfooson", Github_Repository = "foo", + Gitlab_Username = NULL, + Gitlab_Repository = NULL, + Remote_URL = NULL, + Bioc_Views = NULL + )) + +}) + + +test_that("package_installation_info works with desc GitLab fields", { + + withr::local_temp_libpaths() + libpath <- .libPaths()[1] + + pkg_dir <- file.path(libpath, "foo") + dir.create(pkg_dir) + + writeLines( + con = file.path(pkg_dir, "DESCRIPTION"), + paste( + sep = "\n", + 'Package: foo', + 'Type: Package', + 'Version: 0.1.0', + 'RemoteType: gitlab', + 'RemoteUsername: foofyfooson', + 'RemoteRepo: foo' + ) + ) + + info <- package_installation_info("foo") + + expect_identical(info, list( + Available_Sources = "gitlab", + Package = "foo", + Version_Installed = "0.1.0", + Repository = NULL, + Github_Username = NULL, + Github_Repository = NULL, + Gitlab_Username = "foofyfooson", + Gitlab_Repository = "foo", + Remote_URL = NULL, + Bioc_Views = NULL + )) + +}) + +test_that("package_installation_info can pare URL field", { + + withr::local_temp_libpaths() + libpath <- .libPaths()[1] + + pkg_dir <- file.path(libpath, "foo") + dir.create(pkg_dir) + + writeLines( + con = file.path(pkg_dir, "DESCRIPTION"), + paste( + sep = "\n", + 'Package: foo', + 'Type: Package', + 'Version: 0.1.0', + 'URL: https://github.com/foofyfooson/foo, https://gitlab.com/foofyfooson/foo' + ) + ) + + info <- package_installation_info("foo") + + expect_identical(info, list( + Available_Sources = c("github", "gitlab"), + Package = "foo", + Version_Installed = "0.1.0", + Repository = NULL, + Github_Username = "foofyfooson", + Github_Repository = "foo", + Gitlab_Username = "foofyfooson", + Gitlab_Repository = "foo", Remote_URL = NULL, Bioc_Views = NULL )) @@ -149,6 +232,7 @@ test_that("package_installation_info works with desc RemoteRepo/RemoteUsername f 'Package: foo', 'Type: Package', 'Version: 0.1.0', + 'RemoteType: github', 'RemoteUsername: foofyfooson', 'RemoteRepo: foo' ) @@ -163,6 +247,8 @@ test_that("package_installation_info works with desc RemoteRepo/RemoteUsername f Repository = NULL, Github_Username = "foofyfooson", Github_Repository = "foo", + Gitlab_Username = NULL, + Gitlab_Repository = NULL, Remote_URL = NULL, Bioc_Views = NULL )) @@ -197,6 +283,8 @@ test_that("package_installation_info works with desc biocViews fields", { Repository = NULL, Github_Username = NULL, Github_Repository = NULL, + Gitlab_Username = NULL, + Gitlab_Repository = NULL, Remote_URL = NULL, Bioc_Views = "Infrastructure" )) @@ -233,6 +321,8 @@ test_that("package_installation_info works with multiple sources", { Repository = "CRAN", Github_Username = "foofyfooson", Github_Repository = "foo", + Gitlab_Username = NULL, + Gitlab_Repository = NULL, Remote_URL = NULL, Bioc_Views = "Infrastructure" ))