Commit bcba994a authored by Ben Boeckel's avatar Ben Boeckel Committed by Kitware Robot
Browse files

Merge topic 'gitlab-api-sync'

f21dc53d api/doc: add new API doc page links
f186bac7 api/doc: remove doc link which has been handled
61fa8412 api/doc: sort the list of API documentation pages
26d2b1e4 api/docs: add notes for new endpoints on existing pages
3e445202 api/docs: add notes about `not` filter support
f2cd409e api/users: support filtering by internal and administrator statuses
4e2848b4 api/projects: support filtering by storage backend
70baf6d1

 api/projects: support ordering by resource usages
...
Acked-by: Kitware Robot's avatarKitware Robot <kwrobot@kitware.com>
Acked-by: Brad King's avatarBrad King <brad.king@kitware.com>
Merge-request: !280
parents 4a2a1094 f21dc53d
# v0.1308.1 (unreleased)
## Breaking changes
* `ParamValue::as_value` now takes its value as `&self` rather than `self`.
This was required in order to implement `CommaSeparatedList` reliably.
* Merge request discussions on code now have a more fine-grained API. This
change was made by GitLab and is just being followed by the crate.
## Additions
* `api::common::CommaSeparatedList` now exists for easy use of
comma-separated values.
## Deprecations
* `EditIssue::remove_labels` is deprecated in favor of the better
`clear_labels` wording.
* `EditMergeRequest::remove_labels` is deprecated in favor of the better
`clear_labels` wording.
## Changes
* API bindings for the `"minimal"` access level.
* Groups can have "inherit" set as their shared runner minute limit.
* Listing groups can now be set to only return top-level groups.
* Searching for projects within a group can now be sorted by a similarity
score based on the search criteria.
* Project container expiration policies can now use an arbitrary "keep n"
count.
* Project container expiration policies now have `name_regex_delete`
(replacing the now-deprecated `name_regex`) and `name_regex_keep`.
* Projects can now be created and edited with `operations_access_level`
settings.
* Projects can now be created and edited with `requirements_access_level`
settings.
* Projects can now be created and edited with `analytics_access_level`
settings.
* Projects can now be created and edited with `show_default_award_emojis`
settings.
* Projects can now be created and edited with
`restrict_user_defined_variables` settings.
* Projects can now be created and edited with
`allow_merge_on_skipped_pipeline` settings.
* Projects can now be edited with `ci_forward_deployment_enabled` settings.
* Environments can now be filtered by their deployment state.
* Project hooks can now be registered for events related to confidential
notes, deployments, and releases.
* Issues can now be edited with incremental label changes.
* Issues can now be filtered by iterations, due dates, and search queries can
now be scoped.
* Issue notes can now be created and edited with the confidential flag.
* Project labels can be filtered by search queries.
* Project members can now be edited in batch (using multiple IDs).
* Merge requests can now be created and edited with reviewer settings.
* Merge requests can now be created with the `approvals_before_merge`
setting.
* Merge request discussions can now be created on a specific commit.
* Merge requests can now be edited with incremental label changes.
* Merge requests can now be filtered by search scopes.
* Merge requests can now trigger merge status rechecks when listing.
* Merge requests can now be filtered by reviewer.
* Merge requests can now be filtered by environment status.
* API bindings for the `"scheduled"` pipeline status.
* Projects can now be sorted by various resource sizes.
* Projects can now be filtered by storage backend.
* Users can now be filtered by GitLab-internal users and administrator
status.
# v0.1308.0
* No changes needed for GitLab 13.8.
......
This diff is collapsed.
......@@ -11,7 +11,10 @@
use std::borrow::Cow;
use std::fmt;
use std::iter;
use std::ops;
use itertools::Itertools;
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use crate::api::ParamValue;
......@@ -21,6 +24,8 @@ use crate::api::ParamValue;
pub enum AccessLevel {
/// Anonymous access.
Anonymous,
/// Minimal access.
Minimal,
/// Guest access (can see the project).
Guest,
/// Reporter access (can open issues).
......@@ -45,6 +50,7 @@ impl AccessLevel {
AccessLevel::Developer => "developer",
AccessLevel::Reporter => "reporter",
AccessLevel::Guest => "guest",
AccessLevel::Minimal => "minimal",
AccessLevel::Anonymous => "anonymous",
}
}
......@@ -58,6 +64,7 @@ impl AccessLevel {
AccessLevel::Developer => 30,
AccessLevel::Reporter => 20,
AccessLevel::Guest => 10,
AccessLevel::Minimal => 5,
AccessLevel::Anonymous => 0,
}
}
......@@ -89,7 +96,7 @@ impl SortOrder {
}
impl ParamValue<'static> for SortOrder {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
......@@ -124,7 +131,7 @@ impl From<bool> for EnableState {
}
impl ParamValue<'static> for EnableState {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
......@@ -209,7 +216,7 @@ impl VisibilityLevel {
}
impl ParamValue<'static> for VisibilityLevel {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
......@@ -246,7 +253,7 @@ impl From<bool> for YesNo {
}
impl ParamValue<'static> for YesNo {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
......@@ -282,23 +289,103 @@ impl ProtectedAccessLevel {
}
impl ParamValue<'static> for ProtectedAccessLevel {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
/// A comma-separated list of values.
#[derive(Debug, Clone, Default)]
pub struct CommaSeparatedList<T> {
data: Vec<T>,
}
impl<T> CommaSeparatedList<T> {
/// Create a new, empty comma-separated list.
pub fn new() -> Self {
Self {
data: Vec::new(),
}
}
}
impl<T> From<Vec<T>> for CommaSeparatedList<T> {
fn from(data: Vec<T>) -> Self {
Self {
data,
}
}
}
impl<T> iter::FromIterator<T> for CommaSeparatedList<T> {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = T>,
{
Self {
data: iter.into_iter().collect(),
}
}
}
impl<T> ops::Deref for CommaSeparatedList<T> {
type Target = Vec<T>;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T> ops::DerefMut for CommaSeparatedList<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}
impl<T> fmt::Display for CommaSeparatedList<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.data.iter().format(","))
}
}
impl<'a, T> ParamValue<'a> for CommaSeparatedList<T>
where
T: ParamValue<'a>,
{
fn as_value(&self) -> Cow<'a, str> {
format!("{}", self.data.iter().map(|d| d.as_value()).format(",")).into()
}
}
impl<'a, 'b, T> ParamValue<'a> for &'b CommaSeparatedList<T>
where
T: ParamValue<'a>,
{
fn as_value(&self) -> Cow<'a, str> {
format!("{}", self.data.iter().map(|d| d.as_value()).format(",")).into()
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use std::cmp;
use std::iter;
use crate::api::common::{
AccessLevel, EnableState, NameOrId, ProtectedAccessLevel, SortOrder, VisibilityLevel, YesNo,
AccessLevel, CommaSeparatedList, EnableState, NameOrId, ProtectedAccessLevel, SortOrder,
VisibilityLevel, YesNo,
};
use crate::api::params::ParamValue;
#[test]
fn access_level_as_str() {
let items = &[
(AccessLevel::Anonymous, "anonymous", 0),
(AccessLevel::Minimal, "minimal", 5),
(AccessLevel::Guest, "guest", 10),
(AccessLevel::Reporter, "reporter", 20),
(AccessLevel::Developer, "developer", 30),
......@@ -484,4 +571,42 @@ mod tests {
assert_eq!(i.as_str(), *s);
}
}
#[test]
fn comma_separated_list_default() {
let csl = CommaSeparatedList::<u64>::default();
assert!(csl.is_empty());
}
#[test]
fn comma_separated_list_vec() {
let csl = CommaSeparatedList::<u64>::new();
let _: &Vec<u64> = &csl;
}
#[test]
fn comma_separated_list_from_iter() {
let _: CommaSeparatedList<_> = iter::once(2).collect();
}
#[test]
fn comma_separated_list_display() {
let csl_one: CommaSeparatedList<_> = iter::once(2).collect();
assert_eq!(format!("{}", csl_one), "2");
let csl_two: CommaSeparatedList<_> = [1, 2].iter().copied().collect();
assert_eq!(format!("{}", csl_two), "1,2");
}
#[test]
fn comma_separated_list_param_value() {
let csl_one: CommaSeparatedList<_> = iter::once(2).collect();
assert_eq!(csl_one.as_value(), "2");
let csl_two: CommaSeparatedList<_> = [1, 2].iter().copied().collect();
assert_eq!(csl_two.as_value(), "1,2");
let csl_str_one: CommaSeparatedList<Cow<str>> = iter::once("one".into()).collect();
assert_eq!(csl_str_one.as_value(), "one");
let csl_str_two: CommaSeparatedList<Cow<str>> =
["one".into(), "two".into()].iter().cloned().collect();
assert_eq!(csl_str_two.as_value(), "one,two");
}
}
......@@ -22,6 +22,7 @@ pub use create::BranchProtection;
pub use create::CreateGroup;
pub use create::CreateGroupBuilder;
pub use create::GroupProjectCreationAccessLevel;
pub use create::SharedRunnersMinutesLimit;
pub use create::SubgroupCreationAccessLevel;
pub use group::Group;
......
......@@ -32,7 +32,7 @@ impl GroupProjectCreationAccessLevel {
}
impl ParamValue<'static> for GroupProjectCreationAccessLevel {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
......@@ -56,7 +56,7 @@ impl SubgroupCreationAccessLevel {
}
impl ParamValue<'static> for SubgroupCreationAccessLevel {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
......@@ -83,11 +83,48 @@ impl BranchProtection {
}
impl ParamValue<'static> for BranchProtection {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
/// Settings for a group's shared runner minute allocation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SharedRunnersMinutesLimit {
/// Inherit the setting from the parent group or instance.
Inherit,
/// Unlimited shared minutes are allowed.
Unlimited,
/// A set number of minutes are allowed.
Minutes(u64),
}
impl SharedRunnersMinutesLimit {
fn as_str(self) -> Cow<'static, str> {
match self {
SharedRunnersMinutesLimit::Inherit => "nil".into(),
SharedRunnersMinutesLimit::Unlimited => "0".into(),
SharedRunnersMinutesLimit::Minutes(m) => format!("{}", m).into(),
}
}
}
impl From<u64> for SharedRunnersMinutesLimit {
fn from(i: u64) -> Self {
if i == 0 {
Self::Unlimited
} else {
Self::Minutes(i)
}
}
}
impl ParamValue<'static> for SharedRunnersMinutesLimit {
fn as_value(&self) -> Cow<'static, str> {
self.as_str()
}
}
/// Create a new group on an instance.
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
......@@ -148,8 +185,8 @@ pub struct CreateGroup<'a> {
#[builder(default)]
default_branch_protection: Option<BranchProtection>,
/// Pipeline quota (in minutes) for the group on shared runners.
#[builder(default)]
shared_runners_minutes_limit: Option<u64>,
#[builder(setter(into), default)]
shared_runners_minutes_limit: Option<SharedRunnersMinutesLimit>,
/// Pipeline quota excess (in minutes) for the group on shared runners.
#[builder(default)]
extra_shared_runners_minutes_limit: Option<u64>,
......@@ -214,7 +251,8 @@ mod tests {
use crate::api::common::VisibilityLevel;
use crate::api::groups::{
BranchProtection, CreateGroup, GroupProjectCreationAccessLevel, SubgroupCreationAccessLevel,
BranchProtection, CreateGroup, GroupProjectCreationAccessLevel, SharedRunnersMinutesLimit,
SubgroupCreationAccessLevel,
};
use crate::api::{self, Query};
use crate::test::client::{ExpectedUrl, SingleTestClient};
......@@ -257,6 +295,21 @@ mod tests {
}
}
#[test]
fn shared_runners_minutes_limit_as_str() {
let items = &[
(SharedRunnersMinutesLimit::Inherit, "nil"),
(SharedRunnersMinutesLimit::Unlimited, "0"),
(SharedRunnersMinutesLimit::Minutes(10), "10"),
(SharedRunnersMinutesLimit::Minutes(24), "24"),
(15.into(), "15"),
];
for (i, s) in items {
assert_eq!(i.as_str(), *s);
}
}
#[test]
fn name_and_path_are_necessary() {
let err = CreateGroup::builder().build().unwrap_err();
......@@ -645,6 +698,30 @@ mod tests {
#[test]
fn endpoint_shared_runners_minutes_limit() {
let endpoint = ExpectedUrl::builder()
.method(Method::POST)
.endpoint("groups")
.content_type("application/x-www-form-urlencoded")
.body_str(concat!(
"name=name",
"&path=path",
"&shared_runners_minutes_limit=0",
))
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = CreateGroup::builder()
.name("name")
.path("path")
.shared_runners_minutes_limit(SharedRunnersMinutesLimit::Unlimited)
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_shared_runners_minutes_limit_into() {
let endpoint = ExpectedUrl::builder()
.method(Method::POST)
.endpoint("groups")
......
......@@ -41,7 +41,7 @@ impl GroupOrderBy {
}
impl ParamValue<'static> for GroupOrderBy {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
......@@ -71,6 +71,9 @@ pub struct Groups<'a> {
/// Filter groups by those where the API caller has a minimum access level.
#[builder(default)]
min_access_level: Option<AccessLevel>,
/// Only return top-level groups.
#[builder(default)]
top_level_only: Option<bool>,
/// Include project statistics in the results.
#[builder(default)]
......@@ -140,6 +143,7 @@ impl<'a> Endpoint for Groups<'a> {
"min_access_level",
self.min_access_level.map(|level| level.as_u64()),
)
.push_opt("top_level_only", self.top_level_only)
.push_opt("statistics", self.statistics)
.push_opt("with_custom_attributes", self.with_custom_attributes)
.push_opt("order_by", self.order_by)
......@@ -262,6 +266,19 @@ mod tests {
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_top_level_only() {
let endpoint = ExpectedUrl::builder()
.endpoint("groups")
.add_query_params(&[("top_level_only", "true")])
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = Groups::builder().top_level_only(true).build().unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_statistics() {
let endpoint = ExpectedUrl::builder()
......
......@@ -23,6 +23,8 @@ pub enum GroupProjectsOrderBy {
CreatedAt,
/// Order by the last updated date of the project.
UpdatedAt,
/// Order by a similarity score based on the search.
Similarity,
/// Order by the last activity date of the project.
LastActivityAt,
}
......@@ -42,13 +44,14 @@ impl GroupProjectsOrderBy {
GroupProjectsOrderBy::Path => "path",
GroupProjectsOrderBy::CreatedAt => "created_at",
GroupProjectsOrderBy::UpdatedAt => "updated_at",
GroupProjectsOrderBy::Similarity => "similarity",
GroupProjectsOrderBy::LastActivityAt => "last_activity_at",
}
}
}
impl ParamValue<'static> for GroupProjectsOrderBy {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
......@@ -183,6 +186,7 @@ mod tests {
(GroupProjectsOrderBy::Path, "path"),
(GroupProjectsOrderBy::CreatedAt, "created_at"),
(GroupProjectsOrderBy::UpdatedAt, "updated_at"),
(GroupProjectsOrderBy::Similarity, "similarity"),
(GroupProjectsOrderBy::LastActivityAt, "last_activity_at"),
];
......
......@@ -41,7 +41,7 @@ impl GroupSubgroupsOrderBy {
}
impl ParamValue<'static> for GroupSubgroupsOrderBy {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
......
......@@ -11,10 +11,8 @@
//! enough" away from their usage to make `super::` access inconvenient.
use std::borrow::Cow;
use std::collections::BTreeSet;
use itertools::Itertools;
use crate::api::common::CommaSeparatedList;
use crate::api::ParamValue;
/// Keys note results may be ordered by.
......@@ -42,7 +40,7 @@ impl NoteOrderBy {
}
impl ParamValue<'static> for NoteOrderBy {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
......@@ -51,7 +49,7 @@ impl ParamValue<'static> for NoteOrderBy {
pub(crate) enum Labels<'a> {
Any,
None,
AllOf(BTreeSet<Cow<'a, str>>),
AllOf(CommaSeparatedList<Cow<'a, str>>),
}
impl<'a> Labels<'a> {
......@@ -59,13 +57,13 @@ impl<'a> Labels<'a> {
match self {
Labels::Any => "Any".into(),
Labels::None => "None".into(),
Labels::AllOf(labels) => format!("{}", labels.iter().format(",")).into(),
Labels::AllOf(labels) => format!("{}", labels).into(),
}
}
}
impl<'a, 'b: 'a> ParamValue<'static> for &'b Labels<'a> {
fn as_value(self) -> Cow<'static, str> {
fn as_value(&self) -> Cow<'static, str> {
self.as_str()
}
}
......@@ -88,7 +86,7 @@ impl<'a> Milestone<'a> {
}
impl<'a, 'b: 'a> ParamValue<'a> for &'b Milestone<'a> {
fn as_value(self) -> Cow<'a, str> {
fn as_value(&self) -> Cow<'a, str> {
self.as_str().into()
}
}
......@@ -111,14 +109,14 @@ impl<'a> ReactionEmoji<'a> {
}
impl<'a, 'b: 'a> ParamValue<'a> for &'b ReactionEmoji<'a> {
fn as_value(self) -> Cow<'a, str> {
fn as_value(&self) -> Cow<'a, str> {
self.as_str().into()
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::iter;
use super::{Labels, Milestone, NoteOrderBy, ReactionEmoji};
......@@ -141,17 +139,8 @@ mod tests {
#[test]
fn labels_as_str() {
let one_user = {
let mut set = BTreeSet::new();
set.insert("one".into());
set
};
let two_users = {
let mut set = BTreeSet::new();
set.insert("one".into());
set.insert("two".into());
set
};
let one_user = iter::once("one".into()).collect();
let two_users = ["one".into(), "two".into()].iter().cloned().collect();
let items = &[
(Labels::Any, "Any"),
......