From 31551bf845e2b8d52e45a7f252c93a9487bc4c36 Mon Sep 17 00:00:00 2001 From: wlandau-lilly Date: Tue, 5 Mar 2024 04:51:47 -0500 Subject: [PATCH 1/8] Sketch record_versions() --- DESCRIPTION | 2 +- NAMESPACE | 2 ++ NEWS.md | 3 +- R/package.R | 2 +- R/record_versions.R | 71 ++++++++++++++++++++++++++++++++++++++++++ man/record_versions.Rd | 31 ++++++++++++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 R/record_versions.R create mode 100644 man/record_versions.Rd diff --git a/DESCRIPTION b/DESCRIPTION index b52ce12..a7efcd4 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: r.releases.utils Title: Utilities for An R Universe of Package Releases Description: Utilities for an R universe of package releases. -Version: 0.0.7.9000 +Version: 0.0.8 License: MIT + file LICENSE URL: https://r-releases.github.io/r.releases.utils/, diff --git a/NAMESPACE b/NAMESPACE index d11de82..4f22f63 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,12 +4,14 @@ export(assert_cran_url) export(assert_package) export(assert_release_exists) export(build_universe) +export(record_versions) export(review_pull_request) export(review_pull_requests) export(try_message) importFrom(gh,gh) importFrom(jsonlite,parse_json) importFrom(jsonlite,read_json) +importFrom(jsonlite,write_json) importFrom(nanonext,ncurl) importFrom(nanonext,parse_url) importFrom(nanonext,status_code) diff --git a/NEWS.md b/NEWS.md index 0ff4ff0..8b9f519 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,8 @@ -# r.releases.utils 0.0.7.9000 (development) +# r.releases.utils 0.0.8 * Use R-releases and not `r-releases` to refer to the project. * Edit bot messages. +* Add `record_versions()`. # r.releases.utils 0.0.7 diff --git a/R/package.R b/R/package.R index f73f627..edd0f84 100644 --- a/R/package.R +++ b/R/package.R @@ -3,7 +3,7 @@ #' @name r.releases.utils-package #' @family help #' @importFrom gh gh -#' @importFrom jsonlite parse_json read_json +#' @importFrom jsonlite parse_json read_json write_json #' @importFrom nanonext ncurl parse_url status_code #' @importFrom pkgsearch cran_package #' @importFrom vctrs vec_rbind diff --git a/R/record_versions.R b/R/record_versions.R new file mode 100644 index 0000000..9cac95f --- /dev/null +++ b/R/record_versions.R @@ -0,0 +1,71 @@ +#' @title Record the manifest of package versions. +#' @export +#' @keywords internal +#' @description Record the manifest of versions of packages +#' and their MD5 hashes. +#' @details As well as their versions and MD5 hashes, this function +#' records the highest version ever recorded and the MD5 of that +#' version. This information helps check if a package complies with +#' best practices for version numbers: the version number should increment +#' on each new release. +#' @return `NULL` (invisibly). Writes a package manifest as a JSON file. +#' @param manifest Character of length 1, file path to the JSON manifest. +#' @param repos Character string of package repositories to track. +record_versions <- function( + manifest = "versions.json", + repos = "https://r-releases.r-universe.dev" +) { + current <- get_versions(repos = repos) + if (!file.exists(manifest)) { + jsonlite::write_json(x = current, path = manifest, pretty = TRUE) + return(invisible()) + } + previous <- read_previous(manifest = manifest) + new <- update_manifest(current = current, previous = previous) + jsonlite::write_json(x = new, path = manifest, pretty = TRUE) +} + +get_versions <- function(repos) { + out <- available.packages(repos = "https://r-releases.r-universe.dev") + out <- as.data.frame(out) + out <- out[, c("Package", "Version", "MD5sum")] + colnames(out) <- c("package", "version_current", "md5_current") + rownames(out) <- NULL + out +} + +read_previous <- function(manifest) { + out <- jsonlite::read_json(path = manifest) + out <- do.call(what = vctrs::vec_rbind, args = out) + for (field in colnames(out)) { + out[[field]] <- as.character(out[[field]]) + } + if (is.null(out$version_highest)) { + out$version_highest <- out$version_current + } + if (is.null(out$md5_highest)) { + out$md5_highest <- out$md5_current + } + out$version_current <- NULL + out$md5_current <- NULL + out +} + +update_manifest <- function(current, previous) { + new <- merge(x = current, y = previous, all = TRUE) + incremented <- apply( + X = new, + MARGIN = 1L, + FUN = function(row) { + print(.subset2(row, "version_current")) + print(.subset2(row, "version_highest")) + utils::compareVersion( + a = .subset2(row, "version_current"), + b = .subset2(row, "version_highest") + ) > 0.5 + } + ) + new$version_highest[incremented] <- new$version_current[incremented] + new$md5_highest[incremented] <- new$md5_current[incremented] + new +} diff --git a/man/record_versions.Rd b/man/record_versions.Rd new file mode 100644 index 0000000..179ac55 --- /dev/null +++ b/man/record_versions.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/record_versions.R +\name{record_versions} +\alias{record_versions} +\title{Record the manifest of package versions.} +\usage{ +record_versions( + manifest = "versions.json", + repos = "https://r-releases.r-universe.dev" +) +} +\arguments{ +\item{manifest}{Character of length 1, file path to the JSON manifest.} + +\item{repos}{Character string of package repositories to track.} +} +\value{ +\code{NULL} (invisibly). Writes a package manifest as a JSON file. +} +\description{ +Record the manifest of versions of packages +and their MD5 hashes. +} +\details{ +As well as their versions and MD5 hashes, this function +records the highest version ever recorded and the MD5 of that +version. This information helps check if a package complies with +best practices for version numbers: the version number should increment +on each new release. +} +\keyword{internal} From fa0b67c9aa013496f209c40bc94bfba433620983 Mon Sep 17 00:00:00 2001 From: wlandau-lilly Date: Tue, 5 Mar 2024 05:26:02 -0500 Subject: [PATCH 2/8] Write version_issues.json --- .Rbuildignore | 4 ++- .gitignore | 2 ++ R/record_versions.R | 61 ++++++++++++++++++++++++++++-------------- man/record_versions.Rd | 23 +++++++++++----- 4 files changed, 63 insertions(+), 27 deletions(-) diff --git a/.Rbuildignore b/.Rbuildignore index 3ec5687..012eb7c 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -1,6 +1,8 @@ -^LICENSE\.md$ +^.*~$ +^.*\.json$ ^.*\.Rproj$ ^\.Rproj\.user$ +^LICENSE\.md$ ^main$ ^pull_request$ ^packages\.json$ diff --git a/.gitignore b/.gitignore index 234f028..37c1341 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*~ +*.json .Rproj.user .Rhistory .RData diff --git a/R/record_versions.R b/R/record_versions.R index 9cac95f..a55fe2f 100644 --- a/R/record_versions.R +++ b/R/record_versions.R @@ -3,29 +3,42 @@ #' @keywords internal #' @description Record the manifest of versions of packages #' and their MD5 hashes. -#' @details As well as their versions and MD5 hashes, this function -#' records the highest version ever recorded and the MD5 of that -#' version. This information helps check if a package complies with -#' best practices for version numbers: the version number should increment -#' on each new release. -#' @return `NULL` (invisibly). Writes a package manifest as a JSON file. +#' @details This function tracks a manifest containing the current version, +#' the current MD5 hash, the highest version ever released, and +#' the MD5 hash of the highest version ever released. Each time it runs, +#' it reads scrapes the package repository for the current releases, +#' reads the old manifest, and updates recorded highest version and MD5 +#' for all packages which incremented their version numbers. +#' After recording these incremented versions, the current version should +#' be the highest version, and the current and highest-version +#' MD5 hashes should agree. Packages +#' that fall out of alignment are recorded in a small JSON with only +#' the packages with version issues. +#' @return `NULL` (invisibly). Writes a package version manifest +#' and a manifest of version issues as JSON files. #' @param manifest Character of length 1, file path to the JSON manifest. +#' @param issues Character of length 1, file path to a JSON file +#' which records packages with version issues. #' @param repos Character string of package repositories to track. record_versions <- function( manifest = "versions.json", + issues = "version_issues.json", repos = "https://r-releases.r-universe.dev" ) { - current <- get_versions(repos = repos) + current <- get_current_versions(repos = repos) if (!file.exists(manifest)) { jsonlite::write_json(x = current, path = manifest, pretty = TRUE) return(invisible()) } - previous <- read_previous(manifest = manifest) - new <- update_manifest(current = current, previous = previous) + previous <- read_versions_previous(manifest = manifest) + new <- update_version_manifest(current = current, previous = previous) jsonlite::write_json(x = new, path = manifest, pretty = TRUE) + new_issues <- new[!versions_aligned(new = new),, drop = FALSE] # nolint + jsonlite::write_json(x = new_issues, path = issues, pretty = TRUE) + invisible() } -get_versions <- function(repos) { +get_current_versions <- function(repos) { out <- available.packages(repos = "https://r-releases.r-universe.dev") out <- as.data.frame(out) out <- out[, c("Package", "Version", "MD5sum")] @@ -34,7 +47,7 @@ get_versions <- function(repos) { out } -read_previous <- function(manifest) { +read_versions_previous <- function(manifest) { out <- jsonlite::read_json(path = manifest) out <- do.call(what = vctrs::vec_rbind, args = out) for (field in colnames(out)) { @@ -51,21 +64,29 @@ read_previous <- function(manifest) { out } -update_manifest <- function(current, previous) { +update_version_manifest <- function(current, previous) { new <- merge(x = current, y = previous, all = TRUE) - incremented <- apply( - X = new, + incremented <- manifest_compare_versions(manifest = new) > 0.5 + new$version_highest[incremented] <- new$version_current[incremented] + new$md5_highest[incremented] <- new$md5_current[incremented] + new +} + +manifest_compare_versions <- function(manifest) { + apply( + X = manifest, MARGIN = 1L, FUN = function(row) { - print(.subset2(row, "version_current")) - print(.subset2(row, "version_highest")) utils::compareVersion( a = .subset2(row, "version_current"), b = .subset2(row, "version_highest") - ) > 0.5 + ) } ) - new$version_highest[incremented] <- new$version_current[incremented] - new$md5_highest[incremented] <- new$md5_current[incremented] - new +} + +versions_aligned <- function(manifest) { + versions_agree <- manifest$version_current == manifest$version_highest + hashes_agree <- manifest$md5_current == manifest$md5_highest + versions_agree & hashes_agree } diff --git a/man/record_versions.Rd b/man/record_versions.Rd index 179ac55..400030a 100644 --- a/man/record_versions.Rd +++ b/man/record_versions.Rd @@ -6,26 +6,37 @@ \usage{ record_versions( manifest = "versions.json", + issues = "version_issues.json", repos = "https://r-releases.r-universe.dev" ) } \arguments{ \item{manifest}{Character of length 1, file path to the JSON manifest.} +\item{issues}{Character of length 1, file path to a JSON file +which records packages with version issues.} + \item{repos}{Character string of package repositories to track.} } \value{ -\code{NULL} (invisibly). Writes a package manifest as a JSON file. +\code{NULL} (invisibly). Writes a package version manifest +and a manifest of version issues as JSON files. } \description{ Record the manifest of versions of packages and their MD5 hashes. } \details{ -As well as their versions and MD5 hashes, this function -records the highest version ever recorded and the MD5 of that -version. This information helps check if a package complies with -best practices for version numbers: the version number should increment -on each new release. +This function tracks a manifest containing the current version, +the current MD5 hash, the highest version ever released, and +the MD5 hash of the highest version ever released. Each time it runs, +it reads scrapes the package repository for the current releases, +reads the old manifest, and updates recorded highest version and MD5 +for all packages which incremented their version numbers. +After recording these incremented versions, the current version should +be the highest version, and the current and highest-version +MD5 hashes should agree. Packages +that fall out of alignment are recorded in a small JSON with only +the packages with version issues. } \keyword{internal} From 9f7aae758ef14d592c9d646aff19f3ffe86d90ee Mon Sep 17 00:00:00 2001 From: wlandau-lilly Date: Tue, 5 Mar 2024 06:01:03 -0500 Subject: [PATCH 3/8] Test record_versions() --- DESCRIPTION | 1 + NAMESPACE | 3 + R/package.R | 1 + R/record_versions.R | 22 +++-- man/get_current_versions.Rd | 19 ++++ man/record_versions.Rd | 7 +- tests/test-record_versions.R | 169 +++++++++++++++++++++++++++++++++++ 7 files changed, 214 insertions(+), 8 deletions(-) create mode 100644 man/get_current_versions.Rd create mode 100644 tests/test-record_versions.R diff --git a/DESCRIPTION b/DESCRIPTION index a7efcd4..81b7c79 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -33,6 +33,7 @@ Imports: jsonlite, nanonext, pkgsearch, + utils, vctrs Encoding: UTF-8 Language: en-US diff --git a/NAMESPACE b/NAMESPACE index 4f22f63..6aab61f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,6 +4,7 @@ export(assert_cran_url) export(assert_package) export(assert_release_exists) export(build_universe) +export(get_current_versions) export(record_versions) export(review_pull_request) export(review_pull_requests) @@ -16,4 +17,6 @@ importFrom(nanonext,ncurl) importFrom(nanonext,parse_url) importFrom(nanonext,status_code) importFrom(pkgsearch,cran_package) +importFrom(utils,available.packages) +importFrom(utils,compareVersion) importFrom(vctrs,vec_rbind) diff --git a/R/package.R b/R/package.R index edd0f84..5f884aa 100644 --- a/R/package.R +++ b/R/package.R @@ -6,5 +6,6 @@ #' @importFrom jsonlite parse_json read_json write_json #' @importFrom nanonext ncurl parse_url status_code #' @importFrom pkgsearch cran_package +#' @importFrom utils available.packages compareVersion #' @importFrom vctrs vec_rbind NULL diff --git a/R/record_versions.R b/R/record_versions.R index a55fe2f..9798cb3 100644 --- a/R/record_versions.R +++ b/R/record_versions.R @@ -1,6 +1,5 @@ #' @title Record the manifest of package versions. #' @export -#' @keywords internal #' @description Record the manifest of versions of packages #' and their MD5 hashes. #' @details This function tracks a manifest containing the current version, @@ -20,12 +19,14 @@ #' @param issues Character of length 1, file path to a JSON file #' which records packages with version issues. #' @param repos Character string of package repositories to track. +#' @param current A data frame of current versions and hashes of packages +#' in `repos`. This argument is exposed for testing only. record_versions <- function( manifest = "versions.json", issues = "version_issues.json", - repos = "https://r-releases.r-universe.dev" + repos = "https://r-releases.r-universe.dev", + current = r.releases.utils::get_current_versions(repos = repos) ) { - current <- get_current_versions(repos = repos) if (!file.exists(manifest)) { jsonlite::write_json(x = current, path = manifest, pretty = TRUE) return(invisible()) @@ -33,13 +34,22 @@ record_versions <- function( previous <- read_versions_previous(manifest = manifest) new <- update_version_manifest(current = current, previous = previous) jsonlite::write_json(x = new, path = manifest, pretty = TRUE) - new_issues <- new[!versions_aligned(new = new),, drop = FALSE] # nolint + new_issues <- new[!versions_aligned(manifest = new),, drop = FALSE] # nolint jsonlite::write_json(x = new_issues, path = issues, pretty = TRUE) invisible() } -get_current_versions <- function(repos) { - out <- available.packages(repos = "https://r-releases.r-universe.dev") +#' @title Get the current versions of packages +#' @export +#' @keywords internal +#' @description Get the current versions of packages in the repos. +#' @return A data frame of packages with their current versions and MD5 +#' hashes. +#' @inheritParams record_versions +get_current_versions <- function( + repos = "https://r-releases.r-universe.dev" +) { + out <- utils::available.packages(repos = repos) out <- as.data.frame(out) out <- out[, c("Package", "Version", "MD5sum")] colnames(out) <- c("package", "version_current", "md5_current") diff --git a/man/get_current_versions.Rd b/man/get_current_versions.Rd new file mode 100644 index 0000000..e3c58e5 --- /dev/null +++ b/man/get_current_versions.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/record_versions.R +\name{get_current_versions} +\alias{get_current_versions} +\title{Get the current versions of packages} +\usage{ +get_current_versions(repos = "https://r-releases.r-universe.dev") +} +\arguments{ +\item{repos}{Character string of package repositories to track.} +} +\value{ +A data frame of packages with their current versions and MD5 +hashes. +} +\description{ +Get the current versions of packages in the repos. +} +\keyword{internal} diff --git a/man/record_versions.Rd b/man/record_versions.Rd index 400030a..c620afe 100644 --- a/man/record_versions.Rd +++ b/man/record_versions.Rd @@ -7,7 +7,8 @@ record_versions( manifest = "versions.json", issues = "version_issues.json", - repos = "https://r-releases.r-universe.dev" + repos = "https://r-releases.r-universe.dev", + current = r.releases.utils::get_current_versions(repos = repos) ) } \arguments{ @@ -17,6 +18,9 @@ record_versions( which records packages with version issues.} \item{repos}{Character string of package repositories to track.} + +\item{current}{A data frame of current versions and hashes of packages +in \code{repos}. This argument is exposed for testing only.} } \value{ \code{NULL} (invisibly). Writes a package version manifest @@ -39,4 +43,3 @@ MD5 hashes should agree. Packages that fall out of alignment are recorded in a small JSON with only the packages with version issues. } -\keyword{internal} diff --git a/tests/test-record_versions.R b/tests/test-record_versions.R new file mode 100644 index 0000000..cad27a7 --- /dev/null +++ b/tests/test-record_versions.R @@ -0,0 +1,169 @@ +# Temporary files used in the mock test. +manifest <- tempfile() +issues <- tempfile() + +# First update to the manifest. +contents <- data.frame( + package = c( + "package_unmodified", + "version_decremented", + "version_incremented", + "version_unmodified" + ), + version_current = rep("1.0.0", 4L), + md5_current = rep("md5_1.0.0", 4L) +) +r.releases.utils::record_versions( + manifest = manifest, + issues = issues, + current = contents +) +written <- jsonlite::read_json(manifest) +expected <- list( + list( + package = "package_unmodified", + version_current = "1.0.0", + md5_current = "md5_1.0.0" + ), + list( + package = "version_decremented", + version_current = "1.0.0", + md5_current = "md5_1.0.0" + ), + list( + package = "version_incremented", + version_current = "1.0.0", + md5_current = "md5_1.0.0" + ), + list( + package = "version_unmodified", + version_current = "1.0.0", + md5_current = "md5_1.0.0" + ) +) +stopifnot(identical(written, expected)) +stopifnot(!file.exists(issues)) + +# Update the manifest after no changes to packages or versions. +r.releases.utils::record_versions( + manifest = manifest, + issues = issues, + current = contents +) +written <- jsonlite::read_json(manifest) +expected <- list( + list( + package = "package_unmodified", + version_current = "1.0.0", + md5_current = "md5_1.0.0", + version_highest = "1.0.0", + md5_highest = "md5_1.0.0" + ), + list( + package = "version_decremented", + version_current = "1.0.0", + md5_current = "md5_1.0.0", + version_highest = "1.0.0", + md5_highest = "md5_1.0.0" + ), + list( + package = "version_incremented", + version_current = "1.0.0", + md5_current = "md5_1.0.0", + version_highest = "1.0.0", + md5_highest = "md5_1.0.0" + ), + list( + package = "version_unmodified", + version_current = "1.0.0", + md5_current = "md5_1.0.0", + version_highest = "1.0.0", + md5_highest = "md5_1.0.0" + ) +) +stopifnot(identical(written, expected)) +stopifnot(file.exists(issues)) +stopifnot(identical(jsonlite::read_json(issues), list())) + +# Update the packages in all the ways indicated above. +index <- contents$package == "version_decremented" +contents$version_current[index] <- "0.0.1" +contents$md5_current[index] <- "md5_0.0.1" +index <- contents$package == "version_incremented" +contents$version_current[index] <- "2.0.0" +contents$md5_current[index] <- "md5_2.0.0" +index <- contents$package == "version_unmodified" +contents$version_current[index] <- "1.0.0" +contents$md5_current[index] <- "md5_1.0.0-modified" +r.releases.utils::record_versions( + manifest = manifest, + issues = issues, + current = contents +) +written <- jsonlite::read_json(manifest) +expected <- list( + list( + package = "package_unmodified", + version_current = "1.0.0", + md5_current = "md5_1.0.0", + version_highest = "1.0.0", + md5_highest = "md5_1.0.0" + ), + list( + package = "version_decremented", + version_current = "0.0.1", + md5_current = "md5_0.0.1", + version_highest = "1.0.0", + md5_highest = "md5_1.0.0" + ), + list( + package = "version_incremented", + version_current = "2.0.0", + md5_current = "md5_2.0.0", + version_highest = "2.0.0", + md5_highest = "md5_2.0.0" + ), + list( + package = "version_unmodified", + version_current = "1.0.0", + md5_current = "md5_1.0.0-modified", + version_highest = "1.0.0", + md5_highest = "md5_1.0.0" + ) +) +stopifnot(identical(written, expected)) +stopifnot(file.exists(issues)) +written_issues <- jsonlite::read_json(issues) +expected_issues <- list( + list( + package = "version_decremented", + version_current = "0.0.1", + md5_current = "md5_0.0.1", + version_highest = "1.0.0", + md5_highest = "md5_1.0.0" + ), + list( + package = "version_unmodified", + version_current = "1.0.0", + md5_current = "md5_1.0.0-modified", + version_highest = "1.0.0", + md5_highest = "md5_1.0.0" + ) +) +stopifnot(identical(written_issues, expected_issues)) + +# Remove temporary files +unlink(c(manifest, issues)) + +# The manifest can be created and updated from the actual repo. +manifest <- tempfile() +issues <- tempfile() +r.releases.utils::record_versions(manifest = manifest, issues = issues) +stopifnot(file.exists(manifest)) +r.releases.utils::record_versions(manifest = manifest, issues = issues) +contents <- jsonlite::read_json(manifest) +stopifnot(is.character(contents[[1L]]$package)) +stopifnot(length(contents[[1L]]$package) == 1L) +stopifnot(file.exists(manifest)) +stopifnot(file.exists(issues)) +unlink(c(manifest, issues)) From 17ff61c073b2bfeb4b33a5da2e838187b1afe901 Mon Sep 17 00:00:00 2001 From: wlandau Date: Tue, 5 Mar 2024 07:55:51 -0500 Subject: [PATCH 4/8] Use the generic term 'hash' --- R/record_versions.R | 25 ++++++++-------- man/get_current_versions.Rd | 3 +- man/record_versions.Rd | 10 +++---- tests/test-record_versions.R | 56 ++++++++++++++++++------------------ 4 files changed, 46 insertions(+), 48 deletions(-) diff --git a/R/record_versions.R b/R/record_versions.R index 9798cb3..fdeeaa9 100644 --- a/R/record_versions.R +++ b/R/record_versions.R @@ -1,16 +1,16 @@ #' @title Record the manifest of package versions. #' @export #' @description Record the manifest of versions of packages -#' and their MD5 hashes. +#' and their hashes. #' @details This function tracks a manifest containing the current version, -#' the current MD5 hash, the highest version ever released, and -#' the MD5 hash of the highest version ever released. Each time it runs, +#' the current hash, the highest version ever released, and +#' the hash of the highest version ever released. Each time it runs, #' it reads scrapes the package repository for the current releases, -#' reads the old manifest, and updates recorded highest version and MD5 +#' reads the old manifest, and updates recorded highest version and hash #' for all packages which incremented their version numbers. #' After recording these incremented versions, the current version should #' be the highest version, and the current and highest-version -#' MD5 hashes should agree. Packages +#' hashes should agree. Packages #' that fall out of alignment are recorded in a small JSON with only #' the packages with version issues. #' @return `NULL` (invisibly). Writes a package version manifest @@ -43,8 +43,7 @@ record_versions <- function( #' @export #' @keywords internal #' @description Get the current versions of packages in the repos. -#' @return A data frame of packages with their current versions and MD5 -#' hashes. +#' @return A data frame of packages with their current versions and hashes. #' @inheritParams record_versions get_current_versions <- function( repos = "https://r-releases.r-universe.dev" @@ -52,7 +51,7 @@ get_current_versions <- function( out <- utils::available.packages(repos = repos) out <- as.data.frame(out) out <- out[, c("Package", "Version", "MD5sum")] - colnames(out) <- c("package", "version_current", "md5_current") + colnames(out) <- c("package", "version_current", "hash_current") rownames(out) <- NULL out } @@ -66,11 +65,11 @@ read_versions_previous <- function(manifest) { if (is.null(out$version_highest)) { out$version_highest <- out$version_current } - if (is.null(out$md5_highest)) { - out$md5_highest <- out$md5_current + if (is.null(out$hash_highest)) { + out$hash_highest <- out$hash_current } out$version_current <- NULL - out$md5_current <- NULL + out$hash_current <- NULL out } @@ -78,7 +77,7 @@ update_version_manifest <- function(current, previous) { new <- merge(x = current, y = previous, all = TRUE) incremented <- manifest_compare_versions(manifest = new) > 0.5 new$version_highest[incremented] <- new$version_current[incremented] - new$md5_highest[incremented] <- new$md5_current[incremented] + new$hash_highest[incremented] <- new$hash_current[incremented] new } @@ -97,6 +96,6 @@ manifest_compare_versions <- function(manifest) { versions_aligned <- function(manifest) { versions_agree <- manifest$version_current == manifest$version_highest - hashes_agree <- manifest$md5_current == manifest$md5_highest + hashes_agree <- manifest$hash_current == manifest$hash_highest versions_agree & hashes_agree } diff --git a/man/get_current_versions.Rd b/man/get_current_versions.Rd index e3c58e5..2f2247c 100644 --- a/man/get_current_versions.Rd +++ b/man/get_current_versions.Rd @@ -10,8 +10,7 @@ get_current_versions(repos = "https://r-releases.r-universe.dev") \item{repos}{Character string of package repositories to track.} } \value{ -A data frame of packages with their current versions and MD5 -hashes. +A data frame of packages with their current versions and hashes. } \description{ Get the current versions of packages in the repos. diff --git a/man/record_versions.Rd b/man/record_versions.Rd index c620afe..186af74 100644 --- a/man/record_versions.Rd +++ b/man/record_versions.Rd @@ -28,18 +28,18 @@ and a manifest of version issues as JSON files. } \description{ Record the manifest of versions of packages -and their MD5 hashes. +and their hashes. } \details{ This function tracks a manifest containing the current version, -the current MD5 hash, the highest version ever released, and -the MD5 hash of the highest version ever released. Each time it runs, +the current hash, the highest version ever released, and +the hash of the highest version ever released. Each time it runs, it reads scrapes the package repository for the current releases, -reads the old manifest, and updates recorded highest version and MD5 +reads the old manifest, and updates recorded highest version and hash for all packages which incremented their version numbers. After recording these incremented versions, the current version should be the highest version, and the current and highest-version -MD5 hashes should agree. Packages +hashes should agree. Packages that fall out of alignment are recorded in a small JSON with only the packages with version issues. } diff --git a/tests/test-record_versions.R b/tests/test-record_versions.R index cad27a7..c74447e 100644 --- a/tests/test-record_versions.R +++ b/tests/test-record_versions.R @@ -11,7 +11,7 @@ contents <- data.frame( "version_unmodified" ), version_current = rep("1.0.0", 4L), - md5_current = rep("md5_1.0.0", 4L) + hash_current = rep("hash_1.0.0", 4L) ) r.releases.utils::record_versions( manifest = manifest, @@ -23,22 +23,22 @@ expected <- list( list( package = "package_unmodified", version_current = "1.0.0", - md5_current = "md5_1.0.0" + hash_current = "hash_1.0.0" ), list( package = "version_decremented", version_current = "1.0.0", - md5_current = "md5_1.0.0" + hash_current = "hash_1.0.0" ), list( package = "version_incremented", version_current = "1.0.0", - md5_current = "md5_1.0.0" + hash_current = "hash_1.0.0" ), list( package = "version_unmodified", version_current = "1.0.0", - md5_current = "md5_1.0.0" + hash_current = "hash_1.0.0" ) ) stopifnot(identical(written, expected)) @@ -55,30 +55,30 @@ expected <- list( list( package = "package_unmodified", version_current = "1.0.0", - md5_current = "md5_1.0.0", + hash_current = "hash_1.0.0", version_highest = "1.0.0", - md5_highest = "md5_1.0.0" + hash_highest = "hash_1.0.0" ), list( package = "version_decremented", version_current = "1.0.0", - md5_current = "md5_1.0.0", + hash_current = "hash_1.0.0", version_highest = "1.0.0", - md5_highest = "md5_1.0.0" + hash_highest = "hash_1.0.0" ), list( package = "version_incremented", version_current = "1.0.0", - md5_current = "md5_1.0.0", + hash_current = "hash_1.0.0", version_highest = "1.0.0", - md5_highest = "md5_1.0.0" + hash_highest = "hash_1.0.0" ), list( package = "version_unmodified", version_current = "1.0.0", - md5_current = "md5_1.0.0", + hash_current = "hash_1.0.0", version_highest = "1.0.0", - md5_highest = "md5_1.0.0" + hash_highest = "hash_1.0.0" ) ) stopifnot(identical(written, expected)) @@ -88,13 +88,13 @@ stopifnot(identical(jsonlite::read_json(issues), list())) # Update the packages in all the ways indicated above. index <- contents$package == "version_decremented" contents$version_current[index] <- "0.0.1" -contents$md5_current[index] <- "md5_0.0.1" +contents$hash_current[index] <- "hash_0.0.1" index <- contents$package == "version_incremented" contents$version_current[index] <- "2.0.0" -contents$md5_current[index] <- "md5_2.0.0" +contents$hash_current[index] <- "hash_2.0.0" index <- contents$package == "version_unmodified" contents$version_current[index] <- "1.0.0" -contents$md5_current[index] <- "md5_1.0.0-modified" +contents$hash_current[index] <- "hash_1.0.0-modified" r.releases.utils::record_versions( manifest = manifest, issues = issues, @@ -105,30 +105,30 @@ expected <- list( list( package = "package_unmodified", version_current = "1.0.0", - md5_current = "md5_1.0.0", + hash_current = "hash_1.0.0", version_highest = "1.0.0", - md5_highest = "md5_1.0.0" + hash_highest = "hash_1.0.0" ), list( package = "version_decremented", version_current = "0.0.1", - md5_current = "md5_0.0.1", + hash_current = "hash_0.0.1", version_highest = "1.0.0", - md5_highest = "md5_1.0.0" + hash_highest = "hash_1.0.0" ), list( package = "version_incremented", version_current = "2.0.0", - md5_current = "md5_2.0.0", + hash_current = "hash_2.0.0", version_highest = "2.0.0", - md5_highest = "md5_2.0.0" + hash_highest = "hash_2.0.0" ), list( package = "version_unmodified", version_current = "1.0.0", - md5_current = "md5_1.0.0-modified", + hash_current = "hash_1.0.0-modified", version_highest = "1.0.0", - md5_highest = "md5_1.0.0" + hash_highest = "hash_1.0.0" ) ) stopifnot(identical(written, expected)) @@ -138,16 +138,16 @@ expected_issues <- list( list( package = "version_decremented", version_current = "0.0.1", - md5_current = "md5_0.0.1", + hash_current = "hash_0.0.1", version_highest = "1.0.0", - md5_highest = "md5_1.0.0" + hash_highest = "hash_1.0.0" ), list( package = "version_unmodified", version_current = "1.0.0", - md5_current = "md5_1.0.0-modified", + hash_current = "hash_1.0.0-modified", version_highest = "1.0.0", - md5_highest = "md5_1.0.0" + hash_highest = "hash_1.0.0" ) ) stopifnot(identical(written_issues, expected_issues)) From 81bdb65cb50c3e5426b3a2b67a0e1ce5c9508991 Mon Sep 17 00:00:00 2001 From: wlandau Date: Tue, 5 Mar 2024 08:40:42 -0500 Subject: [PATCH 5/8] Add check_hash argument --- R/record_versions.R | 31 ++++++++-------- man/record_versions.Rd | 19 +++++----- tests/test-record_versions.R | 70 +++++++++++++++++++++++++++++++----- 3 files changed, 87 insertions(+), 33 deletions(-) diff --git a/R/record_versions.R b/R/record_versions.R index fdeeaa9..ab19547 100644 --- a/R/record_versions.R +++ b/R/record_versions.R @@ -4,15 +4,9 @@ #' and their hashes. #' @details This function tracks a manifest containing the current version, #' the current hash, the highest version ever released, and -#' the hash of the highest version ever released. Each time it runs, -#' it reads scrapes the package repository for the current releases, -#' reads the old manifest, and updates recorded highest version and hash -#' for all packages which incremented their version numbers. -#' After recording these incremented versions, the current version should -#' be the highest version, and the current and highest-version -#' hashes should agree. Packages -#' that fall out of alignment are recorded in a small JSON with only -#' the packages with version issues. +#' the hash of the highest version ever released. It uses this information +#' to determine whether the package complies with best +#' practices for version numbers. #' @return `NULL` (invisibly). Writes a package version manifest #' and a manifest of version issues as JSON files. #' @param manifest Character of length 1, file path to the JSON manifest. @@ -21,11 +15,15 @@ #' @param repos Character string of package repositories to track. #' @param current A data frame of current versions and hashes of packages #' in `repos`. This argument is exposed for testing only. +#' @param check_hash Logical of length 1, check hashes when judging package +#' version compliance. This allows [record_versions()] to flag packages +#' that create new releases but keep the same version number. record_versions <- function( manifest = "versions.json", issues = "version_issues.json", repos = "https://r-releases.r-universe.dev", - current = r.releases.utils::get_current_versions(repos = repos) + current = r.releases.utils::get_current_versions(repos = repos), + check_hash = TRUE ) { if (!file.exists(manifest)) { jsonlite::write_json(x = current, path = manifest, pretty = TRUE) @@ -34,7 +32,8 @@ record_versions <- function( previous <- read_versions_previous(manifest = manifest) new <- update_version_manifest(current = current, previous = previous) jsonlite::write_json(x = new, path = manifest, pretty = TRUE) - new_issues <- new[!versions_aligned(manifest = new),, drop = FALSE] # nolint + aligned <- versions_aligned(manifest = new, check_hash = check_hash) + new_issues <- new[!aligned,, drop = FALSE] # nolint jsonlite::write_json(x = new_issues, path = issues, pretty = TRUE) invisible() } @@ -94,8 +93,10 @@ manifest_compare_versions <- function(manifest) { ) } -versions_aligned <- function(manifest) { - versions_agree <- manifest$version_current == manifest$version_highest - hashes_agree <- manifest$hash_current == manifest$hash_highest - versions_agree & hashes_agree +versions_aligned <- function(manifest, check_hash) { + aligned <- manifest$version_current == manifest$version_highest + if (check_hash) { + aligned <- aligned & (manifest$hash_current == manifest$hash_highest) + } + aligned } diff --git a/man/record_versions.Rd b/man/record_versions.Rd index 186af74..8eed436 100644 --- a/man/record_versions.Rd +++ b/man/record_versions.Rd @@ -8,7 +8,8 @@ record_versions( manifest = "versions.json", issues = "version_issues.json", repos = "https://r-releases.r-universe.dev", - current = r.releases.utils::get_current_versions(repos = repos) + current = r.releases.utils::get_current_versions(repos = repos), + check_hash = TRUE ) } \arguments{ @@ -21,6 +22,10 @@ which records packages with version issues.} \item{current}{A data frame of current versions and hashes of packages in \code{repos}. This argument is exposed for testing only.} + +\item{check_hash}{Logical of length 1, check hashes when judging package +version compliance. This allows \code{\link[=record_versions]{record_versions()}} to flag packages +that create new releases but keep the same version number.} } \value{ \code{NULL} (invisibly). Writes a package version manifest @@ -33,13 +38,7 @@ and their hashes. \details{ This function tracks a manifest containing the current version, the current hash, the highest version ever released, and -the hash of the highest version ever released. Each time it runs, -it reads scrapes the package repository for the current releases, -reads the old manifest, and updates recorded highest version and hash -for all packages which incremented their version numbers. -After recording these incremented versions, the current version should -be the highest version, and the current and highest-version -hashes should agree. Packages -that fall out of alignment are recorded in a small JSON with only -the packages with version issues. +the hash of the highest version ever released. It uses this information +to determine whether the package complies with best +practices for version numbers. } diff --git a/tests/test-record_versions.R b/tests/test-record_versions.R index c74447e..c845392 100644 --- a/tests/test-record_versions.R +++ b/tests/test-record_versions.R @@ -95,10 +95,71 @@ contents$hash_current[index] <- "hash_2.0.0" index <- contents$package == "version_unmodified" contents$version_current[index] <- "1.0.0" contents$hash_current[index] <- "hash_1.0.0-modified" +for (index in seq_len(2L)) { + r.releases.utils::record_versions( + manifest = manifest, + issues = issues, + current = contents + ) + written <- jsonlite::read_json(manifest) + expected <- list( + list( + package = "package_unmodified", + version_current = "1.0.0", + hash_current = "hash_1.0.0", + version_highest = "1.0.0", + hash_highest = "hash_1.0.0" + ), + list( + package = "version_decremented", + version_current = "0.0.1", + hash_current = "hash_0.0.1", + version_highest = "1.0.0", + hash_highest = "hash_1.0.0" + ), + list( + package = "version_incremented", + version_current = "2.0.0", + hash_current = "hash_2.0.0", + version_highest = "2.0.0", + hash_highest = "hash_2.0.0" + ), + list( + package = "version_unmodified", + version_current = "1.0.0", + hash_current = "hash_1.0.0-modified", + version_highest = "1.0.0", + hash_highest = "hash_1.0.0" + ) + ) + stopifnot(identical(written, expected)) + stopifnot(file.exists(issues)) + written_issues <- jsonlite::read_json(issues) + expected_issues <- list( + list( + package = "version_decremented", + version_current = "0.0.1", + hash_current = "hash_0.0.1", + version_highest = "1.0.0", + hash_highest = "hash_1.0.0" + ), + list( + package = "version_unmodified", + version_current = "1.0.0", + hash_current = "hash_1.0.0-modified", + version_highest = "1.0.0", + hash_highest = "hash_1.0.0" + ) + ) + stopifnot(identical(written_issues, expected_issues)) +} + +# Same, but do not check the hash. r.releases.utils::record_versions( manifest = manifest, issues = issues, - current = contents + current = contents, + check_hash = FALSE ) written <- jsonlite::read_json(manifest) expected <- list( @@ -141,13 +202,6 @@ expected_issues <- list( hash_current = "hash_0.0.1", version_highest = "1.0.0", hash_highest = "hash_1.0.0" - ), - list( - package = "version_unmodified", - version_current = "1.0.0", - hash_current = "hash_1.0.0-modified", - version_highest = "1.0.0", - hash_highest = "hash_1.0.0" ) ) stopifnot(identical(written_issues, expected_issues)) From 2c39169ba7e27746fb317f5099d06af10f67c2fd Mon Sep 17 00:00:00 2001 From: wlandau Date: Tue, 5 Mar 2024 11:58:33 -0500 Subject: [PATCH 6/8] swap lines --- R/record_versions.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/record_versions.R b/R/record_versions.R index ab19547..2a0bd82 100644 --- a/R/record_versions.R +++ b/R/record_versions.R @@ -48,8 +48,8 @@ get_current_versions <- function( repos = "https://r-releases.r-universe.dev" ) { out <- utils::available.packages(repos = repos) - out <- as.data.frame(out) out <- out[, c("Package", "Version", "MD5sum")] + out <- as.data.frame(out) colnames(out) <- c("package", "version_current", "hash_current") rownames(out) <- NULL out From 6addffaeb595313504a299a2ba59e2b437f33e97 Mon Sep 17 00:00:00 2001 From: wlandau Date: Tue, 5 Mar 2024 12:00:51 -0500 Subject: [PATCH 7/8] lapply() --- R/record_versions.R | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/R/record_versions.R b/R/record_versions.R index 2a0bd82..3bbbd50 100644 --- a/R/record_versions.R +++ b/R/record_versions.R @@ -58,9 +58,7 @@ get_current_versions <- function( read_versions_previous <- function(manifest) { out <- jsonlite::read_json(path = manifest) out <- do.call(what = vctrs::vec_rbind, args = out) - for (field in colnames(out)) { - out[[field]] <- as.character(out[[field]]) - } + out <- lapply(out, as.character) if (is.null(out$version_highest)) { out$version_highest <- out$version_current } From 455de781dceefa6c17a1d56a5eb8386ae777ba5b Mon Sep 17 00:00:00 2001 From: wlandau Date: Tue, 5 Mar 2024 12:02:31 -0500 Subject: [PATCH 8/8] comparison --- R/record_versions.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/record_versions.R b/R/record_versions.R index 3bbbd50..89b660d 100644 --- a/R/record_versions.R +++ b/R/record_versions.R @@ -72,7 +72,7 @@ read_versions_previous <- function(manifest) { update_version_manifest <- function(current, previous) { new <- merge(x = current, y = previous, all = TRUE) - incremented <- manifest_compare_versions(manifest = new) > 0.5 + incremented <- manifest_compare_versions(manifest = new) == 1L new$version_highest[incremented] <- new$version_current[incremented] new$hash_highest[incremented] <- new$hash_current[incremented] new