#1254 GitHub diagnostics

Merged Jennifer (Jenny) Bryan jennybc

@@ -415,106 +415,148 @@
Loading
415 415
#' git_sitrep()
416 416
#' }
417 417
git_sitrep <- function() {
418 -
  # git global ----------------------------------------------------------------
418 +
  ui_silence(try(proj_get(), silent = TRUE))
419 +
420 +
  # git (global / user) --------------------------------------------------------
419 421
  hd_line("Git config (global)")
420 422
  kv_line("Name", git_cfg_get("user.name", "global"))
421 423
  kv_line("Email", git_cfg_get("user.email", "global"))
422 -
  kv_line("Vaccinated", git_vaccinated())
423 -
424 -
  # git project ---------------------------------------------------------------
425 -
  if (proj_active() && uses_git()) {
426 -
    local_user <- list(
427 -
      user.name = git_cfg_get("user.name", "local"),
428 -
      user.email = git_cfg_get("user.email", "local")
429 -
    )
430 -
    if (!is.null(local_user$user.name) || !is.null(local_user$user.name)) {
431 -
      hd_line("Git config (project)")
432 -
      kv_line("Name", git_cfg_get("user.name"))
433 -
      kv_line("Email", git_cfg_get("user.email"))
434 -
    }
424 +
  vaccinated <- git_vaccinated()
425 +
  kv_line("Vaccinated", vaccinated)
426 +
  if (!vaccinated) {
427 +
    ui_info("See {ui_code('?git_vaccinate')} to learn more about this")
435 428
  }
436 -
437 -
  # usethis + gert + credentials -----------------------------------------------
438 -
  hd_line("usethis + gert")
429 +
  # TODO: Revisit when I harden the HTTPS default
439 430
  kv_line("Default usethis protocol", getOption("usethis.protocol"))
440 -
  kv_line("gert supports HTTPS", gert::libgit2_config()$https)
441 -
  kv_line("gert supports SSH", gert::libgit2_config()$ssh)
442 -
  # TODO: forward more info from the credentials package when available
443 -
  # https://github.com/r-lib/credentials/issues/6
444 431
445 -
  # github user ---------------------------------------------------------------
432 +
  # github (global / user) -----------------------------------------------------
446 433
  hd_line("GitHub")
447 -
  auth_token <- gh::gh_token()
448 -
  have_token <- auth_token != ""
449 -
  if (have_token) {
450 -
    kv_line("Personal access token", "<discovered>")
451 -
    tryCatch(
452 -
      {
453 -
        who <- gh::gh_whoami(auth_token)
454 -
        kv_line("User", who$login)
455 -
        kv_line("Name", who$name)
456 -
      },
457 -
      http_error_401 = function(e) ui_oops("Token is invalid."),
458 -
      error = function(e) ui_oops("Can't validate token. Is the network reachable?")
459 -
    )
460 -
    tryCatch(
461 -
      {
462 -
        emails <- unlist(gh::gh("/user/emails", .token = auth_token))
463 -
        emails <- emails[names(emails) == "email"]
464 -
        kv_line("Email(s)", emails)
465 -
      },
466 -
      http_error_404 = function(e) kv_line("Email(s)", "<unknown>"),
467 -
      error = function(e) ui_oops("Can't validate token. Is the network reachable?")
468 -
    )
469 -
  } else {
470 -
    kv_line("Personal access token", NULL)
471 -
  }
434 +
  default_gh_host <- get_hosturl(default_api_url())
435 +
  kv_line("Default GitHub host", default_gh_host)
436 +
  pat_found <- pat_sitrep(host = default_gh_host)
437 +
438 +
  # git and github for active project ------------------------------------------
439 +
  hd_line("Git repo for current project")
472 440
473 -
  # repo overview -------------------------------------------------------------
474 -
  hd_line("Repo")
475 -
  ui_silence(try(proj_get(), silent = TRUE))
476 441
  if (!proj_active()) {
477 -
    ui_info("No active usethis project.")
442 +
    ui_info("No active usethis project")
478 443
    return(invisible())
479 444
  }
480 -
445 +
  kv_line("Active usethis project", proj_get())
481 446
  if (!uses_git()) {
482 -
    ui_info("Active project is not a Git repo.")
447 +
    ui_info("Active project is not a Git repo")
483 448
    return(invisible())
484 449
  }
485 450
486 -
  kv_line("Path", git_repo())
451 +
  # local git config -----------------------------------------------------------
452 +
  if (proj_active() && uses_git()) {
453 +
    local_user <- list(
454 +
      user.name = git_cfg_get("user.name", "local"),
455 +
      user.email = git_cfg_get("user.email", "local")
456 +
    )
457 +
    if (!is.null(local_user$user.name) || !is.null(local_user$user.name)) {
458 +
      ui_info("This repo has a locally configured user")
459 +
      kv_line("Name", local_user$user.name)
460 +
      kv_line("Email", local_user$user.email)
461 +
    }
462 +
  }
463 +
464 +
  # default branch -------------------------------------------------------------
465 +
  kv_line("Default branch", git_branch_default())
466 +
467 +
  # current branch -------------------------------------------------------------
487 468
  branch <- tryCatch(git_branch(), error = function(e) NULL)
488 469
  tracking_branch <- if (is.null(branch)) NA_character_ else git_branch_tracking()
489 -
  ## TODO: rework when ui_*() functions make it possible to do better
470 +
  # TODO: can't really express with kv_line() helper
490 471
  branch <- if (is.null(branch)) "<unset>" else branch
491 472
  tracking_branch <- if (is.na(tracking_branch)) "<unset>" else tracking_branch
492 -
  ui_inform(
493 -
    "* ", "Local branch -> remote tracking branch: ",
494 -
    ui_value(branch), " -> ", ui_value(tracking_branch)
495 -
  )
473 +
  # vertical alignment would make this nicer, but probably not worth it
474 +
  ui_bullet(glue("
475 +
    Current local branch -> remote tracking branch:
476 +
    {ui_value(branch)} -> {ui_value(tracking_branch)}"), cli::symbol$star)
496 477
497 -
  # PR outlook -------------------------------------------------------------
498 -
  hd_line("GitHub pull request readiness")
499 -
  # TODO: need to surface host more here
478 +
  # GitHub remote config -------------------------------------------------------
500 479
  cfg <- github_remote_config()
501 -
  if (cfg$type == "no_github") {
502 -
    ui_info("
503 -
      This repo has neither {ui_value('origin')} nor {ui_value('upstream')} \\
504 -
      remote on GitHub.com.")
505 -
    return(invisible())
480 +
  repo_host <- cfg$host_url
481 +
  if (!is.na(repo_host) && repo_host != default_gh_host) {
482 +
    kv_line("Non-default GitHub host", repo_host)
483 +
    pat_found <- pat_sitrep(host = repo_host)
506 484
  }
507 -
  # TODO: make this more attractive
508 -
  print(cfg)
485 +
486 +
  hd_line("GitHub remote configuration")
487 +
  purrr::walk(format(cfg), ~ ui_bullet(.x, cli::symbol$star))
488 +
  invisible()
489 +
}
490 +
491 +
pat_sitrep <- function(host = "https://github.com") {
492 +
  pat <- gh::gh_token(api_url = host)
493 +
  have_pat <- pat != ""
494 +
  if (!have_pat) {
495 +
    kv_line("Personal access token for {ui_value(host)}", NULL)
496 +
    ui_oops("
497 +
      Call {ui_code('gh_token_help()')} for help configuring a token")
498 +
    return(FALSE)
499 +
  }
500 +
501 +
  kv_line("Personal access token for {ui_value(host)}", "<discovered>")
502 +
  tryCatch(
503 +
    {
504 +
      who <- gh::gh_whoami(.token = pat, .api_url = host)
505 +
      kv_line("GitHub user", who$login)
506 +
      scopes <- who$scopes
507 +
      kv_line("Token scopes", who$scopes)
508 +
      # https://docs.github.com/en/free-pro-team@latest/developers/apps/scopes-for-oauth-apps
509 +
      # why these checks?
510 +
      # previous defaults for create_github_token(): repo, gist, user:email
511 +
      # more recently: repo, user, gist
512 +
      # (gist scope is a very weak recommendation)
513 +
      scopes <- strsplit(scopes, ", ")[[1]]
514 +
      if (length(scopes) == 0 ||
515 +
          !any(grepl("^repo$", scopes)) ||
516 +
          !any(grepl("^user(:email)?$", scopes))) {
517 +
        ui_oops("
518 +
            Token may be mis-scoped: {ui_value('repo')} and \\
519 +
            {ui_value('user')} are highly recommended scopes
520 +
            If you are troubleshooting, consider this")
521 +
      }
522 +
    },
523 +
    http_error_401 = function(e) ui_oops("Token is invalid"),
524 +
    error = function(e) {
525 +
      ui_oops("
526 +
        Can't get user profile for this token. Is the network reachable?")
527 +
    }
528 +
  )
529 +
  tryCatch(
530 +
    {
531 +
      emails <- gh::gh("/user/emails", .token = pat, .api_url = host)
532 +
      addresses <- map_chr(
533 +
        emails,
534 +
        ~ if (.x$primary) glue_data(.x, "{email} (primary)") else .x[["email"]]
535 +
      )
536 +
      kv_line("Email(s)", addresses)
537 +
      de_facto_email <- git_cfg_get("user.email", "de_facto")
538 +
      if (!any(grepl(de_facto_email, addresses))) {
539 +
        ui_oops("
540 +
          User's Git email ({ui_value(de_facto_email)}) doesn't appear to be \\
541 +
          registered with GitHub")
542 +
      }
543 +
    },
544 +
    error = function(e) {
545 +
      ui_oops("
546 +
        Can't retrieve registered email address(es)
547 +
        If you are troubleshooting, check GitHub host, token, and token scopes")
548 +
    }
549 +
  )
550 +
  TRUE
509 551
}
510 552
511 553
# Vaccination -------------------------------------------------------------
512 554
513 555
#' Vaccinate your global gitignore file
514 556
#'
515 -
#' Adds `.DS_Store`, `.Rproj.user`, `.Rdata`, and `.Rhistory` to your global
516 -
#' (a.k.a. user-level) `.gitignore`. This is good practice as it decreases the
517 -
#' chance that you will accidentally leak credentials to GitHub.
557 +
#' Adds `.DS_Store`, `.Rproj.user`, `.Rdata`, `.Rhistory`, and `.httr-oauth` to
558 +
#' your global (a.k.a. user-level) `.gitignore`. This is good practice as it
559 +
#' decreases the chance that you will accidentally leak credentials to GitHub.
518 560
#'
519 561
#' @export
520 562
git_vaccinate <- function() {
@@ -536,5 +578,6 @@
Loading
536 578
  ".Rproj.user",
537 579
  ".Rhistory",
538 580
  ".Rdata",
581 +
  ".httr-oauth",
539 582
  ".DS_Store"
540 583
)

@@ -289,7 +289,12 @@
Loading
289 289
290 290
# rlang::inform() wrappers -----------------------------------------------------
291 291
292 -
ui_bullet <- function(x, bullet) {
292 +
indent <- function(x, first = "  ", indent = first) {
293 +
  x <- gsub("\n", paste0("\n", indent), x)
294 +
  paste0(first, x)
295 +
}
296 +
297 +
ui_bullet <- function(x, bullet = cli::symbol$bullet) {
293 298
  bullet <- paste0(bullet, " ")
294 299
  x <- indent(x, bullet, "  ")
295 300
  ui_inform(x)
@@ -311,7 +316,8 @@
Loading
311 316
  ui_inform(crayon::bold(name))
312 317
}
313 318
314 -
kv_line <- function(key, value) {
319 +
kv_line <- function(key, value, .envir = parent.frame()) {
315 320
  value <- if (is.null(value)) ui_unset() else ui_value(value)
316 -
  ui_inform("* ", key, ": ", value)
321 +
  key <- glue(key, .envir = .envir)
322 +
  ui_inform(glue("{cli::symbol$star} {key}: {value}"))
317 323
}

@@ -61,11 +61,6 @@
Loading
61 61
  )
62 62
}
63 63
64 -
indent <- function(x, first = "  ", indent = first) {
65 -
  x <- gsub("\n", paste0("\n", indent), x)
66 -
  paste0(first, x)
67 -
}
68 -
69 64
isFALSE = function(x) {
70 65
  identical(x, FALSE)
71 66
}

@@ -1,4 +1,4 @@
Loading
1 -
# repo_spec --> owner, repo
1 +
# OWNER/REPO --> OWNER, REPO
2 2
parse_repo_spec <- function(repo_spec) {
3 3
  repo_split <- strsplit(repo_spec, "/")[[1]]
4 4
  if (length(repo_split) != 2) {
@@ -10,7 +10,7 @@
Loading
10 10
spec_owner <- function(repo_spec) parse_repo_spec(repo_spec)$owner
11 11
spec_repo <- function(repo_spec) parse_repo_spec(repo_spec)$repo
12 12
13 -
# owner, repo --> repo_spec
13 +
# OWNER, REPO --> OWNER/REPO
14 14
make_spec <- function(owner = NA, repo = NA) {
15 15
  no_spec <- is.na(owner) | is.na(repo)
16 16
  as.character(ifelse(no_spec, NA, glue("{owner}/{repo}")))
@@ -167,13 +167,12 @@
Loading
167 167
    oops <- which(!grl$github_got)
168 168
    oops_remotes <- grl$remote[oops]
169 169
    oops_hosts <- unique(grl$host[oops])
170 -
    # TODO: update when there's a place to send people for troubleshooting
171 170
    ui_stop("
172 171
      Unable to get GitHub info for these remotes: {ui_value(oops_remotes)}
173 -
      Are we offline?
172 +
      Are we offline? Is GitHub down?
174 173
      Otherwise, you probably need to configure a personal access token (PAT) \\
175 174
      for {ui_value(oops_hosts)}
176 -
      See {ui_code('?create_github_token')} for advice")
175 +
      See {ui_code('?gh_token_help')} for advice")
177 176
  }
178 177
179 178
  grl$is_fork <- map_lgl(repo_info, "fork", .default = NA)
@@ -280,6 +279,7 @@
Loading
280 279
  structure(
281 280
    list(
282 281
      type = NA_character_,
282 +
      host_url = NA_character_,
283 283
      pr_ready = FALSE,
284 284
      desc = "Unexpected remote configuration.",
285 285
      origin   = c(name = "origin",   is_configured = FALSE, ptype),
@@ -317,6 +317,7 @@
Loading
317 317
        Internal error: Know GitHub permissions for some remotes, but not all")
318 318
    }
319 319
  }
320 +
  cfg$host_url <- unique(grl$host_url)
320 321
  github_got <- any(grl$github_got)
321 322
  perm_known <- any(grl$perm_known)
322 323
@@ -510,14 +511,15 @@
Loading
510 511
511 512
format_fields <- function(cfg) {
512 513
  list(
513 -
    type = glue("type = {ui_value(cfg$type)}"),
514 +
    type = glue("Type = {ui_value(cfg$type)}"),
515 +
    host_url = glue("Host = {ui_value(cfg$host_url)}"),
514 516
    pr_ready = glue("Config supports a pull request = {ui_value(cfg$pr_ready)}"),
515 517
    origin = format_remote(cfg$origin),
516 518
    upstream = format_remote(cfg$upstream),
517 519
    desc = if (is.na(cfg$desc)) {
518 -
      glue("desc = {ui_unset('no description')}")
520 +
      glue("Desc = {ui_unset('no description')}")
519 521
    } else {
520 -
      glue("desc = {cfg$desc}")
522 +
      glue("Desc = {cfg$desc}")
521 523
    }
522 524
  )
523 525
}
@@ -616,6 +618,15 @@
Loading
616 618
}
617 619
618 620
# github remote configurations -------------------------------------------------
621 +
# use for configs
622 +
read_more <- glue("
623 +
  Read more about the GitHub remote configurations that usethis supports at:
624 +
  {ui_value('https://happygitwithr.com/common-remote-setups.html')}")
625 +
626 +
read_more_maybe <- glue("
627 +
  Read more about what this GitHub remote configurations means at:
628 +
  {ui_value('https://happygitwithr.com/common-remote-setups.html')}")
629 +
619 630
cfg_no_github <- function(cfg) {
620 631
  utils::modifyList(
621 632
    cfg,
@@ -624,7 +635,9 @@
Loading
624 635
      pr_ready = FALSE,
625 636
      desc = glue("
626 637
        Neither {ui_value('origin')} nor {ui_value('upstream')} is a GitHub \\
627 -
        repo.")
638 +
        repo.
639 +
640 +
        {read_more}")
628 641
    )
629 642
  )
630 643
}
@@ -635,7 +648,11 @@
Loading
635 648
    list(
636 649
      type = "ours",
637 650
      pr_ready = TRUE,
638 -
      desc = NA)
651 +
      desc = glue("
652 +
        {ui_value('origin')} is both the source and primary repo.
653 +
654 +
        {read_more}")
655 +
    )
639 656
  )
640 657
}
641 658
@@ -650,7 +667,9 @@
Loading
650 667
        The only configured GitHub remote is {ui_value(configured)}, which
651 668
        you cannot push to.
652 669
        If your goal is to make a pull request, you must fork-and-clone.
653 -
        {ui_code('usethis::create_from_github()')} can do this.")
670 +
        {ui_code('usethis::create_from_github()')} can do this.
671 +
672 +
        {read_more}")
654 673
    )
655 674
  )
656 675
}
@@ -670,7 +689,12 @@
Loading
670 689
      pr_ready = NA,
671 690
      desc = glue("
672 691
        {ui_value(configured)} is a GitHub repo and {ui_value(not_configured)} \\
673 -
        is either not configured or is not a GitHub repo.")
692 +
        is either not configured or is not a GitHub repo.
693 +
694 +
        We may be offline or you may need to configure a GitHub personal access
695 +
        token. {ui_code('gh_token_help()')} can help with that.
696 +
697 +
        {read_more_maybe}")
674 698
    )
675 699
  )
676 700
}
@@ -681,10 +705,11 @@
Loading
681 705
    list(
682 706
      type = "fork",
683 707
      pr_ready = TRUE,
684 -
      # TODO: say whether user can push to parent / upstream?
685 708
      desc = glue("
686 709
        {ui_value('origin')} is a fork of {ui_value(cfg$upstream$repo_spec)}, \\
687 -
        which is configured as the {ui_value('upstream')} remote.")
710 +
        which is configured as the {ui_value('upstream')} remote.
711 +
712 +
        {read_more}")
688 713
    )
689 714
  )
690 715
}
@@ -699,8 +724,12 @@
Loading
699 724
        Both {ui_value('origin')} and {ui_value('upstream')} appear to be \\
700 725
        GitHub repos. However, we can't confirm their relationship to each \\
701 726
        other (e.g., fork and fork parent) or your permissions (e.g. push \\
702 -
        access). We may be offline or you may need to configure a GitHub \\
703 -
        personal access token.")
727 +
        access).
728 +
729 +
        We may be offline or you may need to configure a GitHub personal access
730 +
        token. {ui_code('gh_token_help()')} can help with that.
731 +
732 +
        {read_more_maybe}")
704 733
    )
705 734
  )
706 735
}
@@ -712,7 +741,9 @@
Loading
712 741
      type = "fork_cannot_push_origin",
713 742
      pr_ready = FALSE,
714 743
      desc = glue("
715 -
        The {ui_value('origin')} remote is a fork, but you can't push to it.")
744 +
        The {ui_value('origin')} remote is a fork, but you can't push to it.
745 +
746 +
        {read_more}")
716 747
    )
717 748
  )
718 749
}
@@ -725,7 +756,9 @@
Loading
725 756
      pr_ready = FALSE,
726 757
      desc = glue("
727 758
        The {ui_value('origin')} GitHub remote is a fork, but its parent is \\
728 -
        not configured as the {ui_value('upstream')} remote.")
759 +
        not configured as the {ui_value('upstream')} remote.
760 +
761 +
        {read_more}")
729 762
    )
730 763
  )
731 764
}
@@ -739,7 +772,9 @@
Loading
739 772
      desc = glue("
740 773
        Both {ui_value('origin')} and {ui_value('upstream')} are GitHub \\
741 774
        remotes, but {ui_value('origin')} is not a fork and, in particular, \\
742 -
        is not a fork of {ui_value('upstream')}.")
775 +
        is not a fork of {ui_value('upstream')}.
776 +
777 +
        {read_more}")
743 778
    )
744 779
  )
745 780
}

@@ -55,7 +55,7 @@
Loading
55 55
#' \dontrun{
56 56
#' create_github_token()
57 57
#' }
58 -
create_github_token <- function(scopes = c("repo", "gist", "user:email"),
58 +
create_github_token <- function(scopes = c("repo", "user", "gist"),
59 59
                                description = "R:GITHUB_PAT",
60 60
                                host = NULL) {
61 61
  scopes <- glue_collapse(scopes, ",")

Everything is accounted for!

No changes detected that need to be reviewed.
What changes does Codecov check for?
Lines, not adjusted in diff, that have changed coverage data.
Files that introduced coverage data that had none before.
Files that have missing coverage data that once were tracked.
Files Coverage
R -0.39% 51.34%
Project Totals (64 files) 51.34%
Loading