R/projects.R

Defines functions delete_project browse_project view_project view_projects update_project create_project

Documented in browse_project create_project delete_project update_project view_project view_projects

#  FUNCTION: create_project ----------------------------------------------------
#
#' Create a GitHub project
#'
#' This function creates a new project in GitHub. The project will be empty so
#' you will need to add columns and cards separately.
#'
#' You can create a project associated with either a repository or organization,
#' by supplying them as an input, as long as you have appropriate permissions.
#' If no repository or organization is specified the project is created for the
#' authenticated user.
#'
#' For more details see the GitHub API documentation:
#'
#' - <https://docs.github.com/en/rest/reference/projects#create-a-repository-project>
#' - <https://docs.github.com/en/rest/reference/projects#create-an-organization-project>
#' - <https://docs.github.com/en/rest/reference/projects#create-a-user-project>
#'
#' @param name (string) The name of the project.
#' @param body (string) The description of the project.
#' @param repo (string, optional) The repository specified in the format:
#'   `owner/repo`.
#' @param org (string, optional) The name of the organization.
#' @param ... Parameters passed to [gh_request()].
#'
#' @return `create_project()` returns a list of the project properties.
#'
#' **Project Properties:**
#'
#' - **id**: The ID of the project.
#' - **number**: The number of the project for the repository, user or
#'   organization.
#' - **name**: The name given to the project.
#' - **body**: The description given to the project.
#' - **state**: Whether the project is "open" or "closed".
#' - **private**: Whether the project is private (organisation project only).
#' - **org_permission**: The default permission for the project - either "read",
#'   "write" or "admin" (organisation project only).
#' - **creator**: The user who created the project.
#' - **created_at**: When it was created.
#' - **updated_at**: When it was last updated.
#' - **html_url**: The URL to view the project.
#'
#' @examples
#' \dontrun{
#'
#'   # Create a project for a repository
#'   create_project(
#'     name = "Repo project",
#'     body = "This is a repository's project",
#'     repo = "ChadGoymer/githapi"
#'   )
#'
#'   # Create a project for the current user
#'   create_project(
#'     name = "User project",
#'     body = "This is a user's project"
#'   )
#'
#'   # Create a project for an organization
#'   create_project(
#'     name = "Organization project",
#'     body = "This is an organization's project",
#'     org  = "HairyCoos"
#'   )
#'
#' }
#'
#' @export
#'
create_project <- function(
  name,
  body,
  repo,
  org,
  ...
) {
  assert_character(name, n = 1)
  assert_character(body, n = 1)

  if (!is_missing_or_null(repo)) {
    assert_repo(repo)
    info("Creating project '", name, "' for repository '", repo, "'")
    url <- gh_url("repos", repo, "projects")
  } else if (!is_missing_or_null(org)) {
    assert_character(org, n = 1)
    info("Creating project '", name, "' for organization '", org, "'")
    url <- gh_url("orgs", org, "projects")
  } else {
    info("Creating project '", name, "' for current user")
    url <- gh_url("user/projects")
  }

  project_lst <- gh_request(
    url     = url,
    type    = "POST",
    payload = list(name = name, body = body),
    accept  = "application/vnd.github.inertia-preview+json",
    ...
  )

  info("Transforming results", level = 4)
  project_gh <- select_properties(project_lst, properties$project)

  if (!is_missing_or_null(org)) {
    project_gh <- project_gh %>%
      modify_list(
        private        = project_lst$private,
        org_permission = project_lst$organization_permission,
        .after         = "state"
      )
  }

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


#  FUNCTION: update_project ----------------------------------------------------
#
#' Update a GitHub project
#'
#' This function updates a project in GitHub. It can be used to change the name
#' and body, but can also be used to close the project, change permissions or
#' add a team.
#'
#' You can update a project associated with either a repository, user, team or
#' organization, by supplying them as an input, as long as you have appropriate
#' permissions. Supplying a team that does not already have access to the
#' project adds them. If they have access, then the team's permissions can be
#' changed with the `permission` argument.
#'
#' For more details see the GitHub API documentation:
#'
#' - <https://docs.github.com/en/rest/reference/projects#update-a-project>
#' - <https://docs.github.com/en/rest/reference/teams#add-or-update-team-project-permissions>
#'
#' @param project (integer or string) Either the project number or name.
#' @param name (string, optional) The new name for the project.
#' @param body (string, optional) The new description of the project.
#' @param state (string, optional) The new state of the project, either `"open"`
#'   or `"closed"`.
#' @param permission (string, optional) The new team or organisation permissions
#'   for the project, either `"read"`, `"write"`, `"admin"` or `"none"`. Note:
#'   applies to team and organization projects only.
#' @param private (boolean, optional) Whether the project should be private.
#'   Note: applies to team and organization projects only.
#' @param repo (string, optional) The repository specified in the format:
#'   `owner/repo`.
#' @param user (string, optional) The login of the user.
#' @param team (string or integer, optional) The team ID or name.
#' @param org (string, optional) The name of the organization.
#' @param ... Parameters passed to [gh_request()].
#'
#' @return `update_project()` returns a list of the project properties.
#'
#' **Project Properties:**
#'
#' - **id**: The ID of the project.
#' - **number**: The number of the project for the repository, user or
#'   organization.
#' - **name**: The name given to the project.
#' - **body**: The description given to the project.
#' - **state**: Whether the project is "open" or "closed".
#' - **private**: Whether the project is private (organization and team projects
#'   only).
#' - **org_permissions**: The default permission for organization members
#'   (organization and team projects only).
#' - **team_permissions**: The default permission for team members (team
#'   projects only).
#' - **creator**: The user who created the project.
#' - **created_at**: When it was created.
#' - **updated_at**: When it was last updated.
#' - **html_url**: The URL to view the project.
#'
#' @examples
#' \dontrun{
#'
#'   # Update the name of a project for a repository
#'   update_project(
#'     project = "Repo project",
#'     name    = "Updated repo project",
#'     body    = "This is an updated repository's project",
#'     repo    = "ChadGoymer/githapi"
#'   )
#'
#'   # Update the state of a project for a user
#'   update_project(
#'     name  = "User project",
#'     state = "closed",
#'     user  = "ChadGoymer"
#'   )
#'
#'   # Update the permissions of a project for an organization
#'   update_project(
#'     name       = "Org project",
#'     permission = "read",
#'     private    = TRUE,
#'     org        = "HairyCoos"
#'   )
#'
#'   # Add a team to the project
#'   update_project(
#'     project = "Org project",
#'     team    = "HeadCoos",
#'     org     = "HairyCoos"
#'   )
#'
#'   # Update the team's permissions on the project
#'   update_project(
#'     project    = "Org project",
#'     permission = "write",
#'     team       = "HeadCoos",
#'     org        = "HairyCoos"
#'   )
#'
#' }
#'
#' @export
#'
update_project <- function(
  project,
  name,
  body,
  state,
  permission,
  private,
  repo,
  user,
  team,
  org,
  ...
) {
  project <- view_project(
    project = project,
    repo    = repo,
    user    = user,
    org     = org,
    ...
  )

  payload <- NULL

  if (!is_missing_or_null(team)) {
    if (!is_missing_or_null(permission)) {
      assert_character(permission, n = 1)
      assert_in(permission,  values$project$permission)
      payload$permission <- permission
    }

    team_id <- team
    if (is_character(team, n = 1)) {
      assert_character(org, n = 1)
      team_id <- gh_url("orgs", org, "teams") %>%
        gh_find(property = "name", value = team, ...) %>%
        pluck("id")
    }
    assert_natural(team_id, n = 1)

    info("Adding project '", project$name, "' to team '", team, "'")
    result <- gh_url("teams", team_id, "projects", project$id) %>%
      gh_request(
        type    = "PUT",
        payload = payload,
        accept  = "application/vnd.github.inertia-preview+json",
        ...
      )

    project_gh <- view_project(
      project = project$name,
      team    = team,
      org     = org,
      ...
    )

    info("Done", level = 7)
    structure(
      project_gh,
      url     = attr(result, "url"),
      request = attr(result, "request"),
      status  = attr(result, "status"),
      header  = attr(result, "header")
    )
  } else {
    if (!is_missing_or_null(permission)) {
      assert_character(permission, n = 1)
      assert_in(permission, values$project$permission)
      payload$organization_permission <- permission
    }

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

    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, str_subset(values$project$state, "all", negate = TRUE))
      payload$state <- state
    }

    if (!is_missing_or_null(private)) {
      assert_logical(private, n = 1)
      payload$private <- private
    }

    info("Updating project '", project$name, "'")
    project_lst <- gh_url("projects", project$id) %>%
      gh_request(
        type    = "PATCH",
        payload = payload,
        accept  = "application/vnd.github.inertia-preview+json",
        ...
      )

    info("Transforming results", level = 4)
    project_gh <- select_properties(project_lst, properties$project)

    if (!is_missing_or_null(org)) {
      project_gh <- project_gh %>%
        modify_list(
          private        = project_lst$private,
          org_permission = project_lst$organization_permission,
          .after         = "state"
        )
    }

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


#  FUNCTION: view_projects -----------------------------------------------------
#
#' View GitHub projects
#'
#' `view_projects()` summarises projects in a table with the properties as
#' columns and a row for each project. `view_project()` returns a list of all
#' properties for a single project. `browse_project()` opens the web page for
#' the project in the default browser.
#'
#' You can summarise all the projects associated with either a repository, user,
#' team or organization, by supplying them as an input.
#'
#' For more details see the GitHub API documentation:
#'
#' - <https://docs.github.com/en/rest/reference/projects#list-repository-projects>
#' - <https://docs.github.com/en/rest/reference/projects#list-user-projects>
#' - <https://docs.github.com/en/rest/reference/projects#list-organization-projects>
#' - <https://docs.github.com/en/rest/reference/teams#check-team-permissions-for-a-project>
#' - <https://docs.github.com/en/rest/reference/projects#get-a-project>
#'
#' @param project (integer or string) The number or name of the project.
#' @param repo (string, optional) The repository specified in the format:
#'   `owner/repo`.
#' @param user (string, optional) The login of the user.
#' @param team (string or integer, optional) The team ID or name.
#' @param org (string, optional) The name of the organization.
#' @param state (string, optional) Indicates the state of the projects to
#'   return. Can be either "open", "closed", or "all". Default: `"open"`.
#' @param n_max (integer, optional) Maximum number to return. Default: `1000`.
#' @param ... Parameters passed to [gh_page()] or [gh_request()].
#'
#' @return `view_projects()` returns a tibble of project properties.
#'   `view_project()` returns a list of properties for a single project.
#'   `browse_project()` opens the default browser on the project's page and
#'   returns the URL.
#'
#' **Project Properties:**
#'
#' - **id**: The ID of the project.
#' - **number**: The number of the project for the repository, user or
#'   organization.
#' - **name**: The name given to the project.
#' - **body**: The description given to the project.
#' - **state**: Whether the project is "open" or "closed".
#' - **private**: Whether the project is private (organization and team projects
#'   only).
#' - **org_permissions**: The default permission for organization members
#'   (organization and team projects only).
#' - **team_permissions**: The default permission for team members (team
#'   projects only).
#' - **creator**: The user who created the project.
#' - **created_at**: When it was created.
#' - **updated_at**: When it was last updated.
#' - **html_url**: The URL to view the project.
#'
#' @examples
#' \dontrun{
#'
#'   # View a repository's projects
#'   view_projects("ChadGoymer/githapi")
#'
#'   # View a user's projects
#'   view_projects(user = "ChadGoymer")
#'
#'   # View an organization's projects
#'   view_projects(org = "HairyCoos")
#'
#'   # View a team's projects
#'   view_projects(team = "HeadCoos", org = "HairyCoos")
#'
#'   # View closed projects
#'   view_projects("ChadGoymer/githapi", state = "closed")
#'
#'   # View all projects
#'   view_projects("ChadGoymer/githapi", state = "all")
#'
#'   # View a specific repository project
#'   view_project("Prioritisation", repo = "ChadGoymer/githapi")
#'
#'   # View a specific user project
#'   view_project("Test project", user = "ChadGoymer")
#'
#'   # View a specific organization project
#'   view_project("Prioritisation", org = "HairyCoos")
#'
#'   # View a specific team project
#'   view_project("Prioritisation", team = "HeadCoos", org = "HairyCoos")
#'
#'   # Browse a specific repository project
#'   browse_project("Prioritisation", "ChadGoymer/githapi")
#'
#'   # Browse a specific user project
#'   browse_project("Test project", user = "ChadGoymer")
#'
#'   # Browse a specific organization project
#'   browse_project("Prioritisation", org = "HairyCoos")
#'
#'   # Browse a specific team project
#'   browse_project("Prioritisation", team = "HeadCoos", org = "HairyCoos")
#'
#' }
#'
#' @export
#'
view_projects <- function(
  repo,
  user,
  team,
  org,
  state = "open",
  n_max = 1000,
  ...
) {
  assert_character(state, n = 1)
  assert_in(state, values$project$state)
  assert_natural(n_max, n = 1)

  if (!is_missing_or_null(repo)) {
    assert_repo(repo)
    info("Viewing projects for repository '", repo, "'")
    url <- gh_url("repos", repo, "projects", state = state)
  } else if (!is_missing_or_null(user)) {
    assert_character(user, n = 1)
    info("Viewing projects for user '", user, "'")
    url <- gh_url("users", user, "projects", state = state)
  } else if (!is_missing_or_null(team)) {
    team_id <- team
    if (is_character(team, n = 1)) {
      assert_character(org, n = 1)
      team_id <- gh_url("orgs", org, "teams") %>%
        gh_find(property = "name", value = team, ...) %>%
        pluck("id")
    }
    assert_natural(team_id, n = 1)

    info("Viewing projects for team '", team, "'")
    url <- gh_url("teams", team_id, "projects", state = state)
  } else if (!is_missing_or_null(org)) {
    assert_character(org, n = 1)
    info("Viewing projects for organization '", org, "'")
    url <- gh_url("orgs", org, "projects", state = state)
  } else {
    error("Must specify either 'repo', 'user' or 'org'!")
  }

  projects_lst <- gh_page(
    url    = url,
    accept = "application/vnd.github.inertia-preview+json",
    n_max  = n_max,
    ...
  )

  info("Transforming results", level = 4)
  projects_gh <- bind_properties(projects_lst, properties$project)

  if (!is_missing_or_null(team)) {
    proj_order <- values$project$permission
    team_permission <- map_chr(projects_lst, function(p) {
      last(
        proj_order[
          proj_order %in% names(p$permissions[as.logical(p$permissions)])
        ]
      )
    })
    projects_gh <- add_column(
      projects_gh,
      team_permission = team_permission,
      .after          = "state"
    )
  }

  if (!is_missing_or_null(org)) {
    org_permission <- map_chr(projects_lst, pluck, "organization_permission")
    private <- map_lgl(projects_lst, pluck, "private")
    projects_gh <- projects_gh %>%
      add_column(org_permission = org_permission, .after = "state") %>%
      add_column(private = private, .after = "state")
  }

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


#  FUNCTION: view_project ------------------------------------------------------
#
#' @rdname view_projects
#' @export
#'
view_project <- function(
  project,
  repo,
  user,
  team,
  org,
  ...
) {
  if (is_natural(project, n = 1)) {
    property <- "number"
  } else if (is_character(project, n = 1)) {
    property <- "name"
  } else {
    error("'project' must be an integer or character vector of length 1")
  }

  if (!is_missing_or_null(repo)) {
    assert_repo(repo)
    info("Viewing project '", project, "' for repository '", repo, "'")
    url <- gh_url("repos", repo, "projects", state = "all")
  } else if (!is_missing_or_null(user)) {
    assert_character(user, n = 1)
    info("Viewing project '", project, "' for user '", user, "'")
    url <- gh_url("users", user, "projects", state = "all")
  } else if (!is_missing_or_null(team)) {
    team_id <- team
    if (is_character(team, n = 1)) {
      assert_character(org, n = 1)
      team_id <- gh_url("orgs", org, "teams") %>%
        gh_find(property = "name", value = team, ...) %>%
        pluck("id")
    }
    assert_natural(team_id, n = 1)

    info("Viewing project '", project, "' for team '", team, "'")
    url <- gh_url("teams", team_id, "projects", state = "all")
  } else if (!is_missing_or_null(org)) {
    assert_character(org, n = 1)
    info("Viewing project '", project, "' for organization '", org, "'")
    url <- gh_url("orgs", org, "projects", state = "all")
  }
  else {
    error("Must specify either 'repo', 'user' or 'org'!")
  }

  project_lst <- gh_find(
    url       = url,
    property  = property,
    value     = project,
    accept    = "application/vnd.github.inertia-preview+json",
    ...
  )

  info("Transforming results", level = 4)
  project_gh <- select_properties(project_lst, properties$project)

  if (!is_missing_or_null(team)) {
    proj_order <- values$project$permission
    permission <- last(
      proj_order[
        proj_order %in%
          names(project_lst$permissions[as.logical(project_lst$permissions)])
      ]
    )

    project_gh <- project_gh %>%
      modify_list(team_permission = permission, .after = "state")
  }

  if (!is_missing_or_null(org)) {
    project_gh <- project_gh %>%
      modify_list(
        private        = project_lst$private,
        org_permission = project_lst$organization_permission,
        .after         = "state"
      )
  }

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


#  FUNCTION: browse_project ----------------------------------------------------
#
#' @rdname view_projects
#' @export
#'
browse_project <- function(
  project,
  repo,
  user,
  team,
  org,
  ...
) {
  project <- view_project(
    project = project,
    repo    = repo,
    user    = user,
    team    = team,
    org     = org,
    ...
  )

  info("Browsing project '", project$name, "'")
  httr::BROWSE(project$html_url)

  info("Done", level = 7)
  structure(
    project$html_url,
    class   = c("github", "character"),
    url     = attr(project, "url"),
    request = attr(project, "request"),
    status  = attr(project, "status"),
    header  = attr(project, "header")
  )
}


#  FUNCTION: delete_project ----------------------------------------------------
#
#' Delete a GitHub project
#'
#' This function deletes a project in GitHub. Care should be taken as it will
#' not be recoverable. If you just want to close the project use
#' [update_project()].
#'
#' You can delete a project associated with either a repository, user, team or
#' organization, by supplying them as an input, as long as you have appropriate
#' permissions. Deleting a team project just removes the team's access. If you
#' want to delete it completely you have to delete the organization's project.
#'
#' For more details see the GitHub API documentation:
#'
#' - <https://docs.github.com/en/rest/reference/projects#delete-a-project>
#' - <https://docs.github.com/en/rest/reference/teams#remove-a-project-from-a-team>
#'
#' @param project (integer or string) Either the project number or name.
#' @param repo (string, optional) The repository specified in the format:
#'   `owner/repo`.
#' @param user (string, optional) The login of the user.
#' @param team (string or integer, optional) The team ID or name.
#' @param org (string, optional) The name of the organization.
#' @param ... Parameters passed to [gh_request()].
#'
#' @return `delete_project()` returns a TRUE if successfully deleted.
#'
#' @examples
#' \dontrun{
#'
#'   # Delete a project for a repository
#'   delete_project(
#'     project = "Repo project",
#'     repo    = "ChadGoymer/githapi"
#'   )
#'
#'   # Delete a project for a user
#'   delete_project(
#'     project = "User project",
#'     user    = "ChadGoymer"
#'   )
#'
#'   # Remove a team's access to the organization's project
#'   delete_project(
#'     project = "User project",
#'     team    = "HeadCoos",
#'     org     = "HairyCoos"
#'   )
#'
#'   # Delete a project for an organization
#'   delete_project(
#'     project = "User project",
#'     org     = "HairyCoos"
#'   )
#'
#' }
#'
#' @export
#'
delete_project <- function(
  project,
  repo,
  user,
  team,
  org,
  ...
) {
  project <- view_project(
    project = project,
    repo    = repo,
    user    = user,
    team    = team,
    org     = org,
    ...
  )

  if (is_missing_or_null(team)) {
    info("Deleting project '", project$name, "'")
    url <- gh_url("projects", project$id)
  }
  else {
    team_id <- team
    if (is_character(team, n = 1)) {
      assert_character(org, n = 1)
      team_id <- gh_url("orgs", org, "teams") %>%
        gh_find(property = "name", value = team, ...) %>%
        pluck("id")
    }
    assert_natural(team_id, n = 1)

    info("Removing project '", project$name, "' from team '", team, "'")
    url <- gh_url("teams", team_id, "projects", project$id)
  }

  result <- gh_request(
    url    = url,
    type   = "DELETE",
    accept = "application/vnd.github.inertia-preview+json",
    ...
  )

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