1# OWNER/REPO --> OWNER, REPO 2parse_repo_spec <- function(repo_spec) { 3 repo_split <- strsplit(repo_spec, "/")[[1]] 4 if (length(repo_split) != 2) { 5 ui_stop("{ui_code('repo_spec')} must be of form {ui_value('owner/repo')}.") 6 } 7 list(owner = repo_split[[1]], repo = repo_split[[2]]) 8} 9 10spec_owner <- function(repo_spec) parse_repo_spec(repo_spec)$owner 11spec_repo <- function(repo_spec) parse_repo_spec(repo_spec)$repo 12 13# OWNER, REPO --> OWNER/REPO 14make_spec <- function(owner = NA, repo = NA) { 15 no_spec <- is.na(owner) | is.na(repo) 16 as.character(ifelse(no_spec, NA, glue("{owner}/{repo}"))) 17} 18 19# named vector or list of GitHub URLs --> data frame of URL parts 20# more general than the name suggests 21# definitely designed for GitHub URLs but not overtly GitHub-specific 22# https://stackoverflow.com/questions/2514859/regular-expression-for-git-repository 23# https://git-scm.com/docs/git-clone#_git_urls 24# https://stackoverflow.com/questions/27745/getting-parts-of-a-url-regex 25github_remote_regex <- paste0( 26 "^", 27 "(?<prefix>git|ssh|http[s]?)", 28 "[:/@]+", 29 "(?<host>[^/:]+)", 30 "[/:]", 31 "(?<repo_owner>[^/]+)", 32 "/", 33 "(?<repo_name>[^/#]+)", 34 "(?<fragment>.*)", 35 "$" 36) 37 38parse_github_remotes <- function(x) { 39 # https://github.com/r-lib/usethis 40 # --> https, github.com, rlib, usethis 41 # https://github.com/r-lib/usethis.git 42 # --> https, github.com, rlib, usethis 43 # https://github.com/r-lib/usethis#readme 44 # --> https, github.com, rlib, usethis 45 # https://github.com/r-lib/usethis/issues/1169 46 # --> https, github.com, rlib, usethis 47 # https://github.acme.com/r-lib/devtools.git 48 # --> https, github.acme.com, rlib, usethis 49 # git@github.com:r-lib/usethis.git 50 # --> ssh, github.com, rlib, usethis 51 dat <- re_match(x, github_remote_regex) 52 dat$url <- dat$.text 53 # as.character() necessary for edge case of length-0 input 54 dat$protocol <- as.character(ifelse(dat$prefix == "https", "https", "ssh")) 55 dat$name <- if (rlang::is_named(x)) { 56 names(x) 57 } else { 58 rep_len(NA_character_, length.out = nrow(dat)) 59 } 60 dat$repo_name <- sub("[.]git$", "", dat$repo_name) 61 dat[c("name", "url", "host", "repo_owner", "repo_name", "protocol")] 62} 63 64parse_repo_url <- function(x) { 65 stopifnot(is_string(x)) 66 dat <- re_match(x, github_remote_regex) 67 if (is.na(dat$.match)) { 68 list(repo_spec = x, host = NULL) 69 } else { 70 dat <- parse_github_remotes(x) 71 # TODO: generalize here for GHE hosts that don't include 'github' 72 if (!grepl("github", dat$host)) { 73 ui_stop("URL doesn't seem to be associated with GitHub: {ui_value(x)}") 74 } 75 list( 76 repo_spec = make_spec(owner = dat$repo_owner, repo = dat$repo_name), 77 host = glue("https://{dat$host}") 78 ) 79 } 80} 81 82github_url_from_git_remotes <- function() { 83 tr <- tryCatch(target_repo(github_get = NA), error = function(e) NULL) 84 if (is.null(tr)) { 85 return() 86 } 87 parsed <- parse_github_remotes(tr$url) 88 glue_data_chr(parsed, "https://{host}/{repo_owner}/{repo_name}") 89} 90 91#' Gather LOCAL data on GitHub-associated remotes 92#' 93#' Creates a data frame where each row represents a GitHub-associated remote. 94#' The data frame is initialized via `gert::git_remote_list()`, possibly 95#' filtered for specific remote names. The remote URLs are parsed into parts, 96#' like `host` and `repo_owner`. This is filtered again for rows where the 97#' `host` appears to be a GitHub deployment (currently a crude search for 98#' "github"). Some of these parts are recombined or embellished to get new 99#' columns (`host_url`, `api_url`, `repo_spec`). All operations are entirely 100#' mechanical and local. 101#' 102#' @param these Intersect the list of remotes with `these` remote names. To keep 103#' all remotes, use `these = NULL` or `these = character()`. 104#' @param x Data frame with character columns `name` and `url`. Exposed as an 105#' argument for internal reasons. It's so we can call the functions that 106#' marshal info about GitHub remotes with 0-row input to obtain a properly 107#' typed template without needing a Git repo or calling GitHub. We just want 108#' to get a data frame with zero rows, but with the column names and types 109#' implicit in our logic. 110#' @keywords internal 111#' @noRd 112github_remote_list <- function(these = c("origin", "upstream"), x = NULL) { 113 x <- x %||% gert::git_remote_list(repo = git_repo()) 114 stopifnot(is.null(these) || is.character(these)) 115 stopifnot(is.data.frame(x), is.character(x$name), is.character(x$url)) 116 if (length(these) > 0) { 117 x <- x[x$name %in% these, ] 118 } 119 120 parsed <- parse_github_remotes(set_names(x$url, x$name)) 121 # TODO: generalize here for GHE hosts that don't include 'github' 122 is_github <- grepl("github", parsed$host) 123 parsed <- parsed[is_github, ] 124 125 parsed$remote <- parsed$name 126 parsed$host_url <- glue_chr("https://{parsed$host}") 127 parsed$api_url <- map_chr(parsed$host_url, get_apiurl) 128 parsed$repo_spec <- make_spec(parsed$repo_owner, parsed$repo_name) 129 130 parsed[c( 131 "remote", 132 "url", "host_url", "api_url", "host", "protocol", 133 "repo_owner", "repo_name", "repo_spec" 134 )] 135} 136 137#' Gather LOCAL and (maybe) REMOTE data on GitHub-associated remotes 138#' 139#' Creates a data frame where each row represents a GitHub-associated remote, 140#' starting with the output of `github_remote_list()` (local data). This 141#' function's job is to (maybe) add information we can only get from the GitHub 142#' API. If `github_get = FALSE`, we don't even attempt to call the API. 143#' Otherwise, we try and will succeed if gh discovers a suitable token. The 144#' resulting data, even if the API data is absent, is massaged into a data 145#' frame. 146#' 147#' @inheritParams github_remote_list 148#' @param github_get Whether to attempt to get repo info from the GitHub API. We 149#' try for `NA` (the default) and `TRUE`. If we aren't successful, we proceed 150#' anyway for `NA` but error for `TRUE`. When `FALSE`, no attempt is made to 151#' call the API. 152#' @keywords internal 153#' @noRd 154github_remotes <- function(these = c("origin", "upstream"), 155 github_get = NA, 156 x = NULL) { 157 grl <- github_remote_list(these = these, x = x) 158 get_gh_repo <- function(repo_owner, repo_name, 159 api_url = "https://api.github.com") { 160 if (isFALSE(github_get)) { 161 f <- function(...) list() 162 } else { 163 f <- purrr::possibly(gh::gh, otherwise = list()) 164 } 165 f( 166 "GET /repos/{owner}/{repo}", 167 owner = repo_owner, repo = repo_name, .api_url = api_url 168 ) 169 } 170 repo_info <- purrr::pmap( 171 grl[c("repo_owner", "repo_name", "api_url")], 172 get_gh_repo 173 ) 174 # NOTE: these can be two separate matters: 175 # 1. Did we call the GitHub API? Means we know `is_fork` and the parent repo. 176 # 2. If so, did we call it with auth? Means we know if we can push. 177 grl$github_got <- map_lgl(repo_info, ~ length(.x) > 0) 178 if (isTRUE(github_get) && any(!grl$github_got)) { 179 oops <- which(!grl$github_got) 180 oops_remotes <- grl$remote[oops] 181 oops_hosts <- unique(grl$host[oops]) 182 ui_stop(" 183 Unable to get GitHub info for these remotes: {ui_value(oops_remotes)} 184 Are we offline? Is GitHub down? 185 Otherwise, you probably need to configure a personal access token (PAT) \\ 186 for {ui_value(oops_hosts)} 187 See {ui_code('?gh_token_help')} for advice") 188 } 189 190 grl$is_fork <- map_lgl(repo_info, "fork", .default = NA) 191 # `permissions` is an example of data that is not present if the request 192 # did not include a PAT 193 grl$can_push <- map_lgl(repo_info, c("permissions", "push"), .default = NA) 194 grl$perm_known <- !is.na(grl$can_push) 195 grl$parent_repo_owner <- 196 map_chr(repo_info, c("parent", "owner", "login"), .default = NA) 197 grl$parent_repo_name <- 198 map_chr(repo_info, c("parent", "name"), .default = NA) 199 grl$parent_repo_spec <- make_spec(grl$parent_repo_owner, grl$parent_repo_name) 200 201 parent_info <- purrr::pmap( 202 set_names( 203 grl[c("parent_repo_owner", "parent_repo_name", "api_url")], 204 ~ sub("parent_", "", .x) 205 ), 206 get_gh_repo 207 ) 208 grl$can_push_to_parent <- 209 map_lgl(parent_info, c("permissions", "push"), .default = NA) 210 211 grl 212} 213 214#' Classify the GitHub remote configuration 215#' 216#' @description 217#' Classify the active project's GitHub remote situation, so diagnostic and 218#' other downstream functions can decide whether to proceed / abort / complain & 219#' offer to fix. 220#' We only consider the remotes where: 221#' * Name is `origin` or `upstream` and the remote URL "looks like github" 222#' (github.com or a GHE deployment) 223#' 224#' We have to call the GitHub API to fully characterize the GitHub remote 225#' situation. That's the only way to learn if the user can push to a remote, 226#' whether a remote is a fork, and which repo is the parent of a fork. 227#' `github_get` controls whether we make these API calls. 228#' 229#' Some functions can get by with the information that's available locally, i.e. 230#' we can use simple logic to decide whether to target `origin` or `upstream` or 231#' present the user with a choice. We can set `github_get = FALSE` in this case. 232#' Other functions, like the `pr_*()` functions, are more demanding and we'll 233#' always determine the config with `github_get = TRUE`. 234#' 235#' Most usethis functions should call the higher-level functions `target_repo()` 236#' or `target_repo_spec()`. 237#' 238#' Only functions that really need full access to the GitHub remote config 239#' should call this directly. Ways to work with a config: 240#' * `cfg <- github_remote_config(github_get = )` 241#' * `check_for_bad_config(cfg)` errors for obviously bad configs (by default) 242#' or you can specify the configs considered to be bad 243#' * Emit a custom message then call `stop_bad_github_remote_config()` directly 244#' * If the config is suboptimal-but-supported, use 245#' `ui_github_remote_config_wat()` to educate the user and give them a chance 246#' to back out. 247#' 248#' Fields in an instance of `github_remote_config`: 249#' * `type`: explained below 250#' * `pr_ready`: Logical. Do the `pr_*()` functions support it? 251#' * `desc`: A description used in messages and menus. 252#' * `origin`: Information about the `origin` GitHub remote. 253#' * `upstream`: Information about the `upstream` GitHub remote. 254#' 255#' Possible GitHub remote configurations, the common cases: 256#' * no_github: No `origin`, no `upstream`. 257#' * ours: `origin` exists, is not a fork, and we can push to it. Owner of 258#' `origin` could be current user, another user, or an org. No `upstream`. 259#' - Less common variant: `upstream` exists, `origin` does not, and we can 260#' push to `upstream`. The fork-ness of `upstream` is not consulted. 261#' * fork: `origin` exists and we can push to it. `origin` is a fork of the repo 262#' configured as `upstream`. We may or may not be able to push to `upstream`. 263#' * theirs: Exactly one of `origin` and `upstream` exist and we can't push to 264#' it. The fork-ness of this remote repo is not consulted. 265#' 266#' Possible GitHub remote configurations, the peculiar ones: 267#' * fork_upstream_is_not_origin_parent: `origin` exists, it's a fork, but its 268#' parent repo is not configured as `upstream`. Either there's no `upstream` 269#' or `upstream` exists but it's not the parent of `origin`. 270#' * fork_cannot_push_origin: `origin` is a fork and its parent is configured 271#' as `upstream`. But we can't push to `origin`. 272#' * upstream_but_origin_is_not_fork: `origin` and `upstream` both exist, but 273#' `origin` is not a fork of anything and, specifically, it's not a fork of 274#' `upstream`. 275#' 276#' Remote configuration "guesses" we apply when `github_get = FALSE` or when 277#' we make unauthorized requests (no PAT found) and therefore have no info on 278#' permissions 279#' * maybe_ours_or_theirs: Exactly one of `origin` and `upstream` exists. 280#' * maybe_fork: Both `origin` and `upstream` exist. 281#' 282#' @inheritParams github_remotes 283#' @keywords internal 284#' @noRd 285new_github_remote_config <- function() { 286 ptype <- github_remotes( 287 x = data.frame(name = character(), url = character(), stringsAsFactors = FALSE) 288 ) 289 # 0-row df --> a well-named list of properly typed NAs 290 ptype <- map(ptype, ~ c(NA, .x)) 291 structure( 292 list( 293 type = NA_character_, 294 host_url = NA_character_, 295 pr_ready = FALSE, 296 desc = "Unexpected remote configuration.", 297 origin = c(name = "origin", is_configured = FALSE, ptype), 298 upstream = c(name = "upstream", is_configured = FALSE, ptype) 299 ), 300 class = "github_remote_config" 301 ) 302} 303 304github_remote_config <- function(github_get = NA) { 305 cfg <- new_github_remote_config() 306 grl <- github_remotes(github_get = github_get) 307 308 if (nrow(grl) == 0) { 309 return(cfg_no_github(cfg)) 310 } 311 312 cfg$origin$is_configured <- "origin" %in% grl$remote 313 cfg$upstream$is_configured <- "upstream" %in% grl$remote 314 315 single_remote <- xor(cfg$origin$is_configured, cfg$upstream$is_configured) 316 317 if (!single_remote) { 318 if (length(unique(grl$host)) != 1) { 319 ui_stop(" 320 Internal error: Multiple GitHub hosts 321 {ui_value(grl$host)}") 322 } 323 if (length(unique(grl$github_got)) != 1) { 324 ui_stop(" 325 Internal error: Got GitHub API info for some remotes, but not all") 326 } 327 if (length(unique(grl$perm_known)) != 1) { 328 ui_stop(" 329 Internal error: Know GitHub permissions for some remotes, but not all") 330 } 331 } 332 cfg$host_url <- unique(grl$host_url) 333 github_got <- any(grl$github_got) 334 perm_known <- any(grl$perm_known) 335 336 if (cfg$origin$is_configured) { 337 cfg$origin <- 338 utils::modifyList(cfg$origin, grl[grl$remote == "origin",]) 339 } 340 341 if (cfg$upstream$is_configured) { 342 cfg$upstream <- 343 utils::modifyList(cfg$upstream, grl[grl$remote == "upstream",]) 344 } 345 346 if (github_got && !single_remote) { 347 cfg$origin$parent_is_upstream <- 348 identical(cfg$origin$parent_repo_spec, cfg$upstream$repo_spec) 349 } 350 351 if (!github_got || !perm_known) { 352 if (single_remote) { 353 return(cfg_maybe_ours_or_theirs(cfg)) 354 } else { 355 return(cfg_maybe_fork(cfg)) 356 } 357 } 358 # `github_got` must be TRUE 359 # `perm_known` must be TRUE 360 361 # origin only 362 if (single_remote && cfg$origin$is_configured) { 363 if (cfg$origin$is_fork) { 364 if (cfg$origin$can_push) { 365 return(cfg_fork_upstream_is_not_origin_parent(cfg)) 366 } else { 367 return(cfg_theirs(cfg)) 368 } 369 } else { 370 if (cfg$origin$can_push) { 371 return(cfg_ours(cfg)) 372 } else { 373 return(cfg_theirs(cfg)) 374 } 375 } 376 } 377 378 # upstream only 379 if (single_remote && cfg$upstream$is_configured) { 380 if (cfg$upstream$can_push) { 381 return(cfg_ours(cfg)) 382 } else { 383 return(cfg_theirs(cfg)) 384 } 385 } 386 387 # origin and upstream 388 if (cfg$origin$is_fork) { 389 if (cfg$origin$parent_is_upstream) { 390 if (cfg$origin$can_push) { 391 return(cfg_fork(cfg)) 392 } else { 393 return(cfg_fork_cannot_push_origin(cfg)) 394 } 395 } else { 396 return(cfg_fork_upstream_is_not_origin_parent(cfg)) 397 } 398 } else { 399 return(cfg_upstream_but_origin_is_not_fork(cfg)) 400 } 401} 402 403#' Select a target (GitHub) repo 404#' 405#' @description 406 407#' Returns information about ONE GitHub repository. Used when we need to 408#' designate which repo we will, e.g., open an issue on or activate a CI service 409#' for. This information might be used in a GitHub API request or to form URLs. 410#' 411 412#' Examples: 413#' * Badge URLs 414#' * URLs where you can activate a CI service 415#' * URLs for DESCRIPTION fields such as URL and BugReports 416 417#' `target_repo()` passes `github_get` along to `github_remote_config()`. If 418#' `github_get = TRUE`, `target_repo()` will error for configs other than 419#' `"ours"` or `"fork"`. `target_repo()` always errors for bad configs. If 420#' `github_get = NA` or `FALSE`, the "maybe" configs are tolerated. 421#' 422#' `target_repo_spec()` is a less capable function for when you just need an 423#' `OWNER/REPO` spec. Currently, it does not set or offer control over 424#' `github_get`, although I've considered explicitly setting `github_get = 425#' FALSE` or adding this argument, defaulting to `FALSE`. 426#' 427 428#' @inheritParams github_remotes 429 430#' @param cfg An optional GitHub remote configuration. Used to get the target 431#' repo when the function had some need for the full config. 432#' @param role We use "source" to mean the principal repo where a project's 433#' development happens. We use "primary" to mean the principal repo this 434#' particular user interacts with or has the greatest power over. They can be 435#' the same or different. Examples: 436#' * For a personal project you own, "source" and "primary" are the same. 437#' Presumably the `origin` remote. 438#' * For a collaboratively developed project, an outside contributor must create 439#' a fork in order to make a PR. For such a person, their fork is "primary" 440#' (presumably `origin`) and the original repo that they forked is "source" 441#' (presumably `upstream`). 442#' This is *almost* consistent with terminology used by the GitHub API. A fork 443#' has a "source repo" and a "parent repo", which are usually the same. They 444#' only differ when working with a fork of a repo that is itself a fork. In this 445#' rare case, the parent is the immediate fork parent and the source is the 446#' ur-parent, i.e. the root of this particular tree. The source repo is not a 447#' fork. 448#' @param ask In some configurations, if `ask = TRUE` and we're in an 449#' interactive session, user gets a choice between `origin` and `upstream`. 450#' @keywords internal 451#' @noRd 452target_repo <- function(cfg = NULL, 453 github_get = NA, 454 role = c("source", "primary"), 455 ask = is_interactive()) { 456 cfg <- cfg %||% github_remote_config(github_get = github_get) 457 stopifnot(inherits(cfg, "github_remote_config")) 458 role <- match.arg(role) 459 460 check_for_bad_config(cfg) 461 462 if (isTRUE(github_get)) { 463 check_ours_or_fork(cfg) 464 } 465 466 # upstream only 467 if (cfg$upstream$is_configured && !cfg$origin$is_configured) { 468 return(cfg$upstream) 469 } 470 471 # origin only 472 if (cfg$origin$is_configured && !cfg$upstream$is_configured) { 473 return(cfg$origin) 474 } 475 476 if (!ask || !is_interactive()) { 477 return(switch( 478 role, 479 source = cfg$upstream, 480 primary = cfg$origin 481 )) 482 } 483 484 choices <- c( 485 origin = glue("{cfg$origin$repo_spec} = {ui_value('origin')}"), 486 upstream = glue("{cfg$upstream$repo_spec} = {ui_value('upstream')}") 487 ) 488 title <- glue("Which repo should we target?") 489 choice <- utils::menu(choices, graphics = FALSE, title = title) 490 cfg[[names(choices)[choice]]] 491} 492 493target_repo_spec <- function(role = c("source", "primary"), 494 ask = is_interactive()) { 495 tr <- target_repo(role = match.arg(role), ask = ask) 496 tr$repo_spec 497} 498 499# formatting github remote configurations for humans --------------------------- 500format_remote <- function(remote) { 501 effective_spec <- function(remote) { 502 if (remote$is_configured) { 503 ui_value(remote$repo_spec) 504 } else { 505 ui_unset("not configured") 506 } 507 } 508 push_clause <- function(remote) { 509 if (!remote$is_configured || is.na(remote$can_push)) { 510 return() 511 } 512 if (remote$can_push) " (can push)" else " (can not push)" 513 } 514 out <- c( 515 glue("{remote$name} = {effective_spec(remote)}"), 516 push_clause(remote), 517 if (isTRUE(remote$is_fork)) { 518 glue(" = fork of {ui_value(remote$parent_repo_spec)}") 519 } 520 ) 521 glue_collapse(out) 522} 523 524format_fields <- function(cfg) { 525 list( 526 type = glue("Type = {ui_value(cfg$type)}"), 527 host_url = glue("Host = {ui_value(cfg$host_url)}"), 528 pr_ready = glue("Config supports a pull request = {ui_value(cfg$pr_ready)}"), 529 origin = format_remote(cfg$origin), 530 upstream = format_remote(cfg$upstream), 531 desc = if (is.na(cfg$desc)) { 532 glue("Desc = {ui_unset('no description')}") 533 } else { 534 glue("Desc = {cfg$desc}") 535 } 536 ) 537} 538 539#' @export 540format.github_remote_config <- function(x, ...) { 541 glue::as_glue(format_fields(x)) 542} 543 544#' @export 545print.github_remote_config <- function(x, ...) { 546 cat(format(x, ...), sep = "\n") 547 invisible(x) 548} 549 550# refines output of format_fields() to create input better suited to 551# ui_github_remote_config_wat() and stop_bad_github_remote_config() 552github_remote_config_wat <- function(cfg, context = c("menu", "abort")) { 553 context <- match.arg(context) 554 adjective <- switch(context, menu = "Unexpected", abort = "Unsupported") 555 out <- format_fields(cfg) 556 out$pr_ready <- NULL 557 out$type <- glue("{adjective} GitHub remote configuration: {ui_value(cfg$type)}") 558 out$desc <- if (is.na(cfg$desc)) NULL else cfg$desc 559 out 560} 561 562# returns TRUE if user selects "no" --> exit the calling function 563# return FALSE if user select "yes" --> keep going, they've been warned 564ui_github_remote_config_wat <- function(cfg) { 565 ui_nope( 566 github_remote_config_wat(cfg, context = "menu"), 567 yes = "Yes, I want to proceed. I know what I'm doing.", 568 no = "No, I want to stop and straighten out my GitHub remotes first.", 569 shuffle = FALSE 570 ) 571} 572 573stop_bad_github_remote_config <- function(cfg) { 574 abort( 575 message = unname(github_remote_config_wat(cfg, context = "abort")), 576 class = c("usethis_error_bad_github_remote_config", "usethis_error"), 577 cfg = cfg 578 ) 579} 580 581stop_maybe_github_remote_config <- function(cfg) { 582 msg <- github_remote_config_wat(cfg) 583 msg$type <- glue(" 584 Pull request functions can't work with GitHub remote configuration: \\ 585 {ui_value(cfg$type)} 586 The most likely problem is that we aren't discovering your GitHub \\ 587 personal access token 588 Call {ui_code('gh_token_help()')} for help") 589 abort( 590 message = unname(msg), 591 class = c("usethis_error_invalid_pr_config", "usethis_error"), 592 cfg = cfg 593 ) 594} 595 596check_for_bad_config <- function(cfg, 597 bad_configs = c( 598 "no_github", 599 "fork_upstream_is_not_origin_parent", 600 "fork_cannot_push_origin", 601 "upstream_but_origin_is_not_fork" 602 )) { 603 if (cfg$type %in% bad_configs) { 604 stop_bad_github_remote_config(cfg) 605 } 606 invisible() 607} 608 609check_for_maybe_config <- function(cfg, 610 maybe_configs = c( 611 "maybe_ours_or_theirs", 612 "maybe_fork" 613 )) { 614 if (cfg$type %in% maybe_configs) { 615 stop_maybe_github_remote_config(cfg) 616 } 617 invisible() 618} 619 620check_ours_or_fork <- function(cfg = NULL) { 621 cfg <- cfg %||% github_remote_config(github_get = TRUE) 622 stopifnot(inherits(cfg, "github_remote_config")) 623 if (cfg$type %in% c("ours", "fork")) { 624 return(invisible(cfg)) 625 } 626 check_for_bad_config(cfg) 627 check_for_maybe_config(cfg) 628 ui_stop(" 629 Internal error: Unexpected GitHub remote configuration: {ui_value(cfg$type)}") 630} 631 632# github remote configurations ------------------------------------------------- 633# use for configs 634read_more <- glue(" 635 Read more about the GitHub remote configurations that usethis supports at: 636 {ui_value('https://happygitwithr.com/common-remote-setups.html')}") 637 638read_more_maybe <- glue(" 639 Read more about what this GitHub remote configurations means at: 640 {ui_value('https://happygitwithr.com/common-remote-setups.html')}") 641 642cfg_no_github <- function(cfg) { 643 utils::modifyList( 644 cfg, 645 list( 646 type = "no_github", 647 pr_ready = FALSE, 648 desc = glue(" 649 Neither {ui_value('origin')} nor {ui_value('upstream')} is a GitHub \\ 650 repo. 651 652 {read_more}") 653 ) 654 ) 655} 656 657cfg_ours <- function(cfg) { 658 utils::modifyList( 659 cfg, 660 list( 661 type = "ours", 662 pr_ready = TRUE, 663 desc = glue(" 664 {ui_value('origin')} is both the source and primary repo. 665 666 {read_more}") 667 ) 668 ) 669} 670 671cfg_theirs <- function(cfg) { 672 configured <- if (cfg$origin$is_configured) "origin" else "upstream" 673 utils::modifyList( 674 cfg, 675 list( 676 type = "theirs", 677 pr_ready = FALSE, 678 desc = glue(" 679 The only configured GitHub remote is {ui_value(configured)}, which 680 you cannot push to. 681 If your goal is to make a pull request, you must fork-and-clone. 682 {ui_code('usethis::create_from_github()')} can do this. 683 684 {read_more}") 685 ) 686 ) 687} 688 689cfg_maybe_ours_or_theirs <- function(cfg) { 690 if (cfg$origin$is_configured) { 691 configured <- "origin" 692 not_configured <- "upstream" 693 } else { 694 configured <- "upstream" 695 not_configured <- "origin" 696 } 697 utils::modifyList( 698 cfg, 699 list( 700 type = "maybe_ours_or_theirs", 701 pr_ready = NA, 702 desc = glue(" 703 {ui_value(configured)} is a GitHub repo and {ui_value(not_configured)} \\ 704 is either not configured or is not a GitHub repo. 705 706 We may be offline or you may need to configure a GitHub personal access 707 token. {ui_code('gh_token_help()')} can help with that. 708 709 {read_more_maybe}") 710 ) 711 ) 712} 713 714cfg_fork <- function(cfg) { 715 utils::modifyList( 716 cfg, 717 list( 718 type = "fork", 719 pr_ready = TRUE, 720 desc = glue(" 721 {ui_value('origin')} is a fork of {ui_value(cfg$upstream$repo_spec)}, \\ 722 which is configured as the {ui_value('upstream')} remote. 723 724 {read_more}") 725 ) 726 ) 727} 728 729cfg_maybe_fork <- function(cfg) { 730 utils::modifyList( 731 cfg, 732 list( 733 type = "maybe_fork", 734 pr_ready = NA, 735 desc = glue(" 736 Both {ui_value('origin')} and {ui_value('upstream')} appear to be \\ 737 GitHub repos. However, we can't confirm their relationship to each \\ 738 other (e.g., fork and fork parent) or your permissions (e.g. push \\ 739 access). 740 741 We may be offline or you may need to configure a GitHub personal access 742 token. {ui_code('gh_token_help()')} can help with that. 743 744 {read_more_maybe}") 745 ) 746 ) 747} 748 749cfg_fork_cannot_push_origin <- function(cfg) { 750 utils::modifyList( 751 cfg, 752 list( 753 type = "fork_cannot_push_origin", 754 pr_ready = FALSE, 755 desc = glue(" 756 The {ui_value('origin')} remote is a fork, but you can't push to it. 757 758 {read_more}") 759 ) 760 ) 761} 762 763cfg_fork_upstream_is_not_origin_parent <- function(cfg) { 764 utils::modifyList( 765 cfg, 766 list( 767 type = "fork_upstream_is_not_origin_parent", 768 pr_ready = FALSE, 769 desc = glue(" 770 The {ui_value('origin')} GitHub remote is a fork, but its parent is \\ 771 not configured as the {ui_value('upstream')} remote. 772 773 {read_more}") 774 ) 775 ) 776} 777 778cfg_upstream_but_origin_is_not_fork <- function(cfg) { 779 utils::modifyList( 780 cfg, 781 list( 782 type = "upstream_but_origin_is_not_fork", 783 pr_ready = FALSE, 784 desc = glue(" 785 Both {ui_value('origin')} and {ui_value('upstream')} are GitHub \\ 786 remotes, but {ui_value('origin')} is not a fork and, in particular, \\ 787 is not a fork of {ui_value('upstream')}. 788 789 {read_more}") 790 ) 791 ) 792} 793