R/pull-requests.R

Defines functions browse_pull_request view_pull_request view_pull_requests update_pull_request create_pull_request

Documented in browse_pull_request create_pull_request update_pull_request view_pull_request view_pull_requests

#  FUNCTION: create_pull_request -----------------------------------------------
#
#' Create an pull request in a repository
#'
#' This function creates a new pull request for the specified repository in
#' GitHub. It can also be used to assign the pull request to a user, request
#' reviewers and add labels or a milestone.
#'
#' For more details see the GitHub API documentation:
#'
#' - <https://docs.github.com/en/rest/reference/pulls#create-a-pull-request>
#' - <https://docs.github.com/en/rest/reference/issues#update-an-issue>
#' - <https://docs.github.com/en/rest/reference/pulls#request-reviewers-for-a-pull-request>
#'
#' @param title (string) The title of the pull request.
#' @param repo (string) The repository specified in the format: `owner/repo`.
#' @param head (string) The name of the branch where your changes are
#'   implemented. For cross-repository pull requests, prefix `head` with an
#'   owner, e.g. `"username:branch"`.
#' @param base (string) The name of the branch you want the changes pulled into.
#'   This should be an existing branch in the specified repository.
#' @param body (string, optional) The contents of the pull request.
#' @param assignees (character, optional) Logins for Users to assign to this
#'   pull request. NOTE: Only users with push access can set assignees for new
#'   pull requests.
#' @param reviewers (character, optional) Logins for Users to review this pull
#'   request. NOTE: Only users with push access can set reviewers for new pull
#'   requests.
#' @param labels (character, optional) Labels to associate with this pull
#'   request. NOTE: Only users with push access can set labels for new pull
#'   requests.
#' @param milestone (character or integer, optional) The title or number of the
#'   milestone to associate this pull request with. NOTE: Only users with push
#'   access can set the milestone for new pull requests.
#' @param ... Parameters passed to [gh_request()].
#'
#' @return `create_pull_request()` returns a list of the pull request's
#'   properties.
#'
#' **Pull Request Properties:**
#'
#' - **number**: The number assigned to the pull request.
#' - **title**: The title of the pull request.
#' - **body**: The body contents of the pull request.
#' - **head_sha**: The SHA of the commit to merge in.
#' - **head_ref**: The reference, or branch, to merge in.
#' - **head_repo**: The repository containing the branch to merge in.
#' - **base_sha**: The SHA of the commit to merge onto.
#' - **base_ref**: The reference, or branch, to merge onto.
#' - **merge_sha**: The SHA of the merge commit, if the merge has been
#'   completed.
#' - **assignees**: The users assigned to the pull request.
#' - **reviewers**: The users reviewing the pull request.
#' - **labels**: The labels attached to the pull request.
#' - **milestone**: The milestone assigned to the pull request.
#' - **state**: The state of the pull request - either `"open"` or `"closed"`.
#' - **repository**: The repository the pull request is in.
#' - **html_url**: The URL of the pull request's web page in GitHub.
#' - **diff_url**: The URL of the pull request's diff web page in GitHub.
#' - **creator**: The creator's login.
#' - **created_at**: When the pull request was created.
#' - **updated_at**: When the pull request was last updated.
#' - **mergeable**: Whether the pull request can be merged.
#' - **rebaseable**: Whether the pull request can be rebased.
#' - **merged**: Whether the pull request has been merged
#' - **merged_by**: Who merged the pull request.
#' - **merged_at**: When the pull request was merged.
#' - **closed_at**: When the pull request was closed.
#'
#' @examples
#' \dontrun{
#'
#'   create_pull_request(
#'     title = "test pull request",
#'     repo  = "ChadGoymer/githapi",
#'     head  = "test-pulls",
#'     base  = "main",
#'     body  = "This is a pull request to test create_pull_request()"
#'   )
#'
#'   create_pull_request(
#'     title     = "test assigned pull request",
#'     repo      = "ChadGoymer/githapi",
#'     head      = "test-pulls-2",
#'     base      = "main",
#'     body      = "This is a pull request to test create_pull_request()",
#'     assignees = "ChadGoymer",
#'     reviewers = c("BobSmith", "JaneJones"),
#'     labels    = "enhancement",
#'     milestone = "Release-1.0"
#'   )
#'
#' }
#'
#' @export
#'
create_pull_request <- function(
  title,
  repo,
  head,
  base,
  body,
  assignees,
  reviewers,
  labels,
  milestone,
  ...
) {
  assert_character(title, n = 1)
  assert_repo(repo)
  assert_character(head, n = 1)
  assert_character(base, n = 1)

  payload <- list(title = title, head = head, base = base)

  if (!is_missing_or_null(body)) {
    assert_character(body, n = 1)
    payload$body <- body
  }

  info("Creating pull request '", title, "' for repository '", repo, "'")
  pull_lst <- gh_url("repos", repo, "pulls") %>%
    gh_request("POST", payload = payload, ...)

  payload <- list()

  if (!is_missing_or_null(assignees)) {
    assert_character(assignees)
    payload$assignees <- as.list(assignees)
  }

  if (!is_missing_or_null(labels)) {
    assert_character(labels)
    payload$labels <- as.list(labels)
  }

  if (!is_missing_or_null(milestone)) {
    if (is_character(milestone, n = 1)) {
      milestone <- view_milestone(milestone, repo = repo, ...)$number
    }
    assert_natural(milestone, n = 1)
    payload$milestone <- milestone
  }

  issue_lst <- list(milestone = list(title = NA_character_))
  if (length(payload) > 0) {
    info(
      "Adding assignees, labels or milestone to pull request '",
      pull_lst$title, "'"
    )
    issue_lst <- gh_url("repos", repo, "issues", pull_lst$number) %>%
      gh_request("PATCH", payload = payload, ...)
  }

  reviewers_lst <- list()
  if (!is_missing_or_null(reviewers)) {
    assert_character(reviewers)
    info("Adding reviewers to pull request '", pull_lst$title, "'")
    reviewers_lst <- gh_url(
      "repos", repo, "pulls", pull_lst$number, "requested_reviewers"
    ) %>%
      gh_request("POST", payload = list(reviewers = as.list(reviewers)), ...)
  }

  info("Transforming results", level = 4)
  pull_gh <- select_properties(pull_lst, properties$pull_request) %>%
    modify_list(
      assignees = map_chr(issue_lst$assignees, "login"),
      reviewers = map_chr(reviewers_lst$requested_reviewers, "login"),
      labels    = map_chr(issue_lst$labels, "name"),
      .before   = "milestone"
    ) %>%
    modify_list(repository = repo, milestone = issue_lst$milestone$title)

  info("Done", level = 7)
  pull_gh
}


#  FUNCTION: update_pull_request -----------------------------------------------
#
#' Update a pull request in a repository
#'
#' This function updates a pull request for the specified repository in GitHub.
#' It can be used to change the title or body, or used to close the pull
#' request. It can also be used to assign the pull request to a user, request
#' reviewers and replace labels or a milestone.
#'
#' For more details see the GitHub API documentation:
#'
#' - <https://docs.github.com/en/rest/reference/pulls#update-a-pull-request>
#' - <https://docs.github.com/en/rest/reference/issues#update-an-issue>
#' - <https://docs.github.com/en/rest/reference/pulls#request-reviewers-for-a-pull-request>
#'
#' @param pull_request (string or character) The number or title of the pull
#'   request.
#' @param repo (string) The repository specified in the format: `owner/repo`.
#' @param title (string, optional) The new title of the pull request.
#' @param body (string, optional) The contents of the pull request.
#' @param assignees (character, optional) Logins for Users to assign to this
#'   pull request. NOTE: Only users with push access can set assignees for new
#'   pull requests.
#' @param reviewers (character, optional) Logins for Users to review this pull
#'   request. NOTE: Only users with push access can set reviewers for new pull
#'   requests.
#' @param labels (character, optional) Labels to associate with this pull
#'   request. NOTE: Only users with push access can set labels for new pull
#'   requests.
#' @param milestone (character or integer, optional) The title or number of the
#'   milestone to associate this pull request with. NOTE: Only users with push
#'   access can set the milestone for new pull requests.
#' @param state (string, optional) The state of the pull request. Either
#'   `"open"` or `"closed"`.
#' @param base (string, optional) The name of the branch you want the changes
#'   pulled into. This should be an existing branch on the current repository.
#' @param ... Parameters passed to [gh_request()].
#'
#' @return `update_pull_request()` returns a list of the pull request's
#'   properties.
#'
#' **Pull Request Properties:**
#'
#' - **number**: The number assigned to the pull request.
#' - **title**: The title of the pull request.
#' - **body**: The body contents of the pull request.
#' - **head_sha**: The SHA of the commit to merge in.
#' - **head_ref**: The reference, or branch, to merge in.
#' - **head_repo**: The repository containing the branch to merge in.
#' - **base_sha**: The SHA of the commit to merge onto.
#' - **base_ref**: The reference, or branch, to merge onto.
#' - **merge_sha**: The SHA of the merge commit, if the merge has been
#'   completed.
#' - **assignees**: The users assigned to the pull request.
#' - **reviewers**: The users reviewing the pull request.
#' - **labels**: The labels attached to the pull request.
#' - **milestone**: The milestone assigned to the pull request.
#' - **state**: The state of the pull request - either `"open"` or `"closed"`.
#' - **repository**: The repository the pull request is in.
#' - **html_url**: The URL of the pull request's web page in GitHub.
#' - **diff_url**: The URL of the pull request's diff web page in GitHub.
#' - **creator**: The creator's login.
#' - **created_at**: When the pull request was created.
#' - **updated_at**: When the pull request was last updated.
#' - **mergeable**: Whether the pull request can be merged.
#' - **rebaseable**: Whether the pull request can be rebased.
#' - **merged**: Whether the pull request has been merged
#' - **merged_by**: Who merged the pull request.
#' - **merged_at**: When the pull request was merged.
#' - **closed_at**: When the pull request was closed.
#'
#' @examples
#' \dontrun{
#'
#'   # Update a pull request's properties
#'   update_pull_request(
#'     pull_request = "test pull request",
#'     repo         = "ChadGoymer/githapi",
#'     title        = "test updated pull request",
#'     body         = "This is an updated pull request"
#'   )
#'
#'   # Close a pull request
#'   update_pull_request(
#'     pull_request = "test updated pull request",
#'     repo         = "ChadGoymer/githapi",
#'     state        = "closed"
#'   )
#'
#' }
#'
#' @export
#'
update_pull_request <- function(
  pull_request,
  repo,
  title,
  body,
  assignees,
  reviewers,
  labels,
  milestone,
  base,
  state,
  ...
) {
  assert_repo(repo)

  payload <- list()

  if (!is_missing_or_null(title)) {
    assert_character(title, n = 1)
    payload$title <- title
  }

  if (!is_missing_or_null(body)) {
    assert_character(body, n = 1)
    payload$body <- body
  }

  if (!is_missing_or_null(state)) {
    assert_character(state, n = 1)
    assert_in(state, values$pull_request$state)
    payload$state <- state
  }

  if (!is_missing_or_null(base)) {
    assert_character(base, n = 1)
    payload$base <- base
  }

  if (is_natural(pull_request, n = 1)) {
    info("Viewing pull request '", pull_request, "' for repo '", repo, "'")
    pull_lst <- gh_url("repos", repo, "pulls", pull_request) %>%
      gh_request("GET", ...)
  }
  else if (is_character(pull_request, n = 1)) {
    info("Viewing pull_request '", pull_request, "' for repo '", repo, "'")
    pull_lst <- gh_url("repos", repo, "pulls", state = "all") %>%
      gh_find(property  = "title", value = pull_request, ...)
  }
  else {
    error("'pull_request' must be an integer or character vector of length 1")
  }

  if (length(payload) > 0) {
    info(
      "Updating pull request '", pull_lst$title,
      "' in repository '", repo, "'"
    )
    pull_lst <- gh_url("repos", repo, "issues", pull_lst$number) %>%
      gh_request("PATCH", payload = payload, ...)
  }

  payload <- list()

  if (!is_missing_or_null(assignees)) {
    assert_character(assignees)
    payload$assignees <- as.list(assignees)
  }

  if (!is_missing_or_null(labels)) {
    assert_character(labels)
    payload$labels <- as.list(labels)
  }

  if (!is_missing_or_null(milestone)) {
    if (is_character(milestone, n = 1)) {
      milestone <- view_milestone(milestone, repo = repo, ...)$number
    }
    assert_natural(milestone, n = 1)
    payload$milestone <- milestone
  }

  issue_lst <- list(milestone = list(title = NA_character_))
  if (length(payload) > 0) {
    info(
      "Adding assignees, labels or milestone to pull request '",
      pull_lst$title, "'"
    )
    issue_lst <- gh_url("repos", repo, "issues", pull_lst$number) %>%
      gh_request("PATCH", payload = payload, ...)
  }

  reviewers_lst <- list()
  if (!is_missing_or_null(reviewers)) {
    assert_character(reviewers)
    info("Adding reviewers to pull request '", pull_lst$title, "'")
    reviewers_lst <- gh_url(
      "repos", repo, "pulls", pull_lst$number, "requested_reviewers"
    ) %>%
      gh_request("POST", payload = list(reviewers = as.list(reviewers)), ...)
  }

  info("Transforming results", level = 4)
  pull_gh <- select_properties(pull_lst, properties$pull_request) %>%
    modify_list(
      assignees = map_chr(issue_lst$assignees, "login"),
      reviewers = map_chr(reviewers_lst$requested_reviewers, "login"),
      labels    = map_chr(issue_lst$labels, "name"),
      .before   = "milestone"
    ) %>%
    modify_list(repository = repo, milestone = issue_lst$milestone$title)

  info("Done", level = 7)
  pull_gh
}


#  FUNCTION: view_pull_requests ------------------------------------------------
#
#' View pull requests within a repository
#'
#' `view_pull_requests()` summarises pull requests in a table with the
#' properties as columns and a row for each pull request in the repository.
#' `view_pull_request()` returns a list of all properties for a single pull
#' request. `browse_pull_request()` opens the web page for the pull request in
#' the default browser.
#'
#' You can filter the pull requests by the head and base branches (the branch to
#' merge in and the branch to merge into) or the state (whether they are
#' `"open"` or `"closed"`). You can also order the results with `sort` and
#' `direction`.
#'
#' For more details see the GitHub API documentation:
#'
#' - <https://docs.github.com/en/rest/reference/pulls#list-pull-requests>
#' - <https://docs.github.com/en/rest/reference/pulls#get-a-pull-request>
#'
#' @param pull_request (string or character) The number or title of the pull
#'   request.
#' @param repo (string) The repository specified in the format: `owner/repo`.
#' @param head (string, optional) Filter pull requests by the branch name. If it
#'   is not in the specified `repo` then the owner must prefix the branch name,
#'   e.g. `"owner:branch"`.
#' @param base (string, optional) Filter pull requests by the branch to be
#'   merged into.
#' @param state (string, optional) The state of the pull requests to return. Can
#'   be either `"open"`, `"closed"`, or `"all"`. Default: `"open"`.
#' @param sort (string, optional) The property to order the returned pull
#'   requests by. Can be either `"created"`, `"updated"`, `"popularity"`
#'   (comment count) or `"long-running"` (age, filtering by pulls updated in the
#'   last month). Default: `"created"`.
#' @param direction (string, optional) The direction of the sort. Can be either
#'   `"asc"` or `"desc"`. Default: `"desc"`.
#' @param n_max (integer, optional) Maximum number to return. Default: `1000`.
#' @param ... Parameters passed to [gh_page()] or [gh_request()].
#'
#' @return `view_pull_requests()` returns a tibble of pull request properties.
#'   `view_pull_request()` returns a list of properties for a single pull
#'   request. `browse_pull_request()` opens the default browser on the pull
#'   request's page and returns the URL.
#'
#' **Pull Request Properties:**
#'
#' - **number**: The number assigned to the pull request.
#' - **title**: The title of the pull request.
#' - **body**: The body contents of the pull request.
#' - **head_sha**: The SHA of the commit to merge in.
#' - **head_ref**: The reference, or branch, to merge in.
#' - **head_repo**: The repository containing the branch to merge in.
#' - **base_sha**: The SHA of the commit to merge onto.
#' - **base_ref**: The reference, or branch, to merge onto.
#' - **merge_sha**: The SHA of the merge commit, if the merge has been
#'   completed.
#' - **assignees**: The users assigned to the pull request.
#' - **reviewers**: The users reviewing the pull request.
#' - **labels**: The labels attached to the pull request.
#' - **milestone**: The milestone assigned to the pull request.
#' - **state**: The state of the pull request - either `"open"` or `"closed"`.
#' - **repository**: The repository the pull request is in.
#' - **html_url**: The URL of the pull request's web page in GitHub.
#' - **diff_url**: The URL of the pull request's diff web page in GitHub.
#' - **creator**: The creator's login.
#' - **created_at**: When the pull request was created.
#' - **updated_at**: When the pull request was last updated.
#' - **mergeable**: Whether the pull request can be merged.
#' - **rebaseable**: Whether the pull request can be rebased.
#' - **merged**: Whether the pull request has been merged
#' - **merged_by**: Who merged the pull request.
#' - **merged_at**: When the pull request was merged.
#' - **closed_at**: When the pull request was closed.
#'
#' Additionally, the `view_pull_request()` function also returns:
#'
#' - **commits**: Information about the commits made on the branch.
#'   - **message**: The message specified for the commit.
#'   - **author_name**: The author's name.
#'   - **author_email**: The author's email address.
#'   - **author_date**: The date/time it was authored.
#'   - **committer_name**: The committer's name.
#'   - **committer_email**: The committer's email address.
#'   - **committer_date**: The date/time it was committed.
#'   - **parent_sha**: The SHA of the parent commit(s).
#'   - **html_url**: The URL of the commit's web page in GitHub.
#'
#' - **files**: Information about the files changed.
#'   - **sha**: The SHA of the file.
#'   - **filename**: The name of the file.
#'   - **status**: The status of the file.
#'   - **additions**: The number of lines added.
#'   - **deletions**: The number of lines deleted.
#'   - **changes**: The number of lines changed.
#'   - **patch**: The patch information.
#'   - **html_url**: The URL of the file's web page in GitHub.
#'
#' - **reviews**: The reviews registered.
#'   - **body**: The contents of the review.
#'   - **state**: The state of the review.
#'   - **user**: The user who added the review.
#'   - **html_url**: Th URL of the review's web page in GitHub.
#'   - **submitted_at**: When the review was submitted.
#'
#' @examples
#' \dontrun{
#'
#'   # View all open pull requests
#'   view_pull_requests("ChadGoymer/githapi")
#'
#'   # View all closed pull requests
#'   view_pull_requests("ChadGoymer/githapi", state = "closed")
#'
#'   # View all pull requests for the "main" branch
#'   view_pull_requests("ChadGoymer/githapi", base = "main")
#'
#'   # View pull requests, sorted by the most recently updated
#'   view_pull_requests(
#'     repo      = "ChadGoymer/githapi",
#'     sort      = "updated",
#'     direction = "desc"
#'   )
#'
#'   # View single pull request
#'   view_pull_request("test pull request", repo = "ChadGoymer/githapi")
#'
#'   # Open a pull request's page in a browser
#'   browse_pull_request("test pull request", repo = "ChadGoymer/githapi")
#'
#' }
#'
#' @export
#'
view_pull_requests <- function(
  repo,
  head,
  base,
  state     = "open",
  sort      = "created",
  direction = "desc",
  n_max     = 1000,
  ...
) {
  assert_repo(repo)

  if (!is_missing_or_null(head)) {
    assert_character(head, n = 1)
    if (!str_detect(head, ":")) {
      head <- str_c(fs::path_dir(repo), ":", head)
    }
  }
  else {
    head <- NULL
  }

  if (!is_missing_or_null(base)) {
    assert_character(base, n = 1)
  }
  else {
    base <- NULL
  }

  assert_character(state, n = 1)
  assert_in(state, values$pull_request$state)
  assert_character(sort, n = 1)
  assert_in(sort, values$pull_request$sort)
  assert_character(direction, n = 1)
  assert_in(direction, values$pull_request$direction)

  info("Viewing pull requests for repository '", repo, "'")
  pulls_lst <- gh_url(
    "repos", repo, "pulls",
    head      = head,
    base      = base,
    state     = state,
    sort      = sort,
    direction = direction
  ) %>%
    gh_page(n_max = n_max, ...)

  info("Transforming results", level = 4)
  pulls_gh <- bind_properties(pulls_lst, properties$pull_request) %>%
    add_column(
      labels  = map(pulls_lst, ~ map_chr(.$labels, "name")),
      .before = "milestone"
    ) %>%
    add_column(
      assignees = map(pulls_lst, ~ map_chr(.$assignees, "login")),
      .before   = "labels"
    ) %>%
    add_column(
      reviewers = map(pulls_lst, ~ map_chr(.$requested_reviewers, "login")),
      .before   = "labels"
    )

  info("Done", level = 7)
  pulls_gh
}


#  FUNCTION: view_pull_request -------------------------------------------------
#
#' @rdname view_pull_requests
#' @export
#'
view_pull_request <- function(
  pull_request,
  repo,
  n_max = 1000,
  ...
) {
  assert_repo(repo)
  assert_natural(n_max, n = 1)

  if (is_natural(pull_request, n = 1)) {
    info("Viewing pull request '", pull_request, "' for repo '", repo, "'")
    pull_lst <- gh_url("repos", repo, "pulls", pull_request) %>%
      gh_request("GET", ...)
  }
  else if (is_character(pull_request, n = 1)) {
    info("Viewing pull_request '", pull_request, "' for repo '", repo, "'")
    pull_lst <- gh_url("repos", repo, "pulls", state = "all") %>%
      gh_find(property  = "title", value = pull_request, ...)
  }
  else {
    error("'pull_request' must be an integer or character vector of length 1")
  }

  info("Transforming results", level = 4)
  commits_lst <- gh_url("repos", repo, "pulls", pull_lst$number, "commits") %>%
    gh_page(n_max = n_max, ...)
  commits <- bind_properties(commits_lst, properties$pull_commits) %>%
    add_column(
      parents = map(commits_lst, ~ map_chr(.$parents, "sha")),
      .before = "html_url"
    )

  files <- gh_url("repos", repo, "pulls", pull_lst$number, "files") %>%
    gh_page(n_max = n_max, ...) %>%
    bind_properties(properties$pull_files)

  reviews <- gh_url("repos", repo, "pulls", pull_lst$number, "reviews") %>%
    gh_page(n_max = n_max, ...) %>%
    bind_properties(properties$pull_reviews)

  pull_gh <- select_properties(pull_lst, properties$pull_request) %>%
    modify_list(
      assignees = map_chr(pull_lst$assignees, "login"),
      reviewers = map_chr(pull_lst$requested_reviewers, "login"),
      labels    = map_chr(pull_lst$labels, "name"),
      .before   = "milestone"
    ) %>%
    modify_list(
      repository = repo,
      commits    = commits,
      files      = files,
      reviews    = reviews
    )

  info("Done", level = 7)
  pull_gh
}


#  FUNCTION: browse_pull_request -----------------------------------------------
#
#' @rdname view_pull_requests
#' @export
#'
browse_pull_request <- function(
  pull_request,
  repo,
  ...
) {
  pull_request <- view_pull_request(
    pull_request = pull_request,
    repo         = repo,
    ...
  )

  info("Browsing pull request '", pull_request$title, "' in repo '", repo, "'")
  httr::BROWSE(pull_request$html_url)

  info("Done", level = 7)
  structure(
    pull_request$html_url,
    class   = c("github", "character"),
    url     = attr(pull_request, "url"),
    request = attr(pull_request, "request"),
    status  = attr(pull_request, "status"),
    header  = attr(pull_request, "header")
  )
}
ChadGoymer/githapi documentation built on Oct. 22, 2021, 10:56 a.m.