Nothing
#' Get the tags of GitHub releases of a repository
#'
#' Use the GitHub API ([github_api()]) to obtain the tags of the
#' releases.
#' @param repo The repository name of the form `user/repo`, e.g.,
#' `"yihui/xfun"`.
#' @param tag A tag as a character string. If provided, it will be returned if
#' the tag exists. If `tag = "latest"`, the tag of the latest release is
#' returned.
#' @param pattern A regular expression to match the tags.
#' @param use_jsonlite Whether to use \pkg{jsonlite} to parse the releases info.
#' @export
#' @return A character vector of (GIT) tags.
#' @examplesIf interactive()
#' xfun::github_releases('yihui/litedown')
#' xfun::github_releases('gohugoio/hugo', 'latest')
github_releases = function(
repo, tag = '', pattern = 'v[0-9.]+', use_jsonlite = loadable('jsonlite')
) {
if (tag != '') return(github_releases2(repo, tag, pattern))
i = 1; v = character()
repeat {
res = github_api(
sprintf('/repos/%s/tags', repo), NULL, list(per_page = 100, page = i),
raw = !use_jsonlite
)
v2 = unlist(if (use_jsonlite) {
lapply(res, `[[`, 'name')
} else {
reg_match('\\{"name":"([^"]+)",', res)
})
if (length(v2) == 0) break
v = c(v, v2)
if (length(v2) < 100) break # not enough items for the next page
i = i + 1
}
grep(sprintf('^%s$', pattern), unique(v), value = TRUE)
}
# extract the matched elements in the n-th pair of () in the regex
reg_match = function(p, x, n = 1, ...) {
# TODO: gregexec was added in R 4.1.0; remove this workaround when we don't
# need to support R < 4.1.0
v = 'gregexec' %in% ls(baseenv())
m = (if (v) base::gregexec else base::gregexpr)(p, x, ...)
lapply(regmatches(x, m), function(x) {
if (v) x[n + 1, ] else gsub(p, paste0('\\', n), x)
})
}
# the fallback method to retrieve release tags (read HTML source)
github_releases2 = function(repo, tag = '', pattern = '[^"&]+') {
read = function() suppressWarnings(
read_utf8(sprintf('https://github.com/%s/releases/%s', repo, tag))
)
h = if (tag == '') read() else tryCatch(read(), error = function(e) '')
r = sprintf('^.*?%s/releases/tag/(%s)".*', repo, pattern)
v = unique(grep_sub(r, '\\1', h))
# if a tag is specified, use the 1st value because other tags may be mentioned
# in the current tag page
if (tag == '') v else head(v, 1)
}
#' @details `github_api()` is a wrapper function based on
#' `rest_api_raw()` to obtain data from the GitHub API:
#' <https://docs.github.com/en/rest>. You can provide a personal access
#' token (PAT) via the `token` argument, or via one of the environment
#' variables \var{GITHUB_PAT}, \var{GITHUB_TOKEN}, \var{GH_TOKEN}. A PAT
#' allows for a much higher rate limit in API calls. Without a token, you can
#' only make 60 calls in an hour.
#' @param raw Whether to return the raw response or parse the response with
#' \pkg{jsonlite}.
#' @rdname rest_api
#' @export
github_api = function(
endpoint, token = '', params = list(), headers = NULL, raw = !loadable('jsonlite')
) {
token = c(token, github_token())
token = if (length(token <- token[token != ''])) token[1] else ''
names(token) = 'token'
error = TRUE
on.exit(if (error) github_token(NA, token))
res = rest_api_raw('https://api.github.com', endpoint, token, params, headers)
error = FALSE
if (raw) res else jsonlite::fromJSON(res, FALSE)
}
# the environment variables to check for a GitHub personal access token
github_envs = c('GITHUB_PAT', 'GITHUB_TOKEN', 'GH_TOKEN')
github_token = function(error = FALSE, token = unname(Sys.getenv(github_envs))) {
if (length(token <- token[token != ''])) return(token[1])
msg = c(
'You may need to save a GitHub personal access token in one of the ',
'environment variables: ', paste(github_envs, collapse = ', ')
)
if (is.na(error)) message(msg) else if (error) stop(msg)
''
}
git = function(...) {
if (Sys.which('git') == '') stop('git is not available')
# R's HOME var is different from the system's HOME on Windows:
# https://github.com/yihui/crandalf/issues/24
if (is_windows()) {
env = set_envvar(c(HOME = Sys.getenv('USERPROFILE')))
on.exit(set_envvar(env), add = TRUE)
}
system3('git', ...)
}
git_co = function(args = NULL, ...) {
git(c('checkout', args), ...)
}
git_test_branch = function() {
if (length(d <- git(c('diff', '--name-only'), stdout = TRUE))) stop(
'The current branch has changes not stated for commit:\n',
paste(d, collapse = '\n')
)
}
# pull from the current branch if it is a git repository and has a current branch
git_pull_current <- function() {
is_repo = git(c('rev-parse', '--is-inside-work-tree'), stdout = FALSE, stderr = FALSE)
if (is_repo == 0) {
branch = git(c('branch', '--show-current'), stdout = TRUE)
if (length(branch) == 1 && branch != '') {
if (length(git('diff', stdout = TRUE))) {
git('stash'); on.exit(git(c('stash', 'pop')), add = TRUE)
}
git(c('pull', 'origin', branch))
}
}
}
gh = function(...) {
if (Sys.which('gh') == '') stop('GitHub CLI not found: https://cli.github.com')
system3('gh', ...)
}
gh_run = function(..., repo = NA) {
gh(c(if (!is.na(repo)) c('-R', repo), 'run', ...), stdout = TRUE)
}
# Detect the sprintf template for NEWS.md headers from existing content.
# Falls back to '# CHANGES IN %s VERSION %s' when format cannot be inferred.
news_tpl = function(news, pkg, ver) {
h1 = grep('^# ', news, value = TRUE)[1]
tpl = sub(sprintf('\\b%s\\b', ver), '%s', sub(sprintf('\\b%s\\b', pkg), '%s', h1))
if (grepl('%s.*%s', tpl)) tpl else '# CHANGES IN %s VERSION %s'
}
#' Perform post-CRAN-release routine tasks
#'
#' After a CRAN release is accepted, commit and tag the release, bump the
#' package version to the next patch dev version, prepend a new header to
#' \file{NEWS.md}, push to the remote repository, and create a GitHub release
#' with the news items of this version.
#'
#' The version number `X.Y` or `X.Y.0` is extracted from the `Version` field in
#' \file{DESCRIPTION}. For example, if the current version is `0.58` (or
#' `0.58.0`), the repo will be tagged as `v0.58` (or `v0.58.0`). The patch
#' version is then bumped to `0.58.1`, and a new header for the next minor
#' version `0.59` (or `0.59.0`) is prepended to \file{NEWS.md}.
#' @export
post_release = function() {
desc = read.dcf('DESCRIPTION')
pkg = desc[, 'Package']
ver = desc[, 'Version']
vers = release_versions(ver)
ver_next = vers$next_ver
ver_patch = vers$patch
# Step 1: commit all current changes (only if Version was modified)
ver_in_head = system3(
'git', c('show', 'HEAD:DESCRIPTION'), stdout = TRUE, stderr = FALSE
)
ver_in_head = trimws(grep_sub('^Version:\\s+(.*)', '\\1', ver_in_head))
if (!identical(ver_in_head, ver)) {
message('Committing CRAN release v', ver, ' ...')
git(c('commit', '-a', '-m', sprintf('CRAN release v%s', ver)))
}
# Step 2: tag
message('Tagging v', ver, ' ...')
git(c('tag', sprintf('v%s', ver)))
# Step 3: bump patch version and prepend NEWS.md
process_file('DESCRIPTION', function(x) {
ver_line = grep('^Version:\\s+', x)
x[ver_line] = sprintf('Version: %s', ver_patch)
x
})
news = trimws(read_utf8('NEWS.md'))
tpl = news_tpl(news, pkg, ver)
news_hdr = function(v) sprintf(tpl, pkg, v)
process_file('NEWS.md', function(x) c(paste0(news_hdr(ver_next), '\n\n'), x))
hdr = news_hdr(ver)
i = which(hdr == news)
if (length(i) == 0) stop('Cannot find the header "', hdr, '" in NEWS.md')
message('Committing the start of the next version ...')
git(c('commit', '-a', '-m', 'start the next version'))
# Step 4: push with tags
message('Pushing to remote ...')
git(c('push', '--tags', 'origin', 'HEAD'))
# Step 5: create GitHub release
github_token(TRUE)
j = grep('^# ', news)
j = j[j > i]
end = if (length(j) > 0) j[1] - 1 else length(news)
items = strip_blank(news[(i + 1):end])
f = tempfile()
writeLines(items, f)
on.exit(unlink(f), add = TRUE)
gh(c(
'release', 'create', sprintf('v%s', ver),
'--title', sprintf('%s %s', pkg, ver), '--notes-file', f)
)
invisible()
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.