From 1207532debf9881ef1c2957e12b38b61a2edfd3c Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 5 Dec 2025 17:11:17 -0600 Subject: [PATCH] Experimental code to delete unneeded forks It's hard to imagine that we'd want to export this to our users (due to the possibility of deleting repos that might actually be needed) but maybe it's useful internally? --- NAMESPACE | 2 + R/github-fork.R | 107 ++++++++++++++++++++++++++++++++++++++++++ man/gh_fork_status.Rd | 28 +++++++++++ 3 files changed, 137 insertions(+) create mode 100644 R/github-fork.R create mode 100644 man/gh_fork_status.Rd diff --git a/NAMESPACE b/NAMESPACE index 91a15d28e..3ae662a90 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -32,6 +32,8 @@ export(edit_r_profile) export(edit_rstudio_prefs) export(edit_rstudio_snippets) export(edit_template) +export(gh_fork_cleanup) +export(gh_fork_status) export(gh_token_help) export(git_default_branch) export(git_default_branch_configure) diff --git a/R/github-fork.R b/R/github-fork.R new file mode 100644 index 000000000..efeb2e121 --- /dev/null +++ b/R/github-fork.R @@ -0,0 +1,107 @@ +#' Find forks with no open pull requests +#' +#' @description +#' `gh_fork_status()` reports all forks that the current user has, along with +#' the number of open PRs in each. +#' +#' `gh_fork_cleanup()` deletes all forks with zero open PRs. +#' +#' @export +#' @examples +#' \dontrun{ +#' # Find and optionally delete forks with no open PRs +#' gh_fork_status() +#' gh_fork_cleanup() +#' } +gh_fork_status <- function() { + me <- gh::gh_whoami()$login + # Don't want to use /user/repos, since it returns all repos you have + # access to, even in other orgs + repos <- gh::gh( + "GET /users/{user}/repos/forks", + user = me, + visibility = "public", + per_page = 100, + .limit = Inf + ) + is_fork <- map_lgl(repos, \(repo) repo$fork) + + # The /repos endpoint doesn't give us any details about the parent + # so we now retrieve that + repo_names <- map_chr(repos, \(repo) repo$name) + fork_names <- repo_names[is_fork] + forks <- map( + fork_names, + \(name) gh::gh("/repos/{owner}/{repo}", owner = me, repo = name), + .progress = "Retrieving fork metadata" + ) + fork_owners <- map_chr(forks, \(fork) fork$parent$owner$login) + + # Now we can see if there are any outstanding PRs + prs <- purrr::map2( + fork_owners, + fork_names, + gh_repo_open_prs, + pr_creator = me, + .progress = "Looking for open PRs" + ) + + data.frame( + name = fork_owners, + repo = fork_names, + open_prs = lengths(prs) + ) +} + +#' @export +#' @rdname gh_fork_status +#' @param api_key An API key with `delete_repo` scope. We recommend making +#' this a very shortlived token. +gh_fork_cleanup <- function(api_key) { + forks <- gh_fork_status() + if (nrow(forks) == 0) { + return(invisible()) + } + + deleteable <- forks[forks$open_prs == 0, ] + if (nrow(deleteable) == 0) { + return(invisible()) + } + + cli::cli_inform("Found {nrow(deleteable)} fork{?s} with no open PRs") + if (!ui_yeah("Delete them?")) { + return(invisible()) + } + + me <- gh::gh_whoami()$login + for (i in seq_len(nrow(deleteable))) { + repo <- deleteable$name[[i]] + cli::cli_inform("Deleting {me}/{repo}") + + gh::gh( + "DELETE /repos/{owner}/{repo}", + owner = me, + repo = repo, + .token = api_key + ) + } + + invisible() +} + +# Helpers --------------------------------------------------------------------- + +gh_repo_open_prs <- function(fork_owner, fork_name, pr_creator) { + # Docs advertise `head` param to filter by user/branch, but that + # didn't work for me. + prs <- gh::gh( + "GET /repos/{owner}/{repo}/pulls", + owner = fork_owner, + repo = fork_name, + state = "open", + # This will miss forks older forks but should be low risk + per_page = 50 + ) + creator <- map_chr(prs, \(pr) pr$user$login) + prs[creator == pr_creator] +} diff --git a/man/gh_fork_status.Rd b/man/gh_fork_status.Rd new file mode 100644 index 000000000..415dd91cb --- /dev/null +++ b/man/gh_fork_status.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/github-fork.R +\name{gh_fork_status} +\alias{gh_fork_status} +\alias{gh_fork_cleanup} +\title{Find forks with no open pull requests} +\usage{ +gh_fork_status() + +gh_fork_cleanup(api_key) +} +\arguments{ +\item{api_key}{An API key with \code{delete_repo} scope. We recommend making +this a very shortlived token.} +} +\description{ +\code{gh_fork_status()} reports all forks that the current user has, along with +the number of open PRs in each. + +\code{gh_fork_cleanup()} deletes all forks with zero open PRs. +} +\examples{ +\dontrun{ +# Find and optionally delete forks with no open PRs +gh_fork_status() +gh_fork_cleanup() +} +}