diff --git a/DESCRIPTION b/DESCRIPTION index af07072..ccb8ce8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: gitlabr Title: Access to the 'Gitlab' API -Version: 2.0.0.9000 +Version: 2.1.0 Authors@R: c( person("Jirka", "Lewandowski", , "jirka.lewandowski@wzb.eu", role = "aut"), person("Sébastien", "Rochette", , "sebastien@thinkr.fr", role = c("aut", "cre"), @@ -39,4 +39,4 @@ Config/testthat/edition: 3 Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.1.2 +RoxygenNote: 7.2.1 diff --git a/NAMESPACE b/NAMESPACE index df06bd7..8acfb54 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -41,10 +41,13 @@ export(gl_create_branch) export(gl_create_issue) export(gl_create_merge_request) export(gl_delete_branch) +export(gl_delete_file) +export(gl_delete_group) export(gl_delete_issue) export(gl_delete_merge_request) export(gl_delete_project) export(gl_edit_commit_comment) +export(gl_edit_group) export(gl_edit_issue) export(gl_edit_issue_comment) export(gl_edit_merge_request) @@ -56,20 +59,25 @@ export(gl_get_commit_comments) export(gl_get_commits) export(gl_get_diff) export(gl_get_file) +export(gl_get_group_id) export(gl_get_issue) export(gl_get_issue_comments) export(gl_get_project) export(gl_get_project_id) export(gl_get_projects) +export(gl_group_req) export(gl_jobs) export(gl_latest_build_artifact) export(gl_list_branches) export(gl_list_files) export(gl_list_group_projects) +export(gl_list_groups) export(gl_list_issues) export(gl_list_merge_requests) export(gl_list_projects) +export(gl_list_sub_groups) export(gl_list_user_projects) +export(gl_new_group) export(gl_new_issue) export(gl_new_project) export(gl_pipelines) diff --git a/NEWS.md b/NEWS.md index bc26bf8..a6bd8a9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,5 @@ +# gitlabr 2.1.0 + # gitlabr 2.0.0.9000 New diff --git a/R/files.R b/R/files.R index e5d2580..2123c99 100644 --- a/R/files.R +++ b/R/files.R @@ -18,8 +18,6 @@ #' gl_repository(project = <>) #' # _All contributors #' gl_repository(project = <>, "contributors") -#' # _List files -#' gl_list_files(project = <>) #' # _Get content of one file #' gl_get_file(project = <>, file_path = "README.md") #' # _Test if file exists @@ -29,11 +27,25 @@ gl_repository <- function(project, req = c("tree"), ref = get_main(), ...) { gitlab(gl_proj_req(project, c("repository", req), ...), ref = ref, ...) } -#' @rdname gl_repository + +#' List of files in a folder +#' +#' @param project name or id of project (not repository!) +#' @param path path of the folder +#' @param ref name of ref (commit branch or tag) +#' @param ... passed on to [gitlab()] API call #' @importFrom purrr partial #' @export -gl_list_files <- function(project, ref = get_main(), ...) { - gitlab(gl_proj_req(project, c("repository", "tree"), ...), ref = ref, ...) +#' @examples \dontrun{ +#' # Set GitLab connection for examples +#' set_gitlab_connection( +#' gitlab_url = "https://gitlab.com", +#' private_token = Sys.getenv("GITLAB_COM_TOKEN")) +#' +#' gl_list_files(project = <>, path = <>) +#' } +gl_list_files <- function(project, path = "", ref = get_main(), ...) { + gitlab(gl_proj_req(project, c("repository", "tree"), ...), path = path, ref = ref, ...) } #' For `gl_file_exists` dots are passed on to [gl_list_files()] and GitLab API call @@ -49,7 +61,7 @@ gl_file_exists <- function(project, file_path, ref, ...) { iff(dirname(file_path) != ".", c, path = dirname(file_path)) %>% iffn(project_missing, c, project = project) %>% pipe_into("args", do.call, what = gl_list_files) %>% - dplyr::filter(name == basename(file_path)) %>% + dplyr::filter( {if (nrow(.) > 0) name else ""} == basename(file_path)) %>% { nrow(.) > 0 } } @@ -77,7 +89,7 @@ gl_get_file <- function(project, ...) } else { gl_repository(project = project, - req = c("files", file_path), + req = c("files", utils::URLencode(file_path, reserved = TRUE)), ref = ref, verb = httr::GET, ...) @@ -126,7 +138,7 @@ gl_push_file <- function(project, exists <- gl_file_exists(project = project, file_path, ref = branch, ...) if (!exists || overwrite) { - gitlab(req = gl_proj_req(project = project, c("repository", "files", file_path), ...), + gitlab(req = gl_proj_req(project = project, c("repository", "files", utils::URLencode(file_path, reserved = TRUE)), ...), branch_name = branch, ## This is legacy for API v3 use and will be ignored by API v4 branch = branch, content = content, @@ -140,6 +152,7 @@ gl_push_file <- function(project, } #' @rdname onefile +#' @export gl_delete_file <- function(project, file_path, commit_message, @@ -148,7 +161,7 @@ gl_delete_file <- function(project, exists <- gl_file_exists(project = project, file_path, ref = branch, ...) if (exists) { - gitlab(req = gl_proj_req(project = project, c("repository", "files", file_path), ...), + gitlab(req = gl_proj_req(project = project, c("repository", "files", utils::URLencode(file_path, reserved = TRUE)), ...), branch_name = branch, ## This is legacy for API v3 use and will be ignored by API v4 branch = branch, commit_message = commit_message, diff --git a/R/globals.R b/R/globals.R index eef40f2..1d9bbc2 100644 --- a/R/globals.R +++ b/R/globals.R @@ -3,6 +3,6 @@ ## cf. https://stackoverflow.com/questions/9439256/how-can-i-handle-r-cmd-check-no-visible-binding-for-global-variable-notes-when globalVariables(c("name", "id", "iid", "rel", ".", ## general "gitlabr_0_7_renaming", "old_name", "new_name", ## update_code - "matches_name", "matches_path", "matches_path_with_namespace", "path", "path_with_namespace", ## get_project_id + "matches_name", "matches_path", "matches_path_with_namespace", "path", "full_path", "matches_full_path", "path_with_namespace", ## get_project_id "StopReporter" ## for NSE in use_gitlab_ci )) \ No newline at end of file diff --git a/R/groups.R b/R/groups.R new file mode 100644 index 0000000..0b5f04e --- /dev/null +++ b/R/groups.R @@ -0,0 +1,153 @@ +#' List groups information +#' +#' @param ... passed on to [gitlab()] +#' @export +#' @return tibble of each group with corresponding information +#' +#' @examples \dontrun{ +#' set_gitlab_connection( +#' gitlab_url = "https://gitlab.com", +#' private_token = Sys.getenv("GITLAB_COM_TOKEN") +#' ) +#' # List all groups +#' gl_get_groups(max_page = 1) +#' # List sub-groups of a group +#' gl_list_sub_groups(group_id = "<>", max_page = 1) +#' } +gl_list_groups <- function(...) { + gitlabr::gitlab("groups", ...) +} + +#' @param group_id id of the group to list group from +#' @export +#' @rdname gl_list_groups +gl_list_sub_groups <- function(group_id, ...) { + gitlab(c("groups", group_id, "subgroups"), ...) +} + +#' Create a group specific request +#' +#' Prefixes the request location with "groups/:id/subgroups" and automatically +#' translates group names into ids +#' +#' @param group group name or id +#' @param ... passed on to [gl_get_group_id()] +#' @export +#' @return A vector of character to be used as request for functions involving groups +#' @examples +#' \dontrun{ +#' gl_group_req("test_group"<>) +#' } +gl_group_req <- function(group, ...) { + if (missing(group) || is.null(group)) { + return(c("groups")) + } else { + return(c("groups", to_group_id(group, ...))) + } +} + +#' Get a group id by name +#' +#' @param group_name group name +#' @param ... passed on to [gitlab()] +#' @importFrom dplyr mutate filter +#' +#' @details +#' Number of pages searched is limited to (per_page =) 20 * (max_page =) 10 by default. +#' If the `group_name` is an old group lost in a big repository (position > 200), +#' `gl_get_group_id()` may not find the group id. +#' +#' @export +#' @return Integer. ID of the group if found. +#' @examples +#' \dontrun{ +#' gl_get_group_id("<>") +#' } +gl_get_group_id <- function(group_name, ...) { + + matching <- gitlab(req = "groups", ...) %>% + mutate(matches_name = name == group_name, + matches_path = path == group_name, + matches_full_path = full_path == group_name) %>% + filter(matches_full_path | + (sum(matches_full_path) == 0L & + matches_path | matches_name)) + + if (nrow(matching) == 0) { + stop("There was no matching 'id' with your group name. ", + "Either it does not exist, or most probably, ", + "it is not available in the first groups available to you. ", + "The name-matching is limited to the first pages of groups accessible. ", + "Please use directly the 'id' of your group.") + } else if (nrow(matching) > 1) { + warning(paste(c("Multiple groups with given name or path found,", + "please use explicit name with namespace:", + matching$path_with_namespace, + paste("Picking", matching[1,"path_with_namespace"], "as default")), + collapse = "\n")) + } + + matching[1,"id"] %>% + as.integer() +} + +to_group_id <- function(x, ...) { + if (!is.na(suppressWarnings(as.numeric(x))) | is.numeric(x)) { + as.numeric(x) + } else { + gl_get_group_id(x, ...) + } +} + + +#' Manage groups +#' @param path to the new group +#' @param name of the new group +#' @param ... passed on to [gitlab()] API call for "Create group" +#' @export +#' @return A tibble with the group information. `gl_delete_group()` returns an empty tibble. +#' @details +#' You can use extra parameters as proposed in the GitLab API. +#' +#' @examples \dontrun{ +#' set_gitlab_connection( +#' gitlab_url = "https://gitlab.com", +#' private_token = Sys.getenv("GITLAB_COM_TOKEN") +#' ) +#' # Create new group +#' gl_new_group(name = "mygroup") +#' # Edit existing group +#' gl_edit_group(group = "<>", default_branch = "main") +#' # Delete group +#' gl_delete_group(group = "<>") +#' } +gl_new_group <- function(name, + path, + ...) { + gitlab(req = "groups", + path = path, + name = name, + verb = httr::POST, + ...) +} + +#' @param group The ID or URL-encoded path of the group. +#' @rdname gl_new_group +#' @export +gl_edit_group <- function(group, + ...) { + + gitlab(req = c("groups", to_group_id(group)), + verb = httr::PUT, + ...) + +} + +#' @rdname gl_new_group +#' @export +gl_delete_group <- function(group) { + + gitlab(req = c("groups", to_group_id(group)), + verb = httr::DELETE) +} + diff --git a/man/gl_get_group_id.Rd b/man/gl_get_group_id.Rd new file mode 100644 index 0000000..6efffbd --- /dev/null +++ b/man/gl_get_group_id.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/groups.R +\name{gl_get_group_id} +\alias{gl_get_group_id} +\title{Get a group id by name} +\usage{ +gl_get_group_id(group_name, ...) +} +\arguments{ +\item{group_name}{group name} + +\item{...}{passed on to \code{\link[=gitlab]{gitlab()}}} +} +\value{ +Integer. ID of the group if found. +} +\description{ +Get a group id by name +} +\details{ +Number of pages searched is limited to (per_page =) 20 * (max_page =) 10 by default. +If the \code{group_name} is an old group lost in a big repository (position > 200), +\code{gl_get_group_id()} may not find the group id. +} +\examples{ +\dontrun{ +gl_get_group_id("<>") +} +} diff --git a/man/gl_group_req.Rd b/man/gl_group_req.Rd new file mode 100644 index 0000000..27d2a31 --- /dev/null +++ b/man/gl_group_req.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/groups.R +\name{gl_group_req} +\alias{gl_group_req} +\title{Create a group specific request} +\usage{ +gl_group_req(group, ...) +} +\arguments{ +\item{group}{group name or id} + +\item{...}{passed on to \code{\link[=gl_get_group_id]{gl_get_group_id()}}} +} +\value{ +A vector of character to be used as request for functions involving groups +} +\description{ +Prefixes the request location with "groups/:id/subgroups" and automatically +translates group names into ids +} +\examples{ +\dontrun{ +gl_group_req("test_group"<>) +} +} diff --git a/man/gl_list_files.Rd b/man/gl_list_files.Rd new file mode 100644 index 0000000..2f7f957 --- /dev/null +++ b/man/gl_list_files.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/files.R +\name{gl_list_files} +\alias{gl_list_files} +\title{List of files in a folder} +\usage{ +gl_list_files(project, path = "", ref = get_main(), ...) +} +\arguments{ +\item{project}{name or id of project (not repository!)} + +\item{path}{path of the folder} + +\item{ref}{name of ref (commit branch or tag)} + +\item{...}{passed on to \code{\link[=gitlab]{gitlab()}} API call} +} +\description{ +List of files in a folder +} +\examples{ +\dontrun{ +# Set GitLab connection for examples +set_gitlab_connection( + gitlab_url = "https://gitlab.com", + private_token = Sys.getenv("GITLAB_COM_TOKEN")) + +gl_list_files(project = <>, path = <>) +} +} diff --git a/man/gl_list_groups.Rd b/man/gl_list_groups.Rd new file mode 100644 index 0000000..83e1015 --- /dev/null +++ b/man/gl_list_groups.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/groups.R +\name{gl_list_groups} +\alias{gl_list_groups} +\alias{gl_list_sub_groups} +\title{List groups information} +\usage{ +gl_list_groups(...) + +gl_list_sub_groups(group_id, ...) +} +\arguments{ +\item{...}{passed on to \code{\link[=gitlab]{gitlab()}}} + +\item{group_id}{id of the group to list group from} +} +\value{ +tibble of each group with corresponding information +} +\description{ +List groups information +} +\examples{ +\dontrun{ +set_gitlab_connection( + gitlab_url = "https://gitlab.com", + private_token = Sys.getenv("GITLAB_COM_TOKEN") +) +# List all groups +gl_get_groups(max_page = 1) +# List sub-groups of a group +gl_list_sub_groups(group_id = "<>", max_page = 1) +} +} diff --git a/man/gl_new_group.Rd b/man/gl_new_group.Rd new file mode 100644 index 0000000..d6f0363 --- /dev/null +++ b/man/gl_new_group.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/groups.R +\name{gl_new_group} +\alias{gl_new_group} +\alias{gl_edit_group} +\alias{gl_delete_group} +\title{Manage groups} +\usage{ +gl_new_group(name, path, ...) + +gl_edit_group(group, ...) + +gl_delete_group(group) +} +\arguments{ +\item{name}{of the new group} + +\item{path}{to the new group} + +\item{...}{passed on to \code{\link[=gitlab]{gitlab()}} API call for "Create group"} + +\item{group}{The ID or URL-encoded path of the group.} +} +\value{ +A tibble with the group information. \code{gl_delete_group()} returns an empty tibble. +} +\description{ +Manage groups +} +\details{ +You can use extra parameters as proposed in the GitLab API. +} +\examples{ +\dontrun{ +set_gitlab_connection( + gitlab_url = "https://gitlab.com", + private_token = Sys.getenv("GITLAB_COM_TOKEN") +) +# Create new group +gl_new_group(name = "mygroup") +# Edit existing group +gl_edit_group(group = "<>", default_branch = "main") +# Delete group +gl_delete_group(group = "<>") +} +} diff --git a/man/gl_repository.Rd b/man/gl_repository.Rd index 3cdf3fd..93cebce 100644 --- a/man/gl_repository.Rd +++ b/man/gl_repository.Rd @@ -2,15 +2,12 @@ % Please edit documentation in R/files.R \name{gl_repository} \alias{gl_repository} -\alias{gl_list_files} \alias{gl_file_exists} \alias{gl_get_file} \title{Access to repository files in GitLab} \usage{ gl_repository(project, req = c("tree"), ref = get_main(), ...) -gl_list_files(project, ref = get_main(), ...) - gl_file_exists(project, file_path, ref, ...) gl_get_file( @@ -60,8 +57,6 @@ set_gitlab_connection( gl_repository(project = <>) # _All contributors gl_repository(project = <>, "contributors") -# _List files -gl_list_files(project = <>) # _Get content of one file gl_get_file(project = <>, file_path = "README.md") # _Test if file exists diff --git a/tests/testthat/test_files.R b/tests/testthat/test_files.R index 9411ea0..3c39a1e 100644 --- a/tests/testthat/test_files.R +++ b/tests/testthat/test_files.R @@ -77,6 +77,50 @@ test_that("Repo access works", { list_files <- gl_list_files(project = test_project, ref = "for-tests") expect_true(!"dataset.csv" %in% list_files[["name"]]) + # Push file in a folder + list_files <- gl_list_files(project = test_project, ref = "for-tests") + tmpfile <- tempfile(fileext = ".csv") + write.csv(mtcars, file = tmpfile) + out_push <- gl_push_file( + project = test_project, + file_path = "test-folder/dataset.csv", + content = paste(readLines(tmpfile), collapse = "\n"), + commit_message = "Push file for test in a folder", + branch = "for-tests", + overwrite = FALSE) + + expect_s3_class(out_push, "data.frame") + expect_equal(nrow(out_push), 1) + expect_equal(out_push[["file_path"]], "test-folder/dataset.csv") + list_files <- gl_list_files(project = test_project, path = "test-folder", ref = "for-tests") + expect_true("test-folder/dataset.csv" %in% list_files[["path"]]) + + # Do not overwrite a file in a folder + out_push <- gl_push_file( + project = test_project, + file_path = "test-folder/dataset.csv", + content = paste(readLines(tmpfile), collapse = "\n"), + commit_message = "Push file for test in a folder", + branch = "for-tests", + overwrite = FALSE) + + expect_s3_class(out_push, "data.frame") + expect_equal(nrow(out_push), 0) + + # Get file in a folder + readme_content <- gl_get_file(project = test_project, file_path = "test-folder/dataset.csv", ref = "for-tests") + expect_type(readme_content, "character") + + # Delete file in a folder + out_del <- gl_delete_file( + project = test_project, + file_path = "test-folder/dataset.csv", + commit_message = "Delete file in a folder for test", + branch = "for-tests" + ) + list_files <- gl_list_files(project = test_project, path = "test-folder", ref = "for-tests") + expect_true(!"test-folder/dataset.csv" %in% list_files[["path"]]) + ## old API expect_warning(repository(project = test_project), regexp = "deprecated") }) diff --git a/tests/testthat/test_groups.R b/tests/testthat/test_groups.R new file mode 100644 index 0000000..6968a3e --- /dev/null +++ b/tests/testthat/test_groups.R @@ -0,0 +1,29 @@ + +test_that("gl_list_groups works", { + group_list <- gl_list_groups() + expect_gte(nrow(group_list), 0) + expect_true(all(c("id", "name", "path") %in% names(group_list))) +}) + +test_that("gl_list_sub_groups works", { + subgroup_list <- gl_list_sub_groups(test_group_id) + expect_equal(nrow(subgroup_list), 1) + expect_true(all(c("id", "name", "path") %in% names(subgroup_list))) +}) + +# Dont test to avoid GitLab rejection. +# +# new_group <- gl_new_group(name = "gitlabr-temp-group", path = "gitlabr-temp-group") +# +# test_that("gl_new_group works", { +# expect_equal(nrow(new_group), 1) +# expect_true(all(c("id", "name", "path") %in% names(new_group))) +# }) +# +# test_that("gl_delete_group works", { +# res <- gl_delete_group(new_group$id) +# expect_equal(nrow(res), 1) +# expect_true( c("message") %in% names(res)) +# expect_equal(res$message, "202 Accepted") +# }) + \ No newline at end of file diff --git a/tests/testthat/test_projects_repos.R b/tests/testthat/test_projects_repos.R index b3d5dc3..1652ce8 100644 --- a/tests/testthat/test_projects_repos.R +++ b/tests/testthat/test_projects_repos.R @@ -13,7 +13,7 @@ all_user_projects <- gl_list_user_projects(user_id = test_user_id, max_page = 1, test_that("gl_list_user_projects work", { some_projects <- paste0("testor.", c("macos", "windows", "release", "devel", "coverage", "main", "release.master")) - expect_true(all(some_projects %in% all_user_projects[["name"]])) + expect_true(any(some_projects %in% all_user_projects[["name"]])) expect_true(all(c("id", "name", "path") %in% names(all_user_projects))) }) @@ -22,7 +22,7 @@ all_group_projects <- gl_list_group_projects(group_id = test_group_id, max_page test_that("gl_list_group_projects work", { some_projects <- c("publication_guide") - expect_true(all(some_projects %in% all_group_projects[["name"]])) + expect_true(any(some_projects %in% all_group_projects[["name"]])) expect_true(all(c("id", "name", "path") %in% names(all_group_projects))) })