From 6c2012b1462a1d79b7ac73eda5ff1619782e2b8f Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Sun, 5 Mar 2023 11:49:31 -0600 Subject: [PATCH] Reduce number of manual git release steps (#1746) * Reduce number of manual release steps * `use_release_issue()` checks default branch and pulls automatically. * `use_github_release()` automatically pushes, if safe. * `use_github_release()` just creates the release. * `use_version()` gains `push` argument. Fixes #1385. Closes #1678. * Restore initial git pull * Extract out git_push_first() * Apply suggestions from code review Co-authored-by: Jennifer (Jenny) Bryan * WS * Re-document * Tweak argument order * Make some cli helper wrappers * Don't mess with function order * Update NEWS * Tweak word * Tweak comment * Polish docs * Don't mention release() --------- Co-authored-by: Jennifer (Jenny) Bryan --- NEWS.md | 8 +++++ R/github.R | 15 ++------- R/pr.R | 15 +++------ R/proj-desc.R | 2 +- R/release.R | 53 ++++++++++++++++-------------- R/ui.R | 19 +++++++++-- R/utils-git.R | 56 ++++++++++++++++++++++++++++++-- R/version.R | 12 ++++--- man/use_github_release.Rd | 31 ++++++++++-------- man/use_version.Rd | 11 ++++--- tests/testthat/_snaps/release.md | 20 +++++------- 11 files changed, 155 insertions(+), 87 deletions(-) diff --git a/NEWS.md b/NEWS.md index fdf254e0c..7c15f618a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,13 @@ # usethis (development version) +* `use_version()` gains a `push` argument to optionally push the result after + committing. This is used to eliminate a manual step from the + `use_release_issue()` checklist (#1385). + +* `use_github_release()` now automatically pushes to GitHub (if safe) (#1385) + and automatically publishes the release, rather than requiring you to edit + and publish the draft. + * `use_tidy_logo()` is a new function that calls `use_logo()` on the appropriate hex sticker PNG file at (@ateucher, #1871). diff --git a/R/github.R b/R/github.R index d61b513fd..691a4fced 100644 --- a/R/github.R +++ b/R/github.R @@ -158,7 +158,7 @@ use_github <- function(organisation = NULL, if (is_package()) { # we tryCatch(), because we can't afford any failure here to result in not - # making the first push and configuring default branch + # doing the first push and configuring the default branch # such an incomplete setup is hard to diagnose / repair post hoc tryCatch( use_github_links(), @@ -166,18 +166,9 @@ use_github <- function(organisation = NULL, ) } - repo <- git_repo() - remref <- glue("origin/{default_branch}") - ui_done(" - Pushing {ui_value(default_branch)} branch to GitHub and setting \\ - {ui_value(remref)} as upstream branch") - gert::git_push( - remote = "origin", - set_upstream = TRUE, - repo = repo, - verbose = FALSE - ) + git_push_first(default_branch, "origin") + repo <- git_repo() gbl <- gert::git_branch_list(local = TRUE, repo = repo) if (nrow(gbl) > 1) { ui_done(" diff --git a/R/pr.R b/R/pr.R index b69d59dfc..26de04689 100644 --- a/R/pr.R +++ b/R/pr.R @@ -383,22 +383,15 @@ pr_push <- function() { ) title <- glue("Which repo do you want to push to?") choice <- utils::menu(choices, graphics = FALSE, title = title) - remote <- names(choices)[[choice]] + remote <- names(choices)[[choice]] } else { remote <- "origin" } - ui_done(" - Pushing local {ui_value(branch)} branch to {ui_value(remote)} remote.") - gert::git_push(remote = remote, verbose = FALSE, repo = repo) + + git_push_first(branch, remote) } else { check_branch_pulled(use = "pr_pull()") - ui_done("Pushing local {ui_value(branch)} branch to {ui_value(remref)}.") - gert::git_push( - remote = remref_remote(remref), - refspec = glue("refs/heads/{branch}:refs/heads/{remref_branch(remref)}"), - verbose = FALSE, - repo = repo - ) + git_push(branch, remref) } # Prompt to create PR if does not exist yet diff --git a/R/proj-desc.R b/R/proj-desc.R index 53f12ab26..ec7f20a70 100644 --- a/R/proj-desc.R +++ b/R/proj-desc.R @@ -22,7 +22,7 @@ proj_desc_create <- function(name, fields = list(), roxygen = TRUE) { write_over(proj_path("DESCRIPTION"), read_utf8(tf)) # explicit check of "usethis.quiet" since I'm not doing the printing - if (!getOption("usethis.quiet", default = FALSE)) { + if (!is_quiet()) { desc$print() } } diff --git a/R/release.R b/R/release.R index f111c4323..14050a5d3 100644 --- a/R/release.R +++ b/R/release.R @@ -128,10 +128,9 @@ release_checklist <- function(version, on_cran) { "Wait for CRAN...", "", todo("Accepted :tada:"), - todo("`git push`"), todo("`usethis::use_github_release()`"), - todo("`usethis::use_dev_version()`"), - todo("`git push`"), + todo("`usethis::use_dev_version(push = TRUE)`"), + todo("`usethis::use_news_md()`", !has_news), todo("Finish blog post", type != "patch"), todo("Tweet", type != "patch"), todo("Add link to blog post in pkgdown news menu", type != "patch") @@ -207,28 +206,27 @@ release_type <- function(version) { } } -#' Draft a GitHub release +#' Publish a GitHub release #' #' @description -#' Creates a __draft__ GitHub release for the current package. Once you are -#' satisfied that it is correct, you will need to publish the release from -#' GitHub. The key pieces of info are which commit / SHA to tag, the associated -#' package version, and the relevant NEWS entries. -#' -#' If you use `devtools::release()` or `devtools::submit_cran()` to submit to -#' CRAN, information about the submitted state is captured in a CRAN-SUBMISSION -#' or CRAN-RELEASE file. `use_github_release()` uses this info to populate the -#' draft GitHub release and, after success, deletes the CRAN-SUBMISSION or -#' CRAN-RELEASE file. +#' Pushes the current branch (if safe) then publishes a GitHub release for the +#' latest CRAN submission. #' -#' In the absence of such a file, we must fall back to assuming the current -#' state (SHA of `HEAD`, package version, NEWS) is the submitted state. +#' If you use [devtools::submit_cran()] to submit to CRAN, information about the +#' submitted state is captured in a `CRAN-SUBMISSION` file. +#' `use_github_release()` uses this info to populate the GitHub release notes +#' and, after success, deletes the file. In the absence of such a file, we +#' assume that current state (SHA of `HEAD`, package version, NEWS) is the +#' submitted state. #' #' @param host,auth_token `r lifecycle::badge("deprecated")`: No longer #' consulted now that usethis allows the gh package to lookup a token based on #' a URL determined from the current project's GitHub remotes. +#' @param publish If `TRUE`, publishes a release. If `FALSE`, creates a draft +#' release. #' @export -use_github_release <- function(host = deprecated(), +use_github_release <- function(publish = TRUE, + host = deprecated(), auth_token = deprecated()) { check_is_package("use_github_release()") if (lifecycle::is_present(host)) { @@ -239,7 +237,7 @@ use_github_release <- function(host = deprecated(), } tr <- target_repo(github_get = TRUE, ok_configs = c("ours", "fork")) - check_can_push(tr = tr, "to draft a release") + check_can_push(tr = tr, "to create a release") dat <- get_release_data(tr) release_name <- glue("{dat$Package} {dat$Version}") @@ -248,26 +246,33 @@ use_github_release <- function(host = deprecated(), kv_line("Tag name", tag_name) kv_line("SHA", dat$SHA) + if (git_can_push()) { + git_push() + } check_github_has_SHA(SHA = dat$SHA, tr = tr) on_cran <- !is.null(cran_version()) news <- get_release_news(SHA = dat$SHA, tr = tr, on_cran = on_cran) gh <- gh_tr(tr) + + ui_cli_inform("Publishing {tag_name} release to GitHub") release <- gh( "POST /repos/{owner}/{repo}/releases", - name = release_name, tag_name = tag_name, - target_commitish = dat$SHA, body = news, draft = TRUE + name = release_name, + tag_name = tag_name, + target_commitish = dat$SHA, + body = news, + draft = !publish ) + ui_cli_inform("Release at {.url {release$html_url}}") if (!is.null(dat$file)) { - ui_done("{ui_path(dat$file)} deleted") + ui_cli_inform("Deleting {.path {dat$file}}") file_delete(dat$file) } - Sys.sleep(1) - view_url(release$html_url) - ui_todo("Publish the release via \"Edit draft\" > \"Publish release\"") + invisible() } get_release_data <- function(tr = target_repo(github_get = TRUE)) { diff --git a/R/ui.R b/R/ui.R index ab9814692..e2f8051d2 100644 --- a/R/ui.R +++ b/R/ui.R @@ -299,14 +299,17 @@ ui_bullet <- function(x, bullet = cli::symbol$bullet) { # All UI output must eventually go through ui_inform() so that it # can be quieted with 'usethis.quiet' when needed. -ui_inform <- function(..., quiet = getOption("usethis.quiet", default = FALSE)) { - if (!quiet) { +ui_inform <- function(...) { + if (!is_quiet()) { inform(paste0(...)) } - invisible() } +is_quiet <- function() { + isTRUE(getOption("usethis.quiet", default = FALSE)) +} + # Sitrep helpers --------------------------------------------------------------- hd_line <- function(name) { @@ -318,3 +321,13 @@ kv_line <- function(key, value, .envir = parent.frame()) { key <- glue(key, .envir = .envir) ui_inform(glue("{cli::symbol$bullet} {key}: {value}")) } + + +# cli wrappers ------------------------------------------------------------ + +ui_cli_inform <- function(..., .envir = parent.frame()) { + if (!is_quiet()) { + cli::cli_inform(..., .envir = .envir) + } + invisible() +} diff --git a/R/utils-git.R b/R/utils-git.R index 1726840c8..5dc623c4b 100644 --- a/R/utils-git.R +++ b/R/utils-git.R @@ -127,7 +127,7 @@ git_status <- function(untracked) { } # Commit ----------------------------------------------------------------------- -git_ask_commit <- function(message, untracked, paths = NULL) { +git_ask_commit <- function(message, untracked, push = FALSE, paths = NULL) { if (!is_interactive() || !uses_git()) { return(invisible()) } @@ -168,9 +168,22 @@ git_ask_commit <- function(message, untracked, paths = NULL) { paste0("* ", ui_paths) )) - if (ui_yeah("Is it ok to commit {if (n == 1) 'it' else 'them'}?")) { + # Only push if no remote & a single change + push <- push && git_can_push(max_local = 1) + + msg <- paste0( + "Is it ok to commit ", + if (push) "and push ", + if (n == 1) 'it' else 'them', + "?" + ) + if (ui_yeah(msg)) { git_commit(paths, message) + if (push) { + git_push() + } } + invisible() } @@ -317,6 +330,45 @@ git_branch_compare <- function(branch = git_branch(), remref = NULL) { list(local_only = out$ahead, remote_only = out$behind) } +git_can_push <- function(max_local = Inf, branch = git_branch(), remref = NULL) { + remref <- remref %||% git_branch_tracking(branch) + if (is.null(remref)) { + return(FALSE) + } + comp <- git_branch_compare(branch, remref) + comp$remote_only == 0 && comp$local_only <= max_local +} + +git_push <- function(branch = git_branch(), remref = NULL, verbose = TRUE) { + remref <- remref %||% git_branch_tracking(branch) + if (verbose) { + ui_done("Pushing local {ui_value(branch)} branch to {ui_value(remref)}.") + } + + gert::git_push( + remote = remref_remote(remref), + refspec = glue("refs/heads/{branch}:refs/heads/{remref_branch(remref)}"), + verbose = FALSE, + repo = git_repo() + ) +} + +git_push_first <- function(branch = git_branch(), remote = "origin", verbose = TRUE) { + if (verbose) { + remref <- glue("{remote}/{branch}") + ui_done(" + Pushing {ui_value(branch)} branch to GitHub and setting \\ + {ui_value(remref)} as upstream branch" + ) + } + gert::git_push( + remote = remote, + set_upstream = TRUE, + verbose = FALSE, + repo = git_repo() + ) +} + # Checks ------------------------------------------------------------------ check_current_branch <- function(is = NULL, is_not = NULL, diff --git a/R/version.R b/R/version.R index 7989bb062..d44cb2a2c 100644 --- a/R/version.R +++ b/R/version.R @@ -14,9 +14,9 @@ #' ``` #' `use_version()` increments the "Version" field in `DESCRIPTION`, adds a new -#' heading to `NEWS.md` (if it exists), and commits those changes (if package -#' uses Git). It makes the same update to a line like `PKG_version = "x.y.z";` -#' in `src/version.c` (if it exists). +#' heading to `NEWS.md` (if it exists), commits those changes (if package uses +#' Git), and optionally pushes (if safe to do so). It makes the same update to a +#' line like `PKG_version = "x.y.z";` in `src/version.c` (if it exists). #' #' `use_dev_version()` increments to a development version, e.g. from 1.0.0 to @@ -46,8 +46,10 @@ NULL #' @rdname use_version +#' @param push If `TRUE`, also attempts to push the commits to the remote +#' branch. #' @export -use_version <- function(which = NULL) { +use_version <- function(which = NULL, push = FALSE) { if (is.null(which) && !is_interactive()) { return(invisible(FALSE)) } @@ -74,8 +76,10 @@ use_version <- function(which = NULL) { git_ask_commit( glue("Increment version number to {new_ver}"), untracked = TRUE, + push = push, paths = c("DESCRIPTION", "NEWS.md", path("src", "version.c")) ) + invisible(TRUE) } diff --git a/man/use_github_release.Rd b/man/use_github_release.Rd index 559fabfb4..2fc78ae3b 100644 --- a/man/use_github_release.Rd +++ b/man/use_github_release.Rd @@ -2,27 +2,30 @@ % Please edit documentation in R/release.R \name{use_github_release} \alias{use_github_release} -\title{Draft a GitHub release} +\title{Publish a GitHub release} \usage{ -use_github_release(host = deprecated(), auth_token = deprecated()) +use_github_release( + publish = TRUE, + host = deprecated(), + auth_token = deprecated() +) } \arguments{ +\item{publish}{If \code{TRUE}, publishes a release. If \code{FALSE}, creates a draft +release.} + \item{host, auth_token}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}: No longer consulted now that usethis allows the gh package to lookup a token based on a URL determined from the current project's GitHub remotes.} } \description{ -Creates a \strong{draft} GitHub release for the current package. Once you are -satisfied that it is correct, you will need to publish the release from -GitHub. The key pieces of info are which commit / SHA to tag, the associated -package version, and the relevant NEWS entries. - -If you use \code{devtools::release()} or \code{devtools::submit_cran()} to submit to -CRAN, information about the submitted state is captured in a CRAN-SUBMISSION -or CRAN-RELEASE file. \code{use_github_release()} uses this info to populate the -draft GitHub release and, after success, deletes the CRAN-SUBMISSION or -CRAN-RELEASE file. +Pushes the current branch (if safe) then publishes a GitHub release for the +latest CRAN submission. -In the absence of such a file, we must fall back to assuming the current -state (SHA of \code{HEAD}, package version, NEWS) is the submitted state. +If you use \code{\link[devtools:submit_cran]{devtools::submit_cran()}} to submit to CRAN, information about the +submitted state is captured in a \code{CRAN-SUBMISSION} file. +\code{use_github_release()} uses this info to populate the GitHub release notes +and, after success, deletes the file. In the absence of such a file, we +assume that current state (SHA of \code{HEAD}, package version, NEWS) is the +submitted state. } diff --git a/man/use_version.Rd b/man/use_version.Rd index 5dc1552dc..b53fc344a 100644 --- a/man/use_version.Rd +++ b/man/use_version.Rd @@ -5,13 +5,16 @@ \alias{use_dev_version} \title{Increment package version} \usage{ -use_version(which = NULL) +use_version(which = NULL, push = FALSE) use_dev_version() } \arguments{ \item{which}{A string specifying which level to increment, one of: "major", "minor", "patch", "dev". If \code{NULL}, user can choose interactively.} + +\item{push}{If \code{TRUE}, also attempts to push the commits to the remote +branch.} } \description{ usethis supports semantic versioning, which is described in more detail in @@ -22,9 +25,9 @@ the \href{https://r-pkgs.org/description.html#description-version}{version secti }\if{html}{\out{}} \code{use_version()} increments the "Version" field in \code{DESCRIPTION}, adds a new -heading to \code{NEWS.md} (if it exists), and commits those changes (if package -uses Git). It makes the same update to a line like \code{PKG_version = "x.y.z";} -in \code{src/version.c} (if it exists). +heading to \code{NEWS.md} (if it exists), commits those changes (if package uses +Git), and optionally pushes (if safe to do so). It makes the same update to a +line like \code{PKG_version = "x.y.z";} in \code{src/version.c} (if it exists). \code{use_dev_version()} increments to a development version, e.g. from 1.0.0 to 1.0.0.9000. If the existing version is already a development version with diff --git a/tests/testthat/_snaps/release.md b/tests/testthat/_snaps/release.md index 1fca9a35e..03535f70f 100644 --- a/tests/testthat/_snaps/release.md +++ b/tests/testthat/_snaps/release.md @@ -32,10 +32,9 @@ Wait for CRAN... * [ ] Accepted :tada: - * [ ] `git push` * [ ] `usethis::use_github_release()` - * [ ] `usethis::use_dev_version()` - * [ ] `git push` + * [ ] `usethis::use_dev_version(push = TRUE)` + * [ ] `usethis::use_news_md()` * [ ] Finish blog post * [ ] Tweet * [ ] Add link to blog post in pkgdown news menu @@ -67,10 +66,9 @@ Wait for CRAN... * [ ] Accepted :tada: - * [ ] `git push` * [ ] `usethis::use_github_release()` - * [ ] `usethis::use_dev_version()` - * [ ] `git push` + * [ ] `usethis::use_dev_version(push = TRUE)` + * [ ] `usethis::use_news_md()` --- @@ -100,10 +98,9 @@ Wait for CRAN... * [ ] Accepted :tada: - * [ ] `git push` * [ ] `usethis::use_github_release()` - * [ ] `usethis::use_dev_version()` - * [ ] `git push` + * [ ] `usethis::use_dev_version(push = TRUE)` + * [ ] `usethis::use_news_md()` * [ ] Finish blog post * [ ] Tweet * [ ] Add link to blog post in pkgdown news menu @@ -156,10 +153,9 @@ Wait for CRAN... * [ ] Accepted :tada: - * [ ] `git push` * [ ] `usethis::use_github_release()` - * [ ] `usethis::use_dev_version()` - * [ ] `git push` + * [ ] `usethis::use_dev_version(push = TRUE)` + * [ ] `usethis::use_news_md()` * [ ] Finish blog post * [ ] Tweet * [ ] Add link to blog post in pkgdown news menu