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