diff --git a/R/manifest.R b/R/manifest.R index 706d6386..1ac7435c 100644 --- a/R/manifest.R +++ b/R/manifest.R @@ -189,15 +189,54 @@ if (.is_len_0(version_file)) { return(paste0("Project: ", projr_version_get())) } + + # Get current and previous project versions + current_version <- projr_version_get() + previous_version <- NULL + if (any(grepl("^Project: ", version_file))) { + project_line <- grep("^Project: ", version_file, value = TRUE)[[1]] + previous_version <- gsub("^Project: ", "", project_line) |> trimws() version_file <- version_file[!grepl("^Project: ", version_file)] } - c(paste0("Project: ", projr_version_get()), version_file) + + # If project version has changed, mark existing labels with # to indicate + # they may be out of date with the new project version + if (!is.null(previous_version) && previous_version != current_version) { + version_file <- .version_file_mark_labels_stale(version_file) + } + + c(paste0("Project: ", current_version), version_file) +} + +# Mark label versions with # to indicate they may be stale relative to new project version +.version_file_mark_labels_stale <- function(version_file) { + if (.is_len_0(version_file)) { + return(version_file) + } + + # Process each line + vapply(version_file, function(line) { + # Skip if not a label line + if (!grepl(": ", line)) { + return(line) + } + + # Skip if already marked with # or * + if (grepl("#$|\\*$", line)) { + return(line) + } + + # Add # to indicate this version may be stale + paste0(line, "#") + }, character(1), USE.NAMES = FALSE) } .version_file_update_label_version <- function(version_file, # nolint label, add_asterisk) { + # When updating a label, remove the # marker (if present) since we're now uploading it + # The # marker indicates a label that may be stale relative to the current project version version_add <- if (add_asterisk) { .version_get() |> paste0("*") } else { diff --git a/R/remote-versions.R b/R/remote-versions.R index 433f1d97..4a6d3fe1 100644 --- a/R/remote-versions.R +++ b/R/remote-versions.R @@ -408,7 +408,7 @@ if (is.null(remote_pre)) { return(character(0L)) } - remote_final_vec_basename <- .( + remote_final_vec_basename <- .remote_final_ls( type, remote_pre_down ) .remote_version_latest_get(remote_final_vec_basename, type, label) |> @@ -498,10 +498,12 @@ if (.is_len_0(label_regex)) { return(character(0L)) } - # Extract version, removing the asterisk if present - version_with_possible_asterisk <- gsub(match_str, "", label_regex) |> trimws() - # Remove asterisk for version comparison purposes but don't mark as trusted - gsub("\\*$", "", version_with_possible_asterisk) |> .version_v_rm() + # Extract version, removing the asterisk and hash if present + # * indicates untrusted upload + # # indicates potentially stale (not checked against current project version) + version_with_markers <- gsub(match_str, "", label_regex) |> trimws() + # Remove asterisk and hash for version comparison purposes + gsub("\\*$|#$", "", version_with_markers) |> .version_v_rm() } diff --git a/man/dot-gh_release_create_httr.Rd b/man/dot-gh_release_create_httr.Rd new file mode 100644 index 00000000..6c334698 --- /dev/null +++ b/man/dot-gh_release_create_httr.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/remote-github-httr.R +\name{.gh_release_create_httr} +\alias{.gh_release_create_httr} +\title{Create a GitHub release using httr} +\usage{ +.gh_release_create_httr( + repo, + tag, + description, + api_url = NULL, + token = NULL, + draft = FALSE, + prerelease = FALSE, + target_commitish = NULL +) +} +\arguments{ +\item{repo}{Character string. Repository in format "owner/repo".} + +\item{tag}{Character string. Release tag.} + +\item{description}{Character string. Release body text.} + +\item{api_url}{Character string. Optional GitHub API URL (for Enterprise).} + +\item{token}{Character string. Optional GitHub token. If not supplied, +\code{.auth_get_github_pat_find()} is used.} + +\item{draft}{Logical. Whether the release should be created as a draft.} + +\item{target_commitish}{Character string. Optional target commitish +(branch or SHA). If NULL, GitHub uses the default branch.} +} +\value{ +Parsed release object (list) from GitHub API on success. +} +\description{ +Creates a GitHub release for the given tag using the GitHub REST API. +} +\keyword{internal} diff --git a/man/dot-gh_api_base.Rd b/man/dot-github_api_base.Rd similarity index 91% rename from man/dot-gh_api_base.Rd rename to man/dot-github_api_base.Rd index 0c50149b..bf2d52bb 100644 --- a/man/dot-gh_api_base.Rd +++ b/man/dot-github_api_base.Rd @@ -1,7 +1,7 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/remote-github-httr.R -\name{.gh_api_base} -\alias{.gh_api_base} +\name{.github_api_base} +\alias{.github_api_base} \title{Get GitHub API base URL} \usage{ .github_api_base(api_url = NULL) diff --git a/man/dot-gh_release_exists_httr.Rd b/man/dot-remote_check_exists_github_httr.Rd similarity index 70% rename from man/dot-gh_release_exists_httr.Rd rename to man/dot-remote_check_exists_github_httr.Rd index f8352f75..e92cdfd2 100644 --- a/man/dot-gh_release_exists_httr.Rd +++ b/man/dot-remote_check_exists_github_httr.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/remote-github-httr.R -\name{.gh_release_exists_httr} -\alias{.gh_release_exists_httr} +\name{.remote_check_exists_github_httr} +\alias{.remote_check_exists_github_httr} \title{Check if a GitHub release exists using httr} \usage{ -.gh_release_exists_httr(repo, tag, api_url = NULL, token = NULL) +.remote_check_exists_github_httr(repo, tag, api_url = NULL, token = NULL) } \arguments{ \item{repo}{Character string. Repository in format "owner/repo".} @@ -17,11 +17,10 @@ \code{.auth_get_github_pat_find()}.} } \value{ -Logical TRUE/FALSE if release exists/doesn't exist, NULL on auth errors. +Logical TRUE/FALSE if release exists/doesn't exist and stops on auth errors } \description{ Uses the GitHub API directly to check if a release with the given tag exists. This is faster and more explicit than relying on piggyback internals. -Returns NULL on authentication errors (401/403) to allow fallback to piggyback. } \keyword{internal} diff --git a/man/dot-remote_file_get_all_github_httr.Rd b/man/dot-remote_file_get_all_github_httr.Rd new file mode 100644 index 00000000..9e73e88a --- /dev/null +++ b/man/dot-remote_file_get_all_github_httr.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/remote-github-httr.R +\name{.remote_file_get_all_github_httr} +\alias{.remote_file_get_all_github_httr} +\title{Download all assets from a GitHub release using httr} +\usage{ +.remote_file_get_all_github_httr( + repo, + tag, + fn, + dest_dir, + api_url = NULL, + token = NULL, + overwrite = TRUE, + output_level = "std", + log_file = NULL +) +} +\arguments{ +\item{repo}{Character string. Repository in format "owner/repo".} + +\item{tag}{Character string. Release tag.} + +\item{dest_dir}{Character string. Local directory to save assets into. +Created if it does not exist.} + +\item{api_url}{Character string. Optional GitHub API URL.} + +\item{token}{Character string. Optional GitHub token. If not supplied, +\code{.auth_get_github_pat_find()} is used.} + +\item{overwrite}{Logical. If FALSE, existing files are left untouched.} + +\item{output_level}{Character. Verbosity control passed to \code{.cli_debug()}.} + +\item{log_file}{Optional log file path for \code{.cli_debug()}.} +} +\value{ +Character vector of downloaded file paths (invisibly). +} +\description{ +Downloads every asset attached to a GitHub release (identified by tag) +into a local directory. Uses the GitHub API via \code{httr} and supports +GitHub Enterprise via \code{api_url}. +} +\keyword{internal} diff --git a/man/dot-gh_release_asset_delete_httr.Rd b/man/dot-remote_final_empty_github_httr.Rd similarity index 76% rename from man/dot-gh_release_asset_delete_httr.Rd rename to man/dot-remote_final_empty_github_httr.Rd index 82db45cf..fd6fa52d 100644 --- a/man/dot-gh_release_asset_delete_httr.Rd +++ b/man/dot-remote_final_empty_github_httr.Rd @@ -1,19 +1,19 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/remote-github-httr.R -\name{.gh_release_asset_delete_httr} -\alias{.gh_release_asset_delete_httr} +\name{.remote_final_empty_github_httr} +\alias{.remote_final_empty_github_httr} \title{Delete a release asset using httr} \usage{ -.gh_release_asset_delete_httr(repo, asset_id, api_url = NULL, token = NULL) +.remote_final_empty_github_httr(repo, tag, fn, api_url = NULL, token = NULL) } \arguments{ \item{repo}{Character string. Repository in format "owner/repo".} -\item{asset_id}{Numeric. Asset ID to delete.} - \item{api_url}{Character string. Optional GitHub API URL.} \item{token}{Character string. Optional GitHub token.} + +\item{asset_id}{Numeric. Asset ID to delete.} } \value{ Logical. TRUE if successful. diff --git a/man/dot-gh_release_get_httr.Rd b/man/dot-remote_ls_final_github_httr.Rd similarity index 79% rename from man/dot-gh_release_get_httr.Rd rename to man/dot-remote_ls_final_github_httr.Rd index 43245b45..be31dc81 100644 --- a/man/dot-gh_release_get_httr.Rd +++ b/man/dot-remote_ls_final_github_httr.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/remote-github-httr.R -\name{.gh_release_get_httr} -\alias{.gh_release_get_httr} +\name{.remote_ls_final_github_httr} +\alias{.remote_ls_final_github_httr} \title{Get release information by tag using httr} \usage{ -.gh_release_get_httr(repo, tag, api_url = NULL, token = NULL) +.remote_ls_final_github_httr(repo, tag, api_url = NULL, token = NULL) } \arguments{ \item{repo}{Character string. Repository in format "owner/repo".} diff --git a/man/projr_osf_create_project.Rd b/man/projr_osf_create_project.Rd index cf9a9402..ca5c18b4 100644 --- a/man/projr_osf_create_project.Rd +++ b/man/projr_osf_create_project.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/remote.R +% Please edit documentation in R/remote-osf.R \name{projr_osf_create_project} \alias{projr_osf_create_project} \title{Create a new project on OSF} diff --git a/tests/testthat/test-version-file-hash-marker.R b/tests/testthat/test-version-file-hash-marker.R new file mode 100644 index 00000000..d164167d --- /dev/null +++ b/tests/testthat/test-version-file-hash-marker.R @@ -0,0 +1,142 @@ +test_that(".version_file_mark_labels_stale marks labels with hash", { + skip_if(.is_test_select()) + + # Test with no labels + version_file <- character(0L) + result <- .version_file_mark_labels_stale(version_file) + expect_identical(result, character(0L)) + + # Test with one label + version_file <- c("raw-data: v0.3.0") + result <- .version_file_mark_labels_stale(version_file) + expect_identical(result, c("raw-data: v0.3.0#")) + + # Test with multiple labels + version_file <- c("raw-data: v0.3.0", "cache: v0.2.0") + result <- .version_file_mark_labels_stale(version_file) + expect_identical(result, c("raw-data: v0.3.0#", "cache: v0.2.0#")) + + # Test with label already having asterisk (don't add hash) + version_file <- c("raw-data: v0.3.0*") + result <- .version_file_mark_labels_stale(version_file) + expect_identical(result, c("raw-data: v0.3.0*")) + + # Test with label already having hash (don't add another) + version_file <- c("raw-data: v0.3.0#") + result <- .version_file_mark_labels_stale(version_file) + expect_identical(result, c("raw-data: v0.3.0#")) + + # Test with mixed markers + version_file <- c("raw-data: v0.3.0", "cache: v0.2.0*", "output: v0.1.0#") + result <- .version_file_mark_labels_stale(version_file) + expect_identical(result, c("raw-data: v0.3.0#", "cache: v0.2.0*", "output: v0.1.0#")) +}) + +test_that(".version_file_update_project_version marks labels stale when version changes", { + skip_if(.is_test_select()) + dir_test <- .test_setup_project(git = FALSE, set_env_var = TRUE) + usethis::with_project( + path = dir_test, + code = { + # Start with empty version file + version_file <- character(0L) + result <- .version_file_update_project_version(version_file) + expect_identical(result[[1]], paste0("Project: ", projr_version_get())) + expect_identical(length(result), 1L) + + # Add a label version, then update project version (no change) + projr_version_set("0.0.1") + version_file <- c("Project: 0.0.1", "raw-data: v0.0.1") + + result <- .version_file_update_project_version(version_file) + # Should not add hash since version didn't change + expect_identical(result, c("Project: 0.0.1", "raw-data: v0.0.1")) + + # Now bump project version and update + projr_version_set("0.0.2") + result <- .version_file_update_project_version(version_file) + # Should add hash to raw-data since project version changed + expect_identical(result[[1]], "Project: 0.0.2") + expect_identical(result[[2]], "raw-data: v0.0.1#") + + # Multiple labels with version change + projr_version_set("0.0.3") + version_file <- c("Project: 0.0.2", "raw-data: v0.0.2", "cache: v0.0.1") + result <- .version_file_update_project_version(version_file) + expect_identical(result[[1]], "Project: 0.0.3") + expect_identical(result[[2]], "raw-data: v0.0.2#") + expect_identical(result[[3]], "cache: v0.0.1#") + + # Label with asterisk should keep it when adding hash (but only add hash, not replace) + projr_version_set("0.0.4") + version_file <- c("Project: 0.0.3", "raw-data: v0.0.3*") + result <- .version_file_update_project_version(version_file) + expect_identical(result[[1]], "Project: 0.0.4") + expect_identical(result[[2]], "raw-data: v0.0.3*") + } + ) +}) + +test_that(".version_file_update_label_version removes hash marker", { + skip_if(.is_test_select()) + dir_test <- .test_setup_project(git = FALSE, set_env_var = TRUE) + usethis::with_project( + path = dir_test, + code = { + projr_version_set("0.0.5") + + # Add new label (no hash to remove) + version_file <- c("Project: 0.0.5") + result <- .version_file_update_label_version(version_file, "raw-data", FALSE) + expect_identical(result[[2]], "raw-data: 0.0.5") + + # Update existing label with hash (should remove hash) + version_file <- c("Project: 0.0.5", "raw-data: v0.0.3#") + result <- .version_file_update_label_version(version_file, "raw-data", FALSE) + expect_identical(result[[2]], "raw-data: 0.0.5") + expect_false(grepl("#", result[[2]])) + + # Update with asterisk should not have hash + version_file <- c("Project: 0.0.5", "raw-data: v0.0.3#") + result <- .version_file_update_label_version(version_file, "raw-data", TRUE) + expect_identical(result[[2]], "raw-data: 0.0.5*") + expect_false(grepl("#", result[[2]])) + } + ) +}) + +test_that(".remote_get_version_label_non_project_file_extract handles hash marker", { + skip_if(.is_test_select()) + + # Test extracting version without markers + version_file <- c("Project: v0.5.0", "raw-data: v0.3.0") + result <- .remote_get_version_label_non_project_file_extract(version_file, "raw-data") + expect_identical(result, "0.3.0") + + # Test extracting version with hash marker + version_file <- c("Project: v0.5.0", "raw-data: v0.3.0#") + result <- .remote_get_version_label_non_project_file_extract(version_file, "raw-data") + expect_identical(result, "0.3.0") + + # Test extracting version with asterisk marker + version_file <- c("Project: v0.5.0", "raw-data: v0.3.0*") + result <- .remote_get_version_label_non_project_file_extract(version_file, "raw-data") + expect_identical(result, "0.3.0") + + # Test extracting version with both markers should not happen in practice + # but should still extract version correctly + version_file <- c("Project: v0.5.0", "raw-data: v0.3.0#") + result <- .remote_get_version_label_non_project_file_extract(version_file, "raw-data") + expect_identical(result, "0.3.0") + + # Test with multiple labels, extract specific one + version_file <- c("Project: v0.5.0", "raw-data: v0.3.0#", "cache: v0.2.0", "output: v0.4.0*") + result <- .remote_get_version_label_non_project_file_extract(version_file, "cache") + expect_identical(result, "0.2.0") + + result <- .remote_get_version_label_non_project_file_extract(version_file, "raw-data") + expect_identical(result, "0.3.0") + + result <- .remote_get_version_label_non_project_file_extract(version_file, "output") + expect_identical(result, "0.4.0") +})