diff --git a/src/GitLabApiClient/GitLabClient.cs b/src/GitLabApiClient/GitLabClient.cs index 0259842e..84d51ffd 100644 --- a/src/GitLabApiClient/GitLabClient.cs +++ b/src/GitLabApiClient/GitLabClient.cs @@ -36,13 +36,13 @@ public GitLabClient(string hostUrl, string authenticationToken = "") authenticationToken); var projectQueryBuilder = new ProjectsQueryBuilder(); - var projectIssuesQueryBuilder = new ProjectIssuesQueryBuilder(); var projectIssueNotesQueryBuilder = new ProjectIssueNotesQueryBuilder(); var issuesQueryBuilder = new IssuesQueryBuilder(); var mergeRequestsQueryBuilder = new MergeRequestsQueryBuilder(); var projectMilestonesQueryBuilder = new MilestonesQueryBuilder(); var projectMergeRequestsQueryBuilder = new ProjectMergeRequestsQueryBuilder(); var groupsQueryBuilder = new GroupsQueryBuilder(); + var groupLabelsQueryBuilder = new GroupLabelsQueryBuilder(); var projectsGroupsQueryBuilder = new ProjectsGroupQueryBuilder(); var branchQueryBuilder = new BranchQueryBuilder(); var releaseQueryBuilder = new ReleaseQueryBuilder(); @@ -52,12 +52,12 @@ public GitLabClient(string hostUrl, string authenticationToken = "") var pipelineQueryBuilder = new PipelineQueryBuilder(); var treeQueryBuilder = new TreeQueryBuilder(); - Issues = new IssuesClient(_httpFacade, issuesQueryBuilder, projectIssuesQueryBuilder, projectIssueNotesQueryBuilder); + Issues = new IssuesClient(_httpFacade, issuesQueryBuilder, projectIssueNotesQueryBuilder); Uploads = new UploadsClient(_httpFacade); MergeRequests = new MergeRequestsClient(_httpFacade, mergeRequestsQueryBuilder, projectMergeRequestsQueryBuilder); Projects = new ProjectsClient(_httpFacade, projectQueryBuilder, projectMilestonesQueryBuilder); Users = new UsersClient(_httpFacade); - Groups = new GroupsClient(_httpFacade, groupsQueryBuilder, projectsGroupsQueryBuilder, projectMilestonesQueryBuilder); + Groups = new GroupsClient(_httpFacade, groupsQueryBuilder, projectsGroupsQueryBuilder, projectMilestonesQueryBuilder, groupLabelsQueryBuilder); Branches = new BranchClient(_httpFacade, branchQueryBuilder); Releases = new ReleaseClient(_httpFacade, releaseQueryBuilder); Tags = new TagClient(_httpFacade, tagQueryBuilder); diff --git a/src/GitLabApiClient/GroupsClient.cs b/src/GitLabApiClient/GroupsClient.cs index 9a0fc584..3f157078 100644 --- a/src/GitLabApiClient/GroupsClient.cs +++ b/src/GitLabApiClient/GroupsClient.cs @@ -26,24 +26,27 @@ public sealed class GroupsClient private readonly GroupsQueryBuilder _queryBuilder; private readonly ProjectsGroupQueryBuilder _projectsQueryBuilder; private readonly MilestonesQueryBuilder _queryMilestonesBuilder; + private readonly GroupLabelsQueryBuilder _queryGroupLabelBuilder; internal GroupsClient( GitLabHttpFacade httpFacade, GroupsQueryBuilder queryBuilder, ProjectsGroupQueryBuilder projectsQueryBuilder, - MilestonesQueryBuilder queryMilestonesBuilder) + MilestonesQueryBuilder queryMilestonesBuilder, + GroupLabelsQueryBuilder queryGroupLabelBuilder) { _httpFacade = httpFacade; _queryBuilder = queryBuilder; _projectsQueryBuilder = projectsQueryBuilder; _queryMilestonesBuilder = queryMilestonesBuilder; + _queryGroupLabelBuilder = queryGroupLabelBuilder; } /// /// Get all details of a group. /// This endpoint can be accessed without authentication if the group is publicly accessible. /// - /// The ID, path or of the project. + /// The ID, path or of the group. public async Task GetAsync(GroupId groupId) => await _httpFacade.Get($"groups/{groupId}"); @@ -51,7 +54,7 @@ public async Task GetAsync(GroupId groupId) => /// Get all subgroups of a group. /// This endpoint can be accessed without authentication if the group is publicly accessible. /// - /// The ID, path or of the project. + /// The ID, path or of the group. public async Task> GetSubgroupsAsync(GroupId groupId) => await _httpFacade.GetPagedList($"groups/{groupId}/subgroups"); @@ -80,7 +83,7 @@ public async Task> GetAsync(Action options = nu /// Get a list of projects in this group. /// When accessed without authentication, only public projects are returned. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// Groups projects retrieval options. /// Issues satisfying options. public async Task> GetProjectsAsync(GroupId groupId, Action options = null) @@ -95,7 +98,7 @@ public async Task> GetProjectsAsync(GroupId groupId, Action /// Get a list of members in this group. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// A query string to search for members. /// Group members satisfying options. public async Task> GetMembersAsync(GroupId groupId, string search = null) @@ -113,7 +116,7 @@ public async Task> GetMembersAsync(GroupId groupId, string search /// /// Get a list of all members (including inherited) in this group. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// A query string to search for members. /// Group members satisfying options. public async Task> GetAllMembersAsync(GroupId groupId, string search = null) @@ -131,7 +134,7 @@ public async Task> GetAllMembersAsync(GroupId groupId, string sear /// /// Adds a member to the group. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// Create milestone request. /// Newly created milestone. public async Task CreateMilestoneAsync(GroupId groupId, CreateGroupMilestoneRequest request) @@ -143,7 +146,7 @@ public async Task CreateMilestoneAsync(GroupId groupId, CreateGroupMi /// /// Get a list of milestones in this group. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// Query options. public async Task> GetMilestonesAsync(GroupId groupId, Action options = null) { @@ -157,7 +160,7 @@ public async Task> GetMilestonesAsync(GroupId groupId, Action /// Retrieves a group milestone by its id. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// Id of the milestone. public async Task GetMilestoneAsync(GroupId groupId, int milestoneId) => await _httpFacade.Get($"groups/{groupId}/milestones/{milestoneId}"); @@ -174,7 +177,7 @@ public async Task CreateAsync(CreateGroupRequest request) => /// /// Adds a user to a group. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// Add group member request. /// Newly created membership. public async Task AddMemberAsync(GroupId groupId, AddGroupMemberRequest request) @@ -186,7 +189,7 @@ public async Task AddMemberAsync(GroupId groupId, AddGroupMemberRequest /// /// Updates a user's group membership. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// The user ID of the member. /// Update group member request. /// Updated membership. @@ -199,7 +202,7 @@ public async Task UpdateMemberAsync(GroupId groupId, int userId, AddGrou /// /// Removes a user as a member of the group. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// The user ID of the member. public async Task RemoveMemberAsync(GroupId groupId, int userId) => await _httpFacade.Delete($"groups/{groupId}/members/{userId}"); @@ -207,7 +210,7 @@ public async Task RemoveMemberAsync(GroupId groupId, int userId) => /// /// Transfer a project to the Group namespace. Available only for admin /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// The ID, path or of the project. /// The newly updated group. public async Task TransferAsync(GroupId groupId, ProjectId projectId) => @@ -218,7 +221,7 @@ public async Task TransferAsync(GroupId groupId, ProjectId projectId) => /// Only available to group owners and administrators. /// /// The updated group. - /// The ID, path or of the project. + /// The ID, path or of the group. /// Update group request. public async Task UpdateAsync(GroupId groupId, UpdateGroupRequest request) => await _httpFacade.Put($"groups/{groupId}", request); @@ -226,7 +229,7 @@ public async Task UpdateAsync(GroupId groupId, UpdateGroupRequest request /// /// Updates an existing group milestone. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// The ID of the group's milestone. /// Update milestone request. /// Newly modified milestone. @@ -240,14 +243,14 @@ public async Task UpdateMilestoneAsync(GroupId groupId, int milestone /// Removes group with all projects inside. /// Only available to group owners and administrators. /// - /// The ID, path or of the project. + /// The ID, path or of the group. public async Task DeleteAsync(GroupId groupId) => await _httpFacade.Delete($"groups/{groupId}"); /// /// Deletes a group milestone. Only for user with developer access to the group. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// The ID of the group's milestone. public async Task DeleteMilestoneAsync(GroupId groupId, int milestoneId) => await _httpFacade.Delete($"groups/{groupId}/milestones/{milestoneId}"); @@ -256,14 +259,14 @@ public async Task DeleteMilestoneAsync(GroupId groupId, int milestoneId) => /// Syncs the group with its linked LDAP group. /// Only available to group owners and administrators. /// - /// The ID, path or of the project. + /// The ID, path or of the group. public async Task SyncLdapAsync(GroupId groupId) => await _httpFacade.Post($"groups/{groupId}/ldap_sync"); /// /// Creates LDAP group link. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// Create LDAP group link request. public async Task CreateLdapLinkAsync(GroupId groupId, CreateLdapGroupLinkRequest request) => await _httpFacade.Post($"groups/{groupId}/ldap_group_links", request); @@ -271,7 +274,7 @@ public async Task CreateLdapLinkAsync(GroupId groupId, CreateLdapGroupLinkReques /// /// Deletes a LDAP group link. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// The CN of a LDAP group public async Task DeleteLdapLinkAsync(GroupId groupId, string cn) => await _httpFacade.Delete($"groups/{groupId}/ldap_group_links/{cn}"); @@ -280,10 +283,58 @@ public async Task DeleteLdapLinkAsync(GroupId groupId, string cn) => /// /// Deletes a LDAP group link for a specific LDAP provider. /// - /// The ID, path or of the project. + /// The ID, path or of the group. /// Name of a LDAP provider /// The CN of a LDAP group public async Task DeleteProviderLdapLinkAsync(GroupId groupId, string provider, string cn) => await _httpFacade.Delete($"groups/{groupId}/ldap_group_links/{provider}/{cn}"); + + + /// + /// Get all labels for a given group. + /// + /// The ID, path or of the group. + /// Query options + public async Task> GetLabelsAsync(GroupId groupId, + Action options = null) + { + var labelOptions = new GroupLabelsQueryOptions(); + options?.Invoke(labelOptions); + + string url = _queryGroupLabelBuilder.Build($"groups/{groupId}/labels", labelOptions); + return await _httpFacade.GetPagedList(url); + } + + /// + /// Creates new group label. + /// + /// The ID, path or of the group. + /// Create label request. + /// Newly created label. + public async Task CreateLabelAsync(GroupId groupId, CreateGroupLabelRequest request) + { + Guard.NotNull(request, nameof(request)); + return await _httpFacade.Post($"groups/{groupId}/labels", request); + } + + /// + /// Updates an existing label with new name or new color. At least one parameter is required, to update the label. + /// + /// The ID, path or of the group. + /// Update label request. + /// Newly modified label. + public async Task UpdateLabelAsync(GroupId groupId, UpdateGroupLabelRequest request) + { + Guard.NotNull(request, nameof(request)); + return await _httpFacade.Put($"groups/{groupId}/labels", request); + } + + /// + /// Deletes group labels. + /// + /// The ID, path or of the group. + /// Name of the label. + public async Task DeleteLabelAsync(GroupId groupId, string name) => + await _httpFacade.Delete($"groups/{groupId}/labels?name={name}"); } } diff --git a/src/GitLabApiClient/Internal/Queries/GroupLabelsQueryBuilder.cs b/src/GitLabApiClient/Internal/Queries/GroupLabelsQueryBuilder.cs new file mode 100644 index 00000000..0126eabe --- /dev/null +++ b/src/GitLabApiClient/Internal/Queries/GroupLabelsQueryBuilder.cs @@ -0,0 +1,25 @@ +using GitLabApiClient.Models.Groups.Requests; + +namespace GitLabApiClient.Internal.Queries +{ + internal class GroupLabelsQueryBuilder : QueryBuilder + { + #region Overrides of QueryBuilder + + /// + protected override void BuildCore(GroupLabelsQueryOptions options) + { + if (options.WithCounts) + { + Add("with_counts", true); + } + + if (!options.IncludeAncestorGroups) + { + Add("include_ancestor_groups", false); + } + } + + #endregion + } +} diff --git a/src/GitLabApiClient/Internal/Queries/IssuesQueryBuilder.cs b/src/GitLabApiClient/Internal/Queries/IssuesQueryBuilder.cs index 21c05fbe..96748f7f 100644 --- a/src/GitLabApiClient/Internal/Queries/IssuesQueryBuilder.cs +++ b/src/GitLabApiClient/Internal/Queries/IssuesQueryBuilder.cs @@ -39,6 +39,21 @@ protected override void BuildCore(IssuesQueryOptions options) if (!options.Filter.IsNullOrEmpty()) Add("search", options.Filter); + + if (options.IsConfidential) + Add("confidential", true); + + if (options.CreatedBefore.HasValue) + Add("created_before", options.CreatedBefore.Value); + + if (options.CreatedAfter.HasValue) + Add("created_after", options.CreatedAfter.Value); + + if (options.UpdatedBefore.HasValue) + Add("updated_before", options.UpdatedBefore.Value); + + if (options.UpdatedAfter.HasValue) + Add("updated_after", options.UpdatedAfter.Value); } private static string GetStateQueryValue(IssueState state) diff --git a/src/GitLabApiClient/Internal/Queries/ProjectIssuesQueryBuilder.cs b/src/GitLabApiClient/Internal/Queries/ProjectIssuesQueryBuilder.cs deleted file mode 100644 index 79957163..00000000 --- a/src/GitLabApiClient/Internal/Queries/ProjectIssuesQueryBuilder.cs +++ /dev/null @@ -1,24 +0,0 @@ -using GitLabApiClient.Models.Issues.Requests; - -namespace GitLabApiClient.Internal.Queries -{ - internal sealed class ProjectIssuesQueryBuilder : IssuesQueryBuilder - { - protected override void BuildCore(IssuesQueryOptions options) - { - if (!(options is ProjectIssuesQueryOptions projectIssuesQueryOptions)) - { - base.BuildCore(options); - return; - } - - base.BuildCore(options); - - if (projectIssuesQueryOptions.CreatedAfter.HasValue) - Add("created_after", projectIssuesQueryOptions.CreatedAfter.Value); - - if (projectIssuesQueryOptions.CreatedBefore.HasValue) - Add("created_before", projectIssuesQueryOptions.CreatedBefore.Value); - } - } -} diff --git a/src/GitLabApiClient/IssuesClient.cs b/src/GitLabApiClient/IssuesClient.cs index 3814b63a..5ac8cf84 100644 --- a/src/GitLabApiClient/IssuesClient.cs +++ b/src/GitLabApiClient/IssuesClient.cs @@ -5,6 +5,7 @@ using GitLabApiClient.Internal.Http; using GitLabApiClient.Internal.Paths; using GitLabApiClient.Internal.Queries; +using GitLabApiClient.Models.Groups.Responses; using GitLabApiClient.Models.Issues.Requests; using GitLabApiClient.Models.Issues.Responses; using GitLabApiClient.Models.Notes.Requests; @@ -23,21 +24,76 @@ public sealed class IssuesClient { private readonly GitLabHttpFacade _httpFacade; private readonly IssuesQueryBuilder _queryBuilder; - private readonly ProjectIssuesQueryBuilder _projectIssuesQueryBuilder; private readonly ProjectIssueNotesQueryBuilder _projectIssueNotesQueryBuilder; internal IssuesClient( GitLabHttpFacade httpFacade, IssuesQueryBuilder queryBuilder, - ProjectIssuesQueryBuilder projectIssuesQueryBuilder, ProjectIssueNotesQueryBuilder projectIssueNotesQueryBuilder) { _httpFacade = httpFacade; _queryBuilder = queryBuilder; - _projectIssuesQueryBuilder = projectIssuesQueryBuilder; _projectIssueNotesQueryBuilder = projectIssueNotesQueryBuilder; } + /// + /// Retrieves issues. + /// By default retrieves opened issues from all users. The more specific setting win (if both project and group are set, only project issues will be retrieved). + /// + /// + /// + /// /* Get all issues */ + /// var client = new GitLabClient("https://gitlab.com", "PRIVATE-TOKEN"); + /// var allIssues = await client.Issues.GetAllAsync(); + /// + /// + /// /* Get project issues */ + /// var client = new GitLabClient("https://gitlab.com", "PRIVATE-TOKEN"); + /// string projectPath = "dev/group/project-1"; + /// var allIssues = await client.Issues.GetAllAsync(projectId: projectPath); + /// // OR + /// int projectId = 55; + /// var allIssues = await client.Issues.GetAllAsync(projectId: projectId); + /// // OR - Group ID is skipped, project ID is more specific + /// int projectId = 55; + /// int groupId = 181; + /// var allIssues = await client.Issues.GetAllAsync(projectId: projectId, groupId: groupId); + /// + /// + /// /* Get group issues */ + /// var client = new GitLabClient("https://gitlab.com", "PRIVATE-TOKEN"); + /// string groupPath = "dev/group1/subgroup-1"; + /// var allIssues = await client.Issues.GetAllAsync(groupId: groupPath); + /// // OR + /// int groupId = 55; + /// var allIssues = await client.Issues.GetAllAsync(groupId: groupId); + /// + /// + /// The ID, path or of the project. + /// The ID, path or of the group. + /// Issues retrieval options. + /// Issues satisfying options. + public async Task> GetAllAsync(ProjectId projectId = null, GroupId groupId = null, + Action options = null) + { + var queryOptions = new IssuesQueryOptions(); + options?.Invoke(queryOptions); + + string path = "issues"; + if (projectId != null) + { + path = $"projects/{projectId}/issues"; + } + else if (groupId != null) + { + path = $"groups/{groupId}/issues"; + } + + string url = _queryBuilder.Build(path, queryOptions); + + return await _httpFacade.GetPagedList(url); + } + /// /// Retrieves project issue. /// @@ -51,14 +107,9 @@ public async Task GetAsync(ProjectId projectId, int issueId) => /// The ID, path or of the project. /// Issues retrieval options. /// Issues satisfying options. - public async Task> GetAsync(ProjectId projectId, Action options = null) - { - var queryOptions = new ProjectIssuesQueryOptions(); - options?.Invoke(queryOptions); - - string url = _projectIssuesQueryBuilder.Build($"projects/{projectId}/issues", queryOptions); - return await _httpFacade.GetPagedList(url); - } + [Obsolete("Use GetAllAsync instead")] + public Task> GetAsync(ProjectId projectId, Action options = null) => + GetAllAsync(projectId: projectId, options: options); /// /// Retrieves issues from all projects. @@ -66,14 +117,9 @@ public async Task> GetAsync(ProjectId projectId, Action /// Issues retrieval options. /// Issues satisfying options. - public async Task> GetAsync(Action options = null) - { - var queryOptions = new IssuesQueryOptions(); - options?.Invoke(queryOptions); - - string url = _queryBuilder.Build("issues", queryOptions); - return await _httpFacade.GetPagedList(url); - } + [Obsolete("Use GetAllAsync instead")] + public Task> GetAsync(Action options = null) => + GetAllAsync(options: options); /// /// Retrieves project issue note. diff --git a/src/GitLabApiClient/Models/Groups/Requests/CreateGroupLabelRequest.cs b/src/GitLabApiClient/Models/Groups/Requests/CreateGroupLabelRequest.cs new file mode 100644 index 00000000..78d6b1aa --- /dev/null +++ b/src/GitLabApiClient/Models/Groups/Requests/CreateGroupLabelRequest.cs @@ -0,0 +1,39 @@ +using GitLabApiClient.Internal.Utilities; +using Newtonsoft.Json; + +namespace GitLabApiClient.Models.Groups.Requests +{ + /// + /// Used to create labels in a group. + /// + public sealed class CreateGroupLabelRequest + { + /// + /// Initializes a new instance of the class. + /// + /// Name of the label. + public CreateGroupLabelRequest(string name) + { + Guard.NotEmpty(name, nameof(name)); + Name = name; + } + + /// + /// The name of the label. + /// + [JsonProperty("name")] + public string Name { get; private set; } + + /// + /// The color of the label given in 6-digit hex notation with leading ‘#’ sign (e.g. #FFAABB) or one of the CSS color names. + /// + [JsonProperty("color")] + public string Color { get; set; } + + /// + /// The description of the label. + /// + [JsonProperty("description")] + public string Description { get; set; } + } +} diff --git a/src/GitLabApiClient/Models/Groups/Requests/GroupLabelsQueryOptions.cs b/src/GitLabApiClient/Models/Groups/Requests/GroupLabelsQueryOptions.cs new file mode 100644 index 00000000..c50503c8 --- /dev/null +++ b/src/GitLabApiClient/Models/Groups/Requests/GroupLabelsQueryOptions.cs @@ -0,0 +1,22 @@ +namespace GitLabApiClient.Models.Groups.Requests +{ + /// + /// Options for group label list + /// + public sealed class GroupLabelsQueryOptions + { + internal GroupLabelsQueryOptions() + { + } + + /// + /// Whether or not to include issue and merge request counts. Defaults to `false`. (Introduced in GitLab 12.2) + /// + public bool WithCounts { get; set; } = false; + + /// + /// Include ancestor groups. Defaults to `true`. + /// + public bool IncludeAncestorGroups { get; set; } = true; + } +} diff --git a/src/GitLabApiClient/Models/Groups/Requests/UpdateGroupLabelRequest.cs b/src/GitLabApiClient/Models/Groups/Requests/UpdateGroupLabelRequest.cs new file mode 100644 index 00000000..68b7476d --- /dev/null +++ b/src/GitLabApiClient/Models/Groups/Requests/UpdateGroupLabelRequest.cs @@ -0,0 +1,69 @@ +using GitLabApiClient.Internal.Utilities; +using Newtonsoft.Json; + +namespace GitLabApiClient.Models.Groups.Requests +{ + /// + /// Updates an existing label with new name or new color. At least one parameter is required, to update the label. + /// + public sealed class UpdateGroupLabelRequest + { + /// + /// Initializes a new instance of the class. + /// Old name of the label. + /// The new name of the label. + /// + public static UpdateGroupLabelRequest FromNewName(string name, string newName) + { + Guard.NotEmpty(name, nameof(name)); + Guard.NotEmpty(newName, nameof(newName)); + return new UpdateGroupLabelRequest + { + Name = name, + NewName = newName + }; + } + + /// + /// Initializes a new instance of the class. + /// Old name of the label. + /// The color of the label given in 6-digit hex notation with leading ‘#’ sign (e.g. #FFAABB) or one of the CSS color names. + /// + public static UpdateGroupLabelRequest FromColor(string name, string color) + { + Guard.NotEmpty(name, nameof(name)); + Guard.NotEmpty(color, nameof(color)); + return new UpdateGroupLabelRequest + { + Name = name, + Color = color + }; + } + + private UpdateGroupLabelRequest() { } + + /// + /// The name of the existing label. + /// + [JsonProperty("name")] + public string Name { get; private set; } + + /// + /// The new name of the label + /// + [JsonProperty("new_name")] + public string NewName { get; private set; } + + /// + /// The color of the label given in 6-digit hex notation with leading ‘#’ sign (e.g. #FFAABB) or one of the CSS color names. + /// + [JsonProperty("color")] + public string Color { get; set; } + + /// + /// The description of the label. + /// + [JsonProperty("description")] + public string Description { get; set; } + } +} diff --git a/src/GitLabApiClient/Models/Groups/Responses/GroupLabel.cs b/src/GitLabApiClient/Models/Groups/Responses/GroupLabel.cs new file mode 100644 index 00000000..66166a67 --- /dev/null +++ b/src/GitLabApiClient/Models/Groups/Responses/GroupLabel.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; + +namespace GitLabApiClient.Models.Groups.Responses +{ + public sealed class GroupLabel + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("color")] + public string Color { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("open_issues_count")] + public int OpenIssuesCount { get; set; } + + [JsonProperty("closed_issues_count")] + public int ClosedIssuesCount { get; set; } + + [JsonProperty("open_merge_requests_count")] + public int OpenMergeRequestsCount { get; set; } + + [JsonProperty("subscribed")] + public bool Subscribed { get; set; } + } +} diff --git a/src/GitLabApiClient/Models/Issues/Requests/IssuesQueryOptions.cs b/src/GitLabApiClient/Models/Issues/Requests/IssuesQueryOptions.cs index 87c79a7f..7a24c0cf 100644 --- a/src/GitLabApiClient/Models/Issues/Requests/IssuesQueryOptions.cs +++ b/src/GitLabApiClient/Models/Issues/Requests/IssuesQueryOptions.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using GitLabApiClient.Models.Issues.Responses; @@ -54,7 +55,7 @@ internal IssuesQueryOptions() { } public IssuesOrder Order { get; set; } /// - /// Specifies project sort order. Default is desending. + /// Specifies project sort order. Default is descending. /// public SortOrder SortOrder { get; set; } @@ -62,5 +63,30 @@ internal IssuesQueryOptions() { } /// Search issues against their title and description /// public string Filter { get; set; } + + /// + /// Return issues created after the given time (inclusive) + /// + public DateTime? CreatedAfter { get; set; } + + /// + /// Return issues created before the given time (inclusive) + /// + public DateTime? CreatedBefore { get; set; } + + /// + /// Return issues updated on or after the given time + /// + public DateTime? UpdatedAfter { get; set; } + + /// + /// Return issues updated on or before the given time + /// + public DateTime? UpdatedBefore { get; set; } + + /// + /// Filter confidential or public issues. + /// + public bool IsConfidential { get; set; } = false; } } diff --git a/src/GitLabApiClient/Models/Issues/Requests/ProjectIssuesQueryOptions.cs b/src/GitLabApiClient/Models/Issues/Requests/ProjectIssuesQueryOptions.cs index 06b1d754..6f5f6bf6 100644 --- a/src/GitLabApiClient/Models/Issues/Requests/ProjectIssuesQueryOptions.cs +++ b/src/GitLabApiClient/Models/Issues/Requests/ProjectIssuesQueryOptions.cs @@ -1,5 +1,4 @@ using System; -using GitLabApiClient.Internal.Utilities; namespace GitLabApiClient.Models.Issues.Requests { @@ -7,6 +6,7 @@ namespace GitLabApiClient.Models.Issues.Requests /// /// Options for project issues listing /// + [Obsolete("Use IssuesQueryOptions instead")] public sealed class ProjectIssuesQueryOptions : IssuesQueryOptions { /// @@ -15,15 +15,5 @@ public sealed class ProjectIssuesQueryOptions : IssuesQueryOptions internal ProjectIssuesQueryOptions() { } - - /// - /// Return issues created after the given time (inclusive) - /// - public DateTime? CreatedAfter { get; set; } - - /// - /// Return issues created before the given time (inclusive) - /// - public DateTime? CreatedBefore { get; set; } } } diff --git a/test/GitLabApiClient.Test/GroupsClientTest.cs b/test/GitLabApiClient.Test/GroupsClientTest.cs index f2951e14..140a515e 100644 --- a/test/GitLabApiClient.Test/GroupsClientTest.cs +++ b/test/GitLabApiClient.Test/GroupsClientTest.cs @@ -4,8 +4,10 @@ using FluentAssertions; using GitLabApiClient.Internal.Queries; using GitLabApiClient.Models.Groups.Requests; +using GitLabApiClient.Models.Groups.Responses; using GitLabApiClient.Models.Milestones.Requests; using GitLabApiClient.Models.Milestones.Responses; +using GitLabApiClient.Test.Utilities; using Xunit; using static GitLabApiClient.Test.Utilities.GitLabApiHelper; @@ -22,7 +24,8 @@ public class GroupsClientTest GetFacade(), new GroupsQueryBuilder(), new ProjectsGroupQueryBuilder(), - new MilestonesQueryBuilder()); + new MilestonesQueryBuilder(), + new GroupLabelsQueryBuilder()); [Fact] public async Task GroupCanBeRetrievedByGroupId() @@ -149,6 +152,31 @@ public async Task CreatedGroupCanBeUpdated() updateGroupResponse.RequestAccessEnabled.Should().BeFalse(); } + [Fact] + public async Task CreatedGroupLabelCanBeUpdated() + { + //arrange + var createdLabel = await _sut.CreateLabelAsync(GitLabApiHelper.TestGroupId, new CreateGroupLabelRequest("Label 1") + { + Color = "#FFFFFF", + Description = "description1" + }); + + //act + var updateRequest = UpdateGroupLabelRequest.FromNewName(createdLabel.Name, "Label 11"); + updateRequest.Color = "#000000"; + updateRequest.Description = "description11"; + + var updatedLabel = await _sut.UpdateLabelAsync(GitLabApiHelper.TestGroupId, updateRequest); + await _sut.DeleteLabelAsync(GitLabApiHelper.TestGroupId, updatedLabel.Name); + + //assert + updatedLabel.Should().Match(l => + l.Name == "Label 11" && + l.Color == "#000000" && + l.Description == "description11"); + } + [Fact] public async Task CreatedGroupMilestoneCanBeUpdated() { diff --git a/test/GitLabApiClient.Test/Internal/Queries/GroupLabelsQueryBuilderTest.cs b/test/GitLabApiClient.Test/Internal/Queries/GroupLabelsQueryBuilderTest.cs new file mode 100644 index 00000000..69175d6d --- /dev/null +++ b/test/GitLabApiClient.Test/Internal/Queries/GroupLabelsQueryBuilderTest.cs @@ -0,0 +1,28 @@ +using FluentAssertions; +using GitLabApiClient.Internal.Queries; +using GitLabApiClient.Models.Groups.Requests; +using Xunit; + +namespace GitLabApiClient.Test.Internal.Queries +{ + public class GroupLabelsQueryBuilderTest + { + [Fact] + public void NonDefaultQueryBuilt() + { + var sut = new GroupLabelsQueryBuilder(); + + string query = sut.Build( + "https://gitlab.com/api/v4/groups/123/labels", + new GroupLabelsQueryOptions + { + WithCounts = true, + IncludeAncestorGroups = false + }); + + query.Should().Be("https://gitlab.com/api/v4/groups/123/labels?" + + "with_counts=true&" + + "include_ancestor_groups=false"); + } + } +} diff --git a/test/GitLabApiClient.Test/Internal/Queries/IssuesQueryBuilderTest.cs b/test/GitLabApiClient.Test/Internal/Queries/IssuesQueryBuilderTest.cs index 3f21b4ae..a1002d85 100644 --- a/test/GitLabApiClient.Test/Internal/Queries/IssuesQueryBuilderTest.cs +++ b/test/GitLabApiClient.Test/Internal/Queries/IssuesQueryBuilderTest.cs @@ -1,3 +1,4 @@ +using System; using FluentAssertions; using GitLabApiClient.Internal.Queries; using GitLabApiClient.Models; @@ -27,10 +28,15 @@ public void NonDefaultQueryBuilt() IssueIds = { 3, 4 }, Order = IssuesOrder.UpdatedAt, SortOrder = SortOrder.Ascending, - Filter = "filter" + Filter = "filter", + CreatedAfter = new DateTime(1991, 11, 11, 1, 1, 1), + CreatedBefore = new DateTime(1991, 12, 12, 2, 2, 2), + UpdatedAfter = new DateTime(1991, 4, 4, 4, 4, 4), + UpdatedBefore = new DateTime(1991, 5, 5, 5, 5, 5), + IsConfidential = true }); - query.Should().Be("https://gitlab.com/api/v4/issues?" + + query.Should().BeEquivalentTo("https://gitlab.com/api/v4/issues?" + "state=opened&" + "labels=label1%2Clabel2&" + "milestone=milestone1&" + @@ -40,7 +46,12 @@ public void NonDefaultQueryBuilt() "iids%5B%5D=3&iids%5B%5D=4&" + "order_by=updated_at&" + "sort=asc&" + - "search=filter"); + "search=filter&" + + "confidential=true&" + + "created_before=1991-12-12T02%3a02%3a02.0000000&" + + "created_after=1991-11-11T01%3a01%3a01.0000000&" + + "updated_before=1991-05-05T05%3a05%3a05.0000000&" + + "updated_after=1991-04-04T04%3a04%3a04.0000000"); } } } diff --git a/test/GitLabApiClient.Test/Internal/Queries/ProjectIssuesQueryBuilderTest.cs b/test/GitLabApiClient.Test/Internal/Queries/ProjectIssuesQueryBuilderTest.cs deleted file mode 100644 index e89e4f79..00000000 --- a/test/GitLabApiClient.Test/Internal/Queries/ProjectIssuesQueryBuilderTest.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using FluentAssertions; -using GitLabApiClient.Internal.Queries; -using GitLabApiClient.Models; -using GitLabApiClient.Models.Issues.Requests; -using GitLabApiClient.Models.Issues.Responses; -using Xunit; - -namespace GitLabApiClient.Test.Internal.Queries -{ - public class ProjectIssuesQueryBuilderTest - { - [Fact] - public void NonDefaultQueryBuilt() - { - var sut = new ProjectIssuesQueryBuilder(); - - string query = sut.Build( - "https://gitlab.com/api/v4/issues", - new ProjectIssuesQueryOptions() - { - State = IssueState.Opened, - Labels = { "label1", "label2" }, - MilestoneTitle = "milestone1", - Scope = Scope.All, - AuthorId = 1, - AssigneeId = 2, - IssueIds = { 3, 4 }, - Order = IssuesOrder.UpdatedAt, - SortOrder = SortOrder.Ascending, - Filter = "filter", - CreatedAfter = new DateTime(1991, 11, 11, 1, 1, 1), - CreatedBefore = new DateTime(1991, 12, 12, 2, 2, 2) - }); - - query.Should().Be("https://gitlab.com/api/v4/issues?" + - "state=opened&" + - "labels=label1%2Clabel2&" + - "milestone=milestone1&" + - "scope=all&" + - "author_id=1&" + - "assignee_id=2&" + - "iids%5B%5D=3&iids%5B%5D=4&" + - "order_by=updated_at&" + - "sort=asc&" + - "search=filter&" + - "created_after=1991-11-11T01%3A01%3A01.0000000&" + - "created_before=1991-12-12T02%3A02%3A02.0000000"); - } - } -} diff --git a/test/GitLabApiClient.Test/IssuesClientTest.cs b/test/GitLabApiClient.Test/IssuesClientTest.cs index 8f23eceb..8f321a69 100644 --- a/test/GitLabApiClient.Test/IssuesClientTest.cs +++ b/test/GitLabApiClient.Test/IssuesClientTest.cs @@ -19,7 +19,7 @@ namespace GitLabApiClient.Test public class IssuesClientTest { private readonly IssuesClient _sut = new IssuesClient( - GetFacade(), new IssuesQueryBuilder(), new ProjectIssuesQueryBuilder(), new ProjectIssueNotesQueryBuilder()); + GetFacade(), new IssuesQueryBuilder(), new ProjectIssueNotesQueryBuilder()); [Fact] public async Task CreatedIssueCanBeUpdated() @@ -80,7 +80,7 @@ public async Task CreatedIssueCanBeListedFromProject() await _sut.CreateAsync(TestProjectTextId, new CreateIssueRequest(title)); //act - var listedIssues = await _sut.GetAsync(TestProjectTextId, o => o.Filter = title); + var listedIssues = await _sut.GetAllAsync(projectId: TestProjectTextId, options: o => o.Filter = title); //assert listedIssues.Single().Should().Match(i => @@ -105,8 +105,8 @@ public async Task CreatedIssueCanBeRetrieved() //act var issueById = await _sut.GetAsync(TestProjectId, issue.Iid); - var issueByProjectId = (await _sut.GetAsync(o => o.IssueIds = new[] { issue.Iid })).FirstOrDefault(i => i.Title == title); - var ownedIssue = (await _sut.GetAsync(o => o.Scope = Scope.CreatedByMe)).FirstOrDefault(i => i.Title == title); + var issueByProjectId = (await _sut.GetAllAsync(options: o => o.IssueIds = new[] { issue.Iid })).FirstOrDefault(i => i.Title == title); + var ownedIssue = (await _sut.GetAllAsync(options: o => o.Scope = Scope.CreatedByMe)).FirstOrDefault(i => i.Title == title); //assert issue.Should().Match(i =>