Skip to content

Commit 19456a7

Browse files
jeffpardynmklotas
authored andcommitted
Updates to groups client (#26)
* Include WithCustomAttributes and MinAccessLevel to GroupsQueryOptions * Support retrieving a group's subgroups * Support retrieving and managing group membership
1 parent 586fbf6 commit 19456a7

File tree

14 files changed

+302
-67
lines changed

14 files changed

+302
-67
lines changed

src/GitLabApiClient/GitLabClient.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ public GitLabClient(string hostUrl, string authenticationToken = "")
2424
{
2525
Guard.NotEmpty(hostUrl, nameof(hostUrl));
2626
Guard.NotNull(authenticationToken, nameof(authenticationToken));
27-
HostUrl = hostUrl;
27+
HostUrl = FixBaseUrl(hostUrl);
2828

2929
var jsonSerializer = new RequestsJsonSerializer();
3030

3131
_httpFacade = new GitLabHttpFacade(
32-
FixBaseUrl(hostUrl),
32+
HostUrl,
3333
jsonSerializer,
3434
authenticationToken);
3535

@@ -92,13 +92,12 @@ public Task<Session> LoginAsync(string username, string password)
9292

9393
private static string FixBaseUrl(string url)
9494
{
95-
if (!url.EndsWith("/", StringComparison.OrdinalIgnoreCase))
96-
url += "/";
95+
url = url.TrimEnd('/');
9796

98-
if (!url.EndsWith("/api/v4/", StringComparison.OrdinalIgnoreCase))
99-
url += "/api/v4/";
97+
if (!url.EndsWith("/api/v4", StringComparison.OrdinalIgnoreCase))
98+
url += "/api/v4";
10099

101-
return url;
100+
return url + "/";
102101
}
103102
}
104103
}

src/GitLabApiClient/GroupsClient.cs

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using GitLabApiClient.Models.Milestones.Responses;
1111
using GitLabApiClient.Models.Milestones.Requests;
1212
using GitLabApiClient.Internal.Utilities;
13+
using GitLabApiClient.Models;
1314

1415
namespace GitLabApiClient
1516
{
@@ -28,7 +29,7 @@ public sealed class GroupsClient
2829
internal GroupsClient(
2930
GitLabHttpFacade httpFacade,
3031
GroupsQueryBuilder queryBuilder,
31-
ProjectsGroupQueryBuilder projectsQueryBuilder,
32+
ProjectsGroupQueryBuilder projectsQueryBuilder,
3233
MilestonesQueryBuilder queryMilestonesBuilder)
3334
{
3435
_httpFacade = httpFacade;
@@ -44,6 +45,13 @@ internal GroupsClient(
4445
public async Task<Group> GetAsync(string groupId) =>
4546
await _httpFacade.Get<Group>($"groups/{groupId}");
4647

48+
/// <summary>
49+
/// Get all subgroups of a group.
50+
/// This endpoint can be accessed without authentication if the group is publicly accessible.
51+
/// </summary>
52+
public async Task<IList<Group>> GetSubgroupsAsync(string groupId) =>
53+
await _httpFacade.GetPagedList<Group>($"groups/{groupId}/subgroups");
54+
4755
/// <summary>
4856
/// Get all groups that match your string in their name or path.
4957
/// </summary>
@@ -81,6 +89,53 @@ public async Task<IList<Project>> GetProjectsAsync(string groupId, Action<Projec
8189
return await _httpFacade.GetPagedList<Project>(url);
8290
}
8391

92+
/// <summary>
93+
/// Get a list of members in this group.
94+
/// </summary>
95+
/// <param name="groupId">The ID or URL-encoded path of the group owned by the authenticated user.</param>
96+
/// <param name="search">A query string to search for members.</param>
97+
/// <returns>Group members satisfying options.</returns>
98+
public async Task<IList<Member>> GetMembersAsync(string groupId, string search = null)
99+
{
100+
string url = $"groups/{groupId}/members";
101+
102+
if (!string.IsNullOrEmpty(search))
103+
{
104+
url += $"?search={search}";
105+
}
106+
107+
return await _httpFacade.GetPagedList<Member>(url);
108+
}
109+
110+
/// <summary>
111+
/// Get a list of all members (including inherited) in this group.
112+
/// </summary>
113+
/// <param name="groupId">The ID or URL-encoded path of the group owned by the authenticated user.</param>
114+
/// <param name="search">A query string to search for members.</param>
115+
/// <returns>Group members satisfying options.</returns>
116+
public async Task<IList<Member>> GetAllMembersAsync(string groupId, string search = null)
117+
{
118+
string url = $"groups/{groupId}/members/all";
119+
120+
if (!string.IsNullOrEmpty(search))
121+
{
122+
url += $"?search={search}";
123+
}
124+
125+
return await _httpFacade.GetPagedList<Member>(url);
126+
}
127+
128+
/// <summary>
129+
/// Adds a member to the group.
130+
/// </summary>
131+
/// <param name="request">Create milestone request.</param>
132+
/// <returns>Newly created milestone.</returns>
133+
public async Task<Milestone> CreateMilestoneAsync(CreateGroupMilestoneRequest request)
134+
{
135+
Guard.NotNull(request, nameof(request));
136+
return await _httpFacade.Post<Milestone>($"groups/{request.GroupId}/milestones", request);
137+
}
138+
84139
/// <summary>
85140
/// Get a list of milestones in this group.
86141
/// </summary>
@@ -112,16 +167,35 @@ public async Task<Group> CreateAsync(CreateGroupRequest request) =>
112167
await _httpFacade.Post<Group>("groups", request);
113168

114169
/// <summary>
115-
/// Creates a new group milestone.
170+
/// Adds a user to a group.
116171
/// </summary>
117-
/// <param name="request">Create milestone request.</param>
118-
/// <returns>Newly created milestone.</returns>
119-
public async Task<Milestone> CreateMilestoneAsync(CreateGroupMilestoneRequest request)
172+
/// <param name="request">Add group member request.</param>
173+
/// <returns>Newly created membership.</returns>
174+
public async Task<Member> AddMemberAsync(AddGroupMemberRequest request)
120175
{
121176
Guard.NotNull(request, nameof(request));
122-
return await _httpFacade.Post<Milestone>($"groups/{request.GroupId}/milestones", request);
177+
return await _httpFacade.Post<Member>($"groups/{request.GroupId}/members", request);
178+
}
179+
180+
/// <summary>
181+
/// Updates a user's group membership.
182+
/// </summary>
183+
/// <param name="request">Update group member request.</param>
184+
/// <returns>Updated membership.</returns>
185+
public async Task<Member> UpdateMemberAsync(AddGroupMemberRequest request)
186+
{
187+
Guard.NotNull(request, nameof(request));
188+
return await _httpFacade.Put<Member>($"groups/{request.GroupId}/members/{request.UserId}", request);
123189
}
124190

191+
/// <summary>
192+
/// Removes a user as a member of the group.
193+
/// </summary>
194+
/// <param name="groupId">The ID or path of a group.</param>
195+
/// <param name="userId">The id of the user.</param>
196+
public async Task RemoveMemberAsync(string groupId, int userId) =>
197+
await _httpFacade.Delete($"groups/{groupId}/members/{userId}");
198+
125199
/// <summary>
126200
/// Transfer a project to the Group namespace. Available only for admin
127201
/// </summary>

src/GitLabApiClient/Internal/Queries/GroupsQueryBuilder.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,14 @@ protected override void BuildCore(GroupsQueryOptions options)
2525
if (options.Statistics)
2626
Add("statistics", options.Statistics);
2727

28+
if (options.WithCustomAttributes)
29+
Add("with_custom_attributes", options.WithCustomAttributes);
30+
2831
if (options.Owned)
2932
Add("owned", options.Owned);
33+
34+
if (options.MinAccessLevel.HasValue)
35+
Add("min_access_level", (int)options.MinAccessLevel.Value);
3036
}
3137

3238
private static string GetOrderQueryValue(GroupsOrder order)

src/GitLabApiClient/IssuesClient.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public sealed class IssuesClient
2525
private readonly ProjectIssueNotesQueryBuilder _projectIssueNotesQueryBuilder;
2626

2727
internal IssuesClient(
28-
GitLabHttpFacade httpFacade,
28+
GitLabHttpFacade httpFacade,
2929
IssuesQueryBuilder queryBuilder,
3030
ProjectIssuesQueryBuilder projectIssuesQueryBuilder,
3131
ProjectIssueNotesQueryBuilder projectIssueNotesQueryBuilder)
@@ -39,7 +39,7 @@ internal IssuesClient(
3939
/// <summary>
4040
/// Retrieves project issue.
4141
/// </summary>
42-
public async Task<Issue> GetAsync(int projectId, int issueId) =>
42+
public async Task<Issue> GetAsync(int projectId, int issueId) =>
4343
await _httpFacade.Get<Issue>($"projects/{projectId}/issues/{issueId}");
4444

4545
/// <summary>
@@ -103,7 +103,7 @@ public async Task<IList<Note>> GetNotesAsync(int projectId, int issueIid, Action
103103
/// Creates new issue.
104104
/// </summary>
105105
/// <returns>The newly created issue.</returns>
106-
public async Task<Issue> CreateAsync(CreateIssueRequest request) =>
106+
public async Task<Issue> CreateAsync(CreateIssueRequest request) =>
107107
await _httpFacade.Post<Issue>($"projects/{request.ProjectId}/issues", request);
108108

109109
/// <summary>

src/GitLabApiClient/MergeRequestsClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,14 @@ public async Task<IList<MergeRequest>> GetAsync(Action<MergeRequestsQueryOptions
7070
/// Creates merge request.
7171
/// </summary>
7272
/// <returns>The newly created merge request.</returns>
73-
public async Task<MergeRequest> CreateAsync(CreateMergeRequest request) =>
73+
public async Task<MergeRequest> CreateAsync(CreateMergeRequest request) =>
7474
await _httpFacade.Post<MergeRequest>($"projects/{request.ProjectId}/merge_requests", request);
7575

7676
/// <summary>
7777
/// Updates merge request.
7878
/// </summary>
7979
/// <returns>The updated merge request.</returns>
80-
public async Task<MergeRequest> UpdateAsync(UpdateMergeRequest request) =>
80+
public async Task<MergeRequest> UpdateAsync(UpdateMergeRequest request) =>
8181
await _httpFacade.Put<MergeRequest>($"projects/{request.ProjectId}/merge_requests/{request.MergeRequestId}", request);
8282

8383
/// <summary>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
3+
namespace GitLabApiClient.Models
4+
{
5+
public enum AccessLevel
6+
{
7+
Guest = 10,
8+
Reporter = 20,
9+
Developer = 30,
10+
Maintainer = 40,
11+
Owner = 50 // Only valid for groups
12+
}
13+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using GitLabApiClient.Internal.Utilities;
2+
using Newtonsoft.Json;
3+
4+
namespace GitLabApiClient.Models.Groups.Requests
5+
{
6+
/// <summary>
7+
/// Used to add members in a group.
8+
/// </summary>
9+
public sealed class AddGroupMemberRequest
10+
{
11+
/// <summary>
12+
/// Initializes a new instance of the <see cref="AddGroupMemberRequest"/> class.
13+
/// </summary>
14+
/// <param name="groupId">The ID or URL-encoded path of the group owned by the authenticated user.</param>
15+
/// <param name="userId">The id of the user to add as member.</param>
16+
/// <param name="accessLevel">The access level of the new member.</param>
17+
public AddGroupMemberRequest(string groupId, int userId, AccessLevel accessLevel)
18+
{
19+
Guard.NotEmpty(groupId, nameof(groupId));
20+
GroupId = groupId;
21+
UserId = userId;
22+
AccessLevel = (int)accessLevel;
23+
}
24+
25+
/// <summary>
26+
/// The ID or URL-encoded path of the group owned by the authenticated user.
27+
/// </summary>
28+
[JsonProperty("id")]
29+
public string GroupId { get; private set; }
30+
31+
/// <summary>
32+
/// The id of the user.
33+
/// </summary>
34+
[JsonProperty("user_id")]
35+
public int UserId { get; private set; }
36+
37+
/// <summary>
38+
/// The desired access level
39+
/// </summary>
40+
[JsonProperty("access_level")]
41+
public int AccessLevel { get; private set; }
42+
43+
/// <summary>
44+
/// The membership expiration date. Date time string in the format YEAR-MONTH-DAY, e.g. 2016-03-11.
45+
/// </summary>
46+
[JsonProperty("expires_at")]
47+
public string ExpiresAt { get; set; }
48+
}
49+
}

src/GitLabApiClient/Models/Groups/Requests/GroupsQueryOptions.cs

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,56 @@
22

33
namespace GitLabApiClient.Models.Groups.Requests
44
{
5-
/// <summary>
6-
/// Options for Groups listing
7-
/// </summary>
8-
public class GroupsQueryOptions
5+
/// <summary>
6+
/// Options for Groups listing
7+
/// </summary>
8+
public class GroupsQueryOptions
99
{
1010
internal GroupsQueryOptions() { }
1111

12-
/// <summary>
13-
/// Skip the group IDs passes
14-
/// </summary>
15-
public IList<int> SkipGroups { get; set; } = new List<int>();
16-
17-
/// <summary>
18-
/// Show all the groups you have access to
19-
/// </summary>
20-
public bool AllAvailable { get; set; }
21-
22-
/// <summary>
23-
/// Return list of authorized groups matching the search criteria
24-
/// </summary>
25-
public string Search { get; set; }
26-
27-
/// <summary>
28-
/// Order groups by name or path. Default is name
29-
/// </summary>
30-
public GroupsOrder Order { get; set; }
31-
32-
/// <summary>
33-
/// Order groups in asc or desc order. Default is asc
34-
/// </summary>
35-
public GroupsSort Sort { get; set; }
36-
37-
/// <summary>
38-
/// Include group statistics (admins only)
39-
/// </summary>
40-
public bool Statistics { get; set; }
41-
42-
/// <summary>
43-
/// Limit by groups owned by the current user
44-
/// </summary>
45-
public bool Owned { get; set; }
46-
12+
/// <summary>
13+
/// Skip the group IDs passes
14+
/// </summary>
15+
public IList<int> SkipGroups { get; set; } = new List<int>();
16+
17+
/// <summary>
18+
/// Show all the groups you have access to
19+
/// </summary>
20+
public bool AllAvailable { get; set; }
21+
22+
/// <summary>
23+
/// Return list of authorized groups matching the search criteria
24+
/// </summary>
25+
public string Search { get; set; }
26+
27+
/// <summary>
28+
/// Order groups by name or path. Default is name
29+
/// </summary>
30+
public GroupsOrder Order { get; set; }
31+
32+
/// <summary>
33+
/// Order groups in asc or desc order. Default is asc
34+
/// </summary>
35+
public GroupsSort Sort { get; set; }
36+
37+
/// <summary>
38+
/// Include group statistics (admins only)
39+
/// </summary>
40+
public bool Statistics { get; set; }
41+
42+
/// <summary>
43+
/// Include custom attributes (admins only)
44+
/// </summary>
45+
public bool WithCustomAttributes { get; set; }
46+
47+
/// <summary>
48+
/// Limit by groups owned by the current user
49+
/// </summary>
50+
public bool Owned { get; set; }
51+
52+
/// <summary>
53+
/// Limit to groups where the current users has at least this access level
54+
/// </summary>
55+
public AccessLevel? MinAccessLevel { get; set; }
4756
}
4857
}

0 commit comments

Comments
 (0)