diff --git a/.editorconfig b/.editorconfig index af10f470..dc8e66ff 100644 --- a/.editorconfig +++ b/.editorconfig @@ -73,10 +73,10 @@ csharp_style_var_for_built_in_types = true : suggestion csharp_style_var_when_type_is_apparent = true : warning # Expression-Bodied members -csharp_style_expression_bodied_accessors = true : suggestion -csharp_style_expression_bodied_indexers = true : suggestion -csharp_style_expression_bodied_operators = true : suggestion -csharp_style_expression_bodied_properties = true : suggestion +csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_operators = true:suggestion +csharp_style_expression_bodied_properties = true:suggestion # Explicitly disabled due to difference in coding style between source and tests #csharp_style_expression_bodied_constructors = true : warning #csharp_style_expression_bodied_methods = true : warning @@ -101,7 +101,7 @@ csharp_style_conditional_delegate_call = true : warning csharp_style_throw_expression = true : warning # Code block preferences -csharp_prefer_braces = when_multiline : suggestion +csharp_prefer_braces = when_multiline:suggestion ## Formatting conventions # Dotnet formatting settings: @@ -141,6 +141,16 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false # Wrapping options csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = false +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +dotnet_diagnostic.SA1507.severity = error ## Naming conventions [*.{cs,vb}] @@ -159,7 +169,7 @@ dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case # Constants are PascalCase dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants -dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style +dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_symbols.constants.applicable_kinds = field, local dotnet_naming_symbols.constants.required_modifiers = const @@ -198,7 +208,7 @@ dotnet_naming_style.camel_case_style.capitalization = camel_case # Local functions are PascalCase dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions -dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style +dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_symbols.local_functions.applicable_kinds = local_function dotnet_naming_style.local_function_style.capitalization = pascal_case @@ -216,7 +226,7 @@ dotnet_naming_symbols.type_parameter_symbol.applicable_accessibilities = * # By default, name items with PascalCase dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members -dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.members_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_symbols.all_members.applicable_kinds = * @@ -373,3 +383,6 @@ dotnet_diagnostic.SA1636.severity = none # SA1649: File name should match first type name dotnet_diagnostic.SA1649.severity = none +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +end_of_line = crlf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c3b1a31..621af2e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: create_nuget: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # Get all the history so MinGit can compute the version - name: Setup .NET Core (latest) @@ -51,12 +51,8 @@ jobs: # Keep in sync with the version in GitLabDockerContainer.cs # Available tags: https://hub.docker.com/r/gitlab/gitlab-ee/tags gitlab: [ - 'gitlab/gitlab-ee:15.0.5-ee.0', - 'gitlab/gitlab-ee:15.1.6-ee.0', 'gitlab/gitlab-ee:15.4.6-ee.0', - 'gitlab/gitlab-ee:15.6.8-ee.0', - # Several MR-related tests fail against the following version. We need to investigate... - # 'gitlab/gitlab-ee:15.10.0-ee.0', + 'gitlab/gitlab-ee:15.11.9-ee.0', ] configuration: [ Release ] fail-fast: false @@ -69,7 +65,7 @@ jobs: GITLAB_OMNIBUS_CONFIG: "external_url 'http://localhost:48624/'" GITLAB_ROOT_PASSWORD: "Pa$$w0rd" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET Core (latest) uses: actions/setup-dotnet@v3 - run: | diff --git a/Directory.Build.props b/Directory.Build.props index 82459655..1861a9b9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,7 @@ $(Company), NGitLab contributors - 9.0 + 10.0 true strict true @@ -16,7 +16,7 @@ true - LICENSE + MIT README.md @@ -37,17 +37,17 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/NGitLab.Mock.Tests/BotUserTests.cs b/NGitLab.Mock.Tests/BotUserTests.cs new file mode 100644 index 00000000..97489197 --- /dev/null +++ b/NGitLab.Mock.Tests/BotUserTests.cs @@ -0,0 +1,41 @@ +using NGitLab.Models; +using NUnit.Framework; + +namespace NGitLab.Mock.Tests +{ + public sealed class BotUserTests + { + [Test] + public void Test_project_bot_user() + { + using var server = new GitLabServer(); + var group = new Group("test"); + var project = new Project("test-project"); + server.Groups.Add(group); + group.Projects.Add(project); + + var bot = project.CreateBotUser("token_name", AccessLevel.Maintainer); + + Assert.That(bot.Bot, Is.True); + Assert.That(bot.Name, Is.EqualTo("token_name")); + var permissions = project.GetEffectivePermissions(); + var botPermission = permissions.GetEffectivePermission(bot); + Assert.That(botPermission.AccessLevel, Is.EqualTo(AccessLevel.Maintainer)); + } + + [Test] + public void Test_group_bot_user() + { + using var server = new GitLabServer(); + var group = new Group("test"); + server.Groups.Add(group); + + var bot = group.CreateBotUser(AccessLevel.Maintainer); + + Assert.That(bot.Bot, Is.True); + var permissions = group.GetEffectivePermissions(); + var botPermission = permissions.GetEffectivePermission(bot); + Assert.That(botPermission.AccessLevel, Is.EqualTo(AccessLevel.Maintainer)); + } + } +} diff --git a/NGitLab.Mock.Tests/MembersMockTests.cs b/NGitLab.Mock.Tests/MembersMockTests.cs new file mode 100644 index 00000000..f973c2e0 --- /dev/null +++ b/NGitLab.Mock.Tests/MembersMockTests.cs @@ -0,0 +1,86 @@ +using System.Linq; +using NGitLab.Mock.Config; +using NUnit.Framework; + +namespace NGitLab.Mock.Tests +{ + public class MembersMockTests + { + [Test] + public void Test_members_group_all_direct([Values] bool isDefault) + { + using var server = new GitLabConfig() + .WithUser("user1", isDefault: true) + .WithUser("user2") + .WithGroup("G1", 1, addDefaultUserAsMaintainer: true) + .WithGroup("G2", 2, @namespace: "G1", configure: g => g.WithUserPermission("user2", Models.AccessLevel.Maintainer)) + .BuildServer(); + + var client = server.CreateClient("user1"); + var members = isDefault + ? client.Members.OfGroup("2") + : client.Members.OfGroup("2", includeInheritedMembers: false); + + Assert.AreEqual(1, members.Count(), "Membership found are invalid"); + } + + [Test] + public void Test_members_group_all_inherited() + { + using var server = new GitLabConfig() + .WithUser("user1", isDefault: true) + .WithUser("user2") + .WithProject("Test") + .WithGroup("G1", 1, configure: g => g.WithUserPermission("user1", Models.AccessLevel.Maintainer)) + .WithGroup("G2", 2, @namespace: "G1", configure: g => g.WithUserPermission("user2", Models.AccessLevel.Maintainer)) + .BuildServer(); + + var client = server.CreateClient("user1"); + var members = client.Members.OfGroup("2", includeInheritedMembers: true); + + Assert.AreEqual(2, members.Count(), "Membership found are invalid"); + } + + [Test] + public void Test_members_project_all_direct([Values] bool isDefault) + { + using var server = new GitLabConfig() + .WithUser("user1", isDefault: true) + .WithUser("user2") + .WithUser("user3") + .WithGroup("G1", 1, addDefaultUserAsMaintainer: true) + .WithGroup("G2", 2, @namespace: "G1", configure: g => g.WithUserPermission("user2", Models.AccessLevel.Maintainer)) + .WithProject("Project", @namespace: "G1", configure: g => + g.WithUserPermission("user3", Models.AccessLevel.Maintainer) + .WithGroupPermission("G2", Models.AccessLevel.Developer)) + .BuildServer(); + + var client = server.CreateClient("user1"); + var members = isDefault + ? client.Members.OfProject("1") + : client.Members.OfProject("1", includeInheritedMembers: false); + + Assert.AreEqual(1, members.Count(), "Membership found are invalid"); + } + + [Test] + public void Test_members_project_all_inherited() + { + using var server = new GitLabConfig() + .WithUser("user1", isDefault: true) + .WithUser("user2") + .WithUser("user3") + .WithGroup("G1", addDefaultUserAsMaintainer: true) + .WithGroup("G2", @namespace: "G1", configure: g => g.WithUserPermission("user2", Models.AccessLevel.Maintainer)) + .WithProject("Project", 1, @namespace: "G1", configure: g => + g.WithUserPermission("user3", Models.AccessLevel.Maintainer) + .WithGroupPermission("G1/G2", Models.AccessLevel.Developer)) + .BuildServer(); + + var client = server.CreateClient("user1"); + var members = client.Members.OfProject("1", includeInheritedMembers: true); + + Assert.AreEqual(3, members.Count(), "Membership found are invalid"); + } + } +} diff --git a/NGitLab.Mock.Tests/MilestonesMockTests.cs b/NGitLab.Mock.Tests/MilestonesMockTests.cs index 4339d90f..074dd2a7 100644 --- a/NGitLab.Mock.Tests/MilestonesMockTests.cs +++ b/NGitLab.Mock.Tests/MilestonesMockTests.cs @@ -99,5 +99,47 @@ public void Test_milestones_can_be_closed_and_activated_from_project() Assert.AreEqual(1, activeMilestones.Length, "Active milestones count is invalid"); Assert.AreEqual(0, closedMilestones.Length, "Closed milestones count is invalid"); } + + [Test] + public void Test_projects_merge_request_can_be_found_from_milestone() + { + const int ProjectId = 1; + const int MilestoneId = 1; + using var server = new GitLabConfig() + .WithUser("user1", isDefault: true) + .WithProject("Test", id: ProjectId, addDefaultUserAsMaintainer: true, configure: project => project + .WithMilestone("Milestone 1", id: MilestoneId) + .WithMergeRequest("branch-01", title: "Merge request 1", milestone: "Milestone 1") + .WithMergeRequest("branch-02", title: "Merge request 2", milestone: "Milestone 1") + .WithMergeRequest("branch-03", title: "Merge request 3", milestone: "Milestone 2")) + .BuildServer(); + + var client = server.CreateClient(); + var mergeRequests = client.GetMilestone(ProjectId).GetMergeRequests(MilestoneId).ToArray(); + Assert.AreEqual(2, mergeRequests.Length, "Merge requests count is invalid"); + } + + [Test] + public void Test_groups_merge_request_can_be_found_from_milestone() + { + const int projectId = 1; + const int milestoneId = 1; + using var server = new GitLabConfig() + .WithUser("user1", isDefault: true) + .WithGroup("parentGroup", id: projectId, configure: group => group + .WithMilestone("Milestone 1", id: milestoneId)) + .WithGroup("subGroup1", 2, @namespace: "parentGroup") + .WithGroup("subGroup2", 3, @namespace: "parentGroup") + .WithProject("project1", @namespace: "parentGroup/subGroup1", addDefaultUserAsMaintainer: true, configure: project => project + .WithMergeRequest("branch-01", title: "Merge request 1", milestone: "Milestone 1") + .WithMergeRequest("branch-02", title: "Merge request 2", milestone: "Milestone 2")) + .WithProject("project2", @namespace: "parentGroup/subGroup2", addDefaultUserAsMaintainer: true, configure: project => project + .WithMergeRequest("branch-03", title: "Merge request 3", milestone: "Milestone 1")) + .BuildServer(); + + var client = server.CreateClient(); + var mergeRequests = client.GetGroupMilestone(projectId).GetMergeRequests(milestoneId).ToArray(); + Assert.AreEqual(2, mergeRequests.Length, "Merge requests count is invalid"); + } } } diff --git a/NGitLab.Mock.Tests/NGitLab.Mock.Tests.csproj b/NGitLab.Mock.Tests/NGitLab.Mock.Tests.csproj index a9180fe5..cb56d584 100644 --- a/NGitLab.Mock.Tests/NGitLab.Mock.Tests.csproj +++ b/NGitLab.Mock.Tests/NGitLab.Mock.Tests.csproj @@ -7,9 +7,9 @@ - + - + diff --git a/NGitLab.Mock/Clients/IssueClient.cs b/NGitLab.Mock/Clients/IssueClient.cs index d38712b2..bc1a342b 100644 --- a/NGitLab.Mock/Clients/IssueClient.cs +++ b/NGitLab.Mock/Clients/IssueClient.cs @@ -302,6 +302,16 @@ public Models.Issue Get(int projectId, int issueId) return GetById(issueId); } + public GitLabCollectionResponse LinkedToAsync(int projectId, int issueId) + { + throw new NotImplementedException(); + } + + public bool CreateLinkBetweenIssues(int sourceProjectId, int sourceIssueId, int targetProjectId, int targetIssueId) + { + throw new NotImplementedException(); + } + public Models.Issue GetById(int issueId) { using (Context.BeginOperationScope()) diff --git a/NGitLab.Mock/Clients/LibGit2SharpExtensions.cs b/NGitLab.Mock/Clients/LibGit2SharpExtensions.cs index d58a3845..6b86a8c8 100644 --- a/NGitLab.Mock/Clients/LibGit2SharpExtensions.cs +++ b/NGitLab.Mock/Clients/LibGit2SharpExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NGitLab.Models; @@ -62,7 +63,25 @@ public static Models.CommitInfo ToCommitInfo(this LibGit2Sharp.Commit commit) internal static LibGit2Sharp.Commit GetLastCommitForFileChanges(this LibGit2Sharp.Repository repository, string filePath) { - return repository.Commits.QueryBy(filePath).FirstOrDefault()?.Commit; + try + { + return repository.Commits.QueryBy(filePath).FirstOrDefault()?.Commit; + } + catch (KeyNotFoundException) + { + // LibGit2Sharp sometimes fails with the following exception + // System.Collections.Generic.KeyNotFoundException: The given key '1d08df45e551942eaa70d9f5ab6f5f7665a3f5b3' was not present in the dictionary. + // at System.Collections.Generic.Dictionary`2.get_Item(TKey key) + // at LibGit2Sharp.Core.FileHistory.FullHistory(IRepository repo, String path, CommitFilter filter)+MoveNext() in /_/LibGit2Sharp/Core/FileHistory.cs:line 120 + // at System.Linq.Enumerable.TryGetFirst[TSource](IEnumerable`1 source, Boolean& found) + // at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source) + // at NGitLab.Mock.Clients.LibGit2SharpExtensions.GetLastCommitForFileChanges(Repository repository, String filePath) in /_/NGitLab.Mock/Clients/LibGit2SharpExtensions.cs:line 65 + // at NGitLab.Mock.Repository.GetFile(String filePath, String ref) in /_/NGitLab.Mock/Repository.cs:line 485 + // at NGitLab.Mock.Clients.FileClient.Get(String filePath, String ref) in /_/NGitLab.Mock/Clients/FileClient.cs:line 77 + // at NGitLab.Mock.Clients.FileClient.GetAsync(String filePath, String ref, CancellationToken cancellationToken) in /_/NGitLab.Mock/Clients/FileClient.cs:line 125 + } + + return null; } } } diff --git a/NGitLab.Mock/Clients/MembersClient.cs b/NGitLab.Mock/Clients/MembersClient.cs index a516dfe2..7b33a3e2 100644 --- a/NGitLab.Mock/Clients/MembersClient.cs +++ b/NGitLab.Mock/Clients/MembersClient.cs @@ -98,7 +98,7 @@ public IEnumerable OfGroup(string groupId, bool includeInheritedMemb using (Context.BeginOperationScope()) { var group = GetGroup(groupId, GroupPermission.View); - var members = group.GetEffectivePermissions().Permissions; + var members = group.GetEffectivePermissions(includeInheritedMembers).Permissions; return members.Select(member => member.ToMembershipClient()); } } @@ -118,7 +118,7 @@ public IEnumerable OfProject(string projectId, bool includeInherited using (Context.BeginOperationScope()) { var project = GetProject(projectId, ProjectPermission.View); - var members = project.GetEffectivePermissions().Permissions; + var members = project.GetEffectivePermissions(includeInheritedMembers).Permissions; return members.Select(member => member.ToMembershipClient()); } } diff --git a/NGitLab.Mock/Clients/MergeRequestDiscussionClient.cs b/NGitLab.Mock/Clients/MergeRequestDiscussionClient.cs index a10e304c..82ad734f 100644 --- a/NGitLab.Mock/Clients/MergeRequestDiscussionClient.cs +++ b/NGitLab.Mock/Clients/MergeRequestDiscussionClient.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NGitLab.Models; namespace NGitLab.Mock.Clients @@ -30,6 +32,21 @@ public IEnumerable All } } + public MergeRequestDiscussion Get(string id) + { + using (Context.BeginOperationScope()) + { + var discussions = GetMergeRequest().GetDiscussions(); + var discussion = discussions.FirstOrDefault(x => string.Equals(x.Id, id, StringComparison.Ordinal)); + return discussion ?? throw new GitLabNotFoundException(); + } + } + + public Task GetAsync(string id, CancellationToken cancellationToken = default) + { + return Task.FromResult(Get(id)); + } + public MergeRequestDiscussion Add(Models.MergeRequestComment comment) { return Add(new MergeRequestDiscussionCreate @@ -71,7 +88,7 @@ public MergeRequestDiscussion Resolve(MergeRequestDiscussionResolve resolve) using (Context.BeginOperationScope()) { var discussions = GetMergeRequest().GetDiscussions(); - var discussion = discussions.First(x => string.Equals(x.Id, resolve.Id, StringComparison.Ordinal)); + var discussion = discussions.FirstOrDefault(x => string.Equals(x.Id, resolve.Id, StringComparison.Ordinal)); if (discussion == null) throw new GitLabNotFoundException(); @@ -89,7 +106,7 @@ public void Delete(string discussionId, long noteId) using (Context.BeginOperationScope()) { var discussions = GetMergeRequest().GetDiscussions(); - var discussion = discussions.First(x => string.Equals(x.Id, discussionId, StringComparison.Ordinal)); + var discussion = discussions.FirstOrDefault(x => string.Equals(x.Id, discussionId, StringComparison.Ordinal)); if (discussion == null) throw new GitLabNotFoundException(); diff --git a/NGitLab.Mock/Clients/MilestoneClient.cs b/NGitLab.Mock/Clients/MilestoneClient.cs index 5a265a20..0e688f6d 100644 --- a/NGitLab.Mock/Clients/MilestoneClient.cs +++ b/NGitLab.Mock/Clients/MilestoneClient.cs @@ -11,13 +11,12 @@ namespace NGitLab.Mock internal sealed class MilestoneClient : ClientBase, IMilestoneClient { private readonly int _resourceId; - private readonly MilestoneScope _scope; public MilestoneClient(ClientContext context, int id, MilestoneScope scope) : base(context) { _resourceId = id; - _scope = scope; + Scope = scope; } public Models.Milestone this[int id] @@ -26,35 +25,46 @@ public Models.Milestone this[int id] { using (Context.BeginOperationScope()) { - var project = GetProject(_resourceId, ProjectPermission.View); - return FindMilestone(id, project)?.ToClientMilestone(); + return GetMilestone(id, false).ToClientMilestone(); } } } public IEnumerable All => Get(new MilestoneQuery()); - public MilestoneScope Scope => throw new NotImplementedException(); + public MilestoneScope Scope { get; } public Models.Milestone Activate(int milestoneId) { using (Context.BeginOperationScope()) { - var milestone = new Milestone(); + var milestone = GetMilestone(milestoneId, true); + milestone.State = MilestoneState.active; + return milestone.ToClientMilestone(); + } + } - if (_scope == MilestoneScope.Groups) - { - var group = GetGroup(_resourceId, GroupPermission.Edit); - milestone = FindMilestone(milestoneId, group) ?? throw new GitLabNotFoundException($"Cannot find milestone with ID {milestoneId}"); - } - else if (_scope == MilestoneScope.Projects) + public IEnumerable GetMergeRequests(int milestoneId) + { + using (Context.BeginOperationScope()) + { + var milestone = GetMilestone(milestoneId, false); + IEnumerable mergeRequests; + + switch (Scope) { - var project = GetProject(_resourceId, ProjectPermission.Edit); - milestone = FindMilestone(milestoneId, project) ?? throw new GitLabNotFoundException($"Cannot find milestone with ID {milestoneId}"); + case MilestoneScope.Groups: + mergeRequests = milestone.Group.MergeRequests; + break; + case MilestoneScope.Projects: + mergeRequests = milestone.Project.MergeRequests; + break; + default: + throw new NotSupportedException($"{Scope} milestone is not supported yet."); } - milestone.State = MilestoneState.active; - return milestone.ToClientMilestone(); + mergeRequests = mergeRequests.Where(mr => mr.Milestone == milestone); + return mergeRequests.Select(mr => mr.ToMergeRequestClient()); } } @@ -67,19 +77,7 @@ public Models.Milestone Close(int milestoneId) { using (Context.BeginOperationScope()) { - var milestone = new Milestone(); - - if (_scope == MilestoneScope.Groups) - { - var group = GetGroup(_resourceId, GroupPermission.Edit); - milestone = FindMilestone(milestoneId, group) ?? throw new GitLabNotFoundException($"Cannot find milestone with ID {milestoneId}"); - } - else if (_scope == MilestoneScope.Projects) - { - var project = GetProject(_resourceId, ProjectPermission.Edit); - milestone = FindMilestone(milestoneId, project) ?? throw new GitLabNotFoundException($"Cannot find milestone with ID {milestoneId}"); - } - + var milestone = GetMilestone(milestoneId, true); milestone.State = MilestoneState.closed; return milestone.ToClientMilestone(); } @@ -97,15 +95,18 @@ public Models.Milestone Create(MilestoneCreate milestone) StartDate = string.IsNullOrEmpty(milestone.StartDate) ? DateTimeOffset.UtcNow : DateTimeOffset.Parse(milestone.StartDate), }; - if (_scope == MilestoneScope.Groups) + switch (Scope) { - var group = GetGroup(_resourceId, GroupPermission.Edit); - group.Milestones.Add(ms); - } - else if (_scope == MilestoneScope.Projects) - { - var project = GetProject(_resourceId, ProjectPermission.Edit); - project.Milestones.Add(ms); + case MilestoneScope.Groups: + var group = GetGroup(_resourceId, GroupPermission.Edit); + group.Milestones.Add(ms); + break; + case MilestoneScope.Projects: + var project = GetProject(_resourceId, ProjectPermission.Edit); + project.Milestones.Add(ms); + break; + default: + throw new NotSupportedException($"{Scope} milestone is not supported yet."); } return ms.ToClientMilestone(); @@ -116,17 +117,17 @@ public void Delete(int milestoneId) { using (Context.BeginOperationScope()) { - if (_scope == MilestoneScope.Groups) - { - var group = GetGroup(_resourceId, GroupPermission.Edit); - var milestone = FindMilestone(milestoneId, group) ?? throw new GitLabNotFoundException($"Cannot find milestone with ID {milestoneId}"); - group.Milestones.Remove(milestone); - } - else if (_scope == MilestoneScope.Projects) - { - var project = GetProject(_resourceId, ProjectPermission.Edit); - var milestone = FindMilestone(milestoneId, project) ?? throw new GitLabNotFoundException($"Cannot find milestone with ID {milestoneId}"); - project.Milestones.Remove(milestone); + var milestone = GetMilestone(milestoneId, true); + switch (Scope) + { + case MilestoneScope.Groups: + milestone.Group.Milestones.Remove(milestone); + break; + case MilestoneScope.Projects: + milestone.Project.Milestones.Remove(milestone); + break; + default: + throw new NotSupportedException($"{Scope} milestone is not supported yet."); } } } @@ -135,17 +136,20 @@ public void Delete(int milestoneId) { using (Context.BeginOperationScope()) { - IEnumerable milestones = new List(); + IEnumerable milestones; - if (_scope == MilestoneScope.Groups) + switch (Scope) { - var group = GetGroup(_resourceId, GroupPermission.View); - milestones = milestones.Concat(group.Milestones); - } - else if (_scope == MilestoneScope.Projects) - { - var project = GetProject(_resourceId, ProjectPermission.View); - milestones = milestones.Concat(project.Milestones); + case MilestoneScope.Groups: + var group = GetGroup(_resourceId, GroupPermission.View); + milestones = group.Milestones; + break; + case MilestoneScope.Projects: + var project = GetProject(_resourceId, ProjectPermission.View); + milestones = project.Milestones; + break; + default: + throw new NotSupportedException($"{Scope} milestone is not supported yet."); } if (query.State != null) @@ -166,18 +170,7 @@ public Models.Milestone Update(int milestoneId, MilestoneUpdate milestone) { using (Context.BeginOperationScope()) { - var ms = new Milestone(); - - if (_scope == MilestoneScope.Groups) - { - var group = GetGroup(_resourceId, GroupPermission.Edit); - ms = FindMilestone(milestoneId, group) ?? throw new GitLabNotFoundException($"Cannot find milestone with ID {milestoneId}"); - } - else if (_scope == MilestoneScope.Projects) - { - var project = GetProject(_resourceId, ProjectPermission.Edit); - ms = FindMilestone(milestoneId, project) ?? throw new GitLabNotFoundException($"Cannot find milestone with ID {milestoneId}"); - } + var ms = GetMilestone(milestoneId, true); if (!string.IsNullOrEmpty(milestone.Title)) { @@ -203,14 +196,25 @@ public Models.Milestone Update(int milestoneId, MilestoneUpdate milestone) } } - private static Milestone FindMilestone(int id, Project project) + private Milestone GetMilestone(int milestoneId, bool editing) { - return project.Milestones.FirstOrDefault(x => x.Id == id); - } + Milestone milestone; - private static Milestone FindMilestone(int id, Group group) - { - return group.Milestones.FirstOrDefault(x => x.Id == id); + switch (Scope) + { + case MilestoneScope.Groups: + var group = GetGroup(_resourceId, editing ? GroupPermission.Edit : GroupPermission.View); + milestone = group.Milestones.FirstOrDefault(x => x.Id == milestoneId); + break; + case MilestoneScope.Projects: + var project = GetProject(_resourceId, editing ? ProjectPermission.Edit : ProjectPermission.View); + milestone = project.Milestones.FirstOrDefault(x => x.Id == milestoneId); + break; + default: + throw new NotSupportedException($"{Scope} milestone is not supported yet."); + } + + return milestone ?? throw new GitLabNotFoundException($"Cannot find milestone with ID {milestoneId}"); } } } diff --git a/NGitLab.Mock/Clients/PipelineClient.cs b/NGitLab.Mock/Clients/PipelineClient.cs index 065e3e25..97c028c6 100644 --- a/NGitLab.Mock/Clients/PipelineClient.cs +++ b/NGitLab.Mock/Clients/PipelineClient.cs @@ -291,5 +291,19 @@ public GitLabCollectionResponse GetVariablesAsync(int pipeline { return GitLabCollectionResponse.Create(GetVariables(pipelineId)); } + + public Task RetryAsync(int pipelineId, CancellationToken cancellationToken = default) + { + using (Context.BeginOperationScope()) + { + var jobs = _jobClient.GetJobs(JobScopeMask.Failed).Where(j => j.Pipeline.Id == pipelineId); + foreach (var job in jobs) + { + _jobClient.RunAction(job.Id, JobAction.Retry); + } + + return Task.FromResult(this[pipelineId]); + } + } } } diff --git a/NGitLab.Mock/Config/GitLabGroup.cs b/NGitLab.Mock/Config/GitLabGroup.cs index 9b88e7c4..06746af5 100644 --- a/NGitLab.Mock/Config/GitLabGroup.cs +++ b/NGitLab.Mock/Config/GitLabGroup.cs @@ -11,6 +11,7 @@ public GitLabGroup() { Labels = new GitLabLabelsCollection(this); Permissions = new GitLabPermissionsCollection(this); + Milestones = new GitLabMilestonesCollection(this); } /// @@ -30,6 +31,8 @@ public GitLabGroup() public GitLabLabelsCollection Labels { get; } public GitLabPermissionsCollection Permissions { get; } + + public GitLabMilestonesCollection Milestones { get; } } public class GitLabGroupsCollection : GitLabCollection diff --git a/NGitLab.Mock/Config/GitLabHelpers.cs b/NGitLab.Mock/Config/GitLabHelpers.cs index 005fa3cf..1f43107f 100644 --- a/NGitLab.Mock/Config/GitLabHelpers.cs +++ b/NGitLab.Mock/Config/GitLabHelpers.cs @@ -596,8 +596,9 @@ public static GitLabProject WithMergeRequest(this GitLabProject project, string? /// Merge date time. /// Approvers usernames. /// Labels names. + /// Milestone name. /// Configuration method - public static GitLabProject WithMergeRequest(this GitLabProject project, string? sourceBranch = null, string? title = null, int id = default, string? targetBranch = null, string? description = null, string? author = null, string? assignee = null, DateTime? createdAt = null, DateTime? updatedAt = null, DateTime? closedAt = null, DateTime? mergedAt = null, IEnumerable? approvers = null, IEnumerable? labels = null, Action? configure = null) + public static GitLabProject WithMergeRequest(this GitLabProject project, string? sourceBranch = null, string? title = null, int id = default, string? targetBranch = null, string? description = null, string? author = null, string? assignee = null, DateTime? createdAt = null, DateTime? updatedAt = null, DateTime? closedAt = null, DateTime? mergedAt = null, IEnumerable? approvers = null, IEnumerable? labels = null, string? milestone = null, Action? configure = null) { return WithMergeRequest(project, sourceBranch, title, author, mergeRequest => { @@ -611,6 +612,8 @@ public static GitLabProject WithMergeRequest(this GitLabProject project, string? mergeRequest.UpdatedAt = updatedAt; mergeRequest.ClosedAt = closedAt; mergeRequest.MergedAt = mergedAt; + mergeRequest.Milestone = milestone; + if (labels != null) { foreach (var label in labels) @@ -737,6 +740,57 @@ public static GitLabProject WithGroupPermission(this GitLabProject project, stri }); } + /// + /// Add milestone in group + /// + /// Group. + /// Title (required) + /// Configuration method + public static GitLabGroup WithMilestone(this GitLabGroup group, string title, Action configure) + { + return Configure(group, _ => + { + var milestone = new GitLabMilestone + { + Title = title ?? throw new ArgumentNullException(nameof(title)), + }; + + group.Milestones.Add(milestone); + configure(milestone); + }); + } + + /// + /// Add milestone in group + /// + /// Group. + /// Title (required) + /// Explicit ID (config increment) + /// Description. + /// Due date time. + /// Start date time. + /// Creation date time. + /// Update date time. + /// Close date time. + /// Configuration method + public static GitLabGroup WithMilestone(this GitLabGroup group, string title, int id = default, string? description = null, DateTime? dueDate = null, DateTime? startDate = null, DateTime? createdAt = null, DateTime? updatedAt = null, DateTime? closedAt = null, Action? configure = null) + { + return WithMilestone(group, title, milestone => + { + if (id != default) + milestone.Id = id; + + milestone.Description = description; + milestone.DueDate = dueDate; + milestone.StartDate = startDate; + milestone.CreatedAt = createdAt; + milestone.UpdatedAt = updatedAt; + milestone.ClosedAt = closedAt; + + configure?.Invoke(milestone); + }); + } + /// /// Add milestone in project /// @@ -783,6 +837,8 @@ public static GitLabProject WithMilestone(this GitLabProject project, string tit milestone.CreatedAt = createdAt; milestone.UpdatedAt = updatedAt; milestone.ClosedAt = closedAt; + + configure?.Invoke(milestone); }); } @@ -1108,6 +1164,11 @@ private static void CreateGroup(GitLabServer server, GitLabGroup group) { CreatePermission(server, grp, permission); } + + foreach (var milestone in group.Milestones) + { + CreateMilestone(grp, milestone); + } } private static void CreateProject(GitLabServer server, GitLabProject project) @@ -1401,6 +1462,7 @@ private static void CreateMergeRequest(GitLabServer server, Project project, Git ClosedAt = mergeRequest.ClosedAt, MergedAt = mergeRequest.MergedAt, SourceProject = project, + Milestone = string.IsNullOrEmpty(mergeRequest.Milestone) ? null : GetOrCreateMilestone(project, mergeRequest.Milestone), }; var endedAt = request.MergedAt ?? request.ClosedAt; @@ -1453,7 +1515,19 @@ private static Permission CreatePermission(GitLabServer server, GitLabPermission : new Permission(GetOrCreateUser(server, permission.User), permission.Level); } + private static void CreateMilestone(Group group, GitLabMilestone milestone) + { + var mlt = CreateMilestone(milestone); + group.Milestones.Add(mlt); + } + private static void CreateMilestone(Project project, GitLabMilestone milestone) + { + var mlt = CreateMilestone(milestone); + project.Milestones.Add(mlt); + } + + private static Milestone CreateMilestone(GitLabMilestone milestone) { var mlt = new Milestone { @@ -1465,10 +1539,13 @@ private static void CreateMilestone(Project project, GitLabMilestone milestone) UpdatedAt = milestone.UpdatedAt ?? DateTimeOffset.UtcNow, ClosedAt = milestone.ClosedAt, }; - project.Milestones.Add(mlt); if (mlt.ClosedAt != null && mlt.UpdatedAt > mlt.ClosedAt) + { mlt.UpdatedAt = (DateTimeOffset)mlt.ClosedAt; + } + + return mlt; } private static void CreateComment(GitLabServer server, Issue issue, GitLabComment comment) @@ -1624,6 +1701,14 @@ private static User GetOrCreateUser(GitLabServer server, string username) private static Milestone GetOrCreateMilestone(Project project, string title) { var milestone = project.Milestones.FirstOrDefault(x => string.Equals(x.Title, title, StringComparison.OrdinalIgnoreCase)); + + var group = project.Group; + while (milestone == null && group != null) + { + milestone = group.Milestones.FirstOrDefault(x => string.Equals(x.Title, title, StringComparison.OrdinalIgnoreCase)); + group = group.Parent; + } + if (milestone == null) { milestone = new Milestone diff --git a/NGitLab.Mock/Config/GitLabMergeRequest.cs b/NGitLab.Mock/Config/GitLabMergeRequest.cs index 3b48152d..d07f720d 100644 --- a/NGitLab.Mock/Config/GitLabMergeRequest.cs +++ b/NGitLab.Mock/Config/GitLabMergeRequest.cs @@ -53,6 +53,8 @@ public GitLabMergeRequest() public DateTime? MergedAt { get; set; } public DateTime? ClosedAt { get; set; } + + public string Milestone { get; set; } } public class GitLabMergeRequestsCollection : GitLabCollection diff --git a/NGitLab.Mock/Config/GitLabMilestone.cs b/NGitLab.Mock/Config/GitLabMilestone.cs index f6869085..f8a108bd 100644 --- a/NGitLab.Mock/Config/GitLabMilestone.cs +++ b/NGitLab.Mock/Config/GitLabMilestone.cs @@ -34,6 +34,11 @@ internal GitLabMilestonesCollection(GitLabProject parent) { } + internal GitLabMilestonesCollection(GitLabGroup parent) + : base(parent) + { + } + internal override void SetItem(GitLabMilestone item) { if (item == null) diff --git a/NGitLab.Mock/Group.cs b/NGitLab.Mock/Group.cs index fb9908b8..aea0de2d 100644 --- a/NGitLab.Mock/Group.cs +++ b/NGitLab.Mock/Group.cs @@ -76,6 +76,8 @@ public string Name public MilestoneCollection Milestones { get; } + public IEnumerable MergeRequests => AllProjects.SelectMany(project => project.MergeRequests); + public string Path { get @@ -115,13 +117,15 @@ public IEnumerable DescendantGroups public IEnumerable AllProjects => Projects.Concat(DescendantGroups.SelectMany(group => group.Projects)); - public EffectivePermissions GetEffectivePermissions() + public EffectivePermissions GetEffectivePermissions() => GetEffectivePermissions(includeInheritedPermissions: true); + + public EffectivePermissions GetEffectivePermissions(bool includeInheritedPermissions) { var result = new Dictionary(); - if (Parent != null) + if (Parent != null && includeInheritedPermissions) { - foreach (var effectivePermission in Parent.GetEffectivePermissions().Permissions) + foreach (var effectivePermission in Parent.GetEffectivePermissions(includeInheritedPermissions).Permissions) { Add(effectivePermission.User, effectivePermission.AccessLevel); } @@ -135,7 +139,7 @@ public EffectivePermissions GetEffectivePermissions() } else { - foreach (var effectivePermission in permission.Group.GetEffectivePermissions().Permissions) + foreach (var effectivePermission in permission.Group.GetEffectivePermissions(includeInheritedPermissions).Permissions) { Add(effectivePermission.User, effectivePermission.AccessLevel); } @@ -254,5 +258,22 @@ public Models.Group ToClientGroup(User currentUser) SharedRunnersMinutesLimit = (int)SharedRunnersLimit.TotalMinutes, }; } + + /// + /// https://docs.gitlab.com/ee/user/group/settings/group_access_tokens.html#bot-users-for-groups + /// + /// AccessLevel to give to the bot user + /// Bot user that have been added to the group + public User CreateBotUser(AccessLevel accessLevel) + { + var botUsername = $"group_{Id}_bot_{Guid.NewGuid():D}"; + var bot = new User(botUsername) + { + Email = $"{botUsername}@noreply.example.com", + }; + Permissions.Add(new Permission(bot, accessLevel)); + Server.Users.Add(bot); + return bot; + } } } diff --git a/NGitLab.Mock/MergeRequest.cs b/NGitLab.Mock/MergeRequest.cs index a06e9a48..d48238bc 100644 --- a/NGitLab.Mock/MergeRequest.cs +++ b/NGitLab.Mock/MergeRequest.cs @@ -147,6 +147,8 @@ public Pipeline HeadPipeline } } + public Milestone Milestone { get; set; } + public IList Labels { get; } = new List(); public NoteCollection Comments { get; } diff --git a/NGitLab.Mock/Milestone.cs b/NGitLab.Mock/Milestone.cs index 15bca089..8a652278 100644 --- a/NGitLab.Mock/Milestone.cs +++ b/NGitLab.Mock/Milestone.cs @@ -7,6 +7,8 @@ public sealed class Milestone : GitLabObject { public Project Project => Parent as Project; + public Group Group => Parent as Group; + public int Id { get; set; } public int Iid { get; set; } diff --git a/NGitLab.Mock/NGitLab.Mock.csproj b/NGitLab.Mock/NGitLab.Mock.csproj index 4bb1a565..d44f59d7 100644 --- a/NGitLab.Mock/NGitLab.Mock.csproj +++ b/NGitLab.Mock/NGitLab.Mock.csproj @@ -1,22 +1,22 @@ - net461;netstandard2.0 - $(NoWarn);NU5104 + net472;netstandard2.0 + $(NoWarn);NU1701;NU5104 GitLab REST API .NET Client Mock Library - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers diff --git a/NGitLab.Mock/Project.cs b/NGitLab.Mock/Project.cs index 197e07e8..86f99ba9 100644 --- a/NGitLab.Mock/Project.cs +++ b/NGitLab.Mock/Project.cs @@ -68,7 +68,7 @@ public string DefaultBranch public Project ForkedFrom { get; internal set; } - public RepositoryAccessLevel ForkingAccessLevel { get; set; } + public RepositoryAccessLevel ForkingAccessLevel { get; set; } = RepositoryAccessLevel.Enabled; public string ImportStatus { get; set; } @@ -154,13 +154,18 @@ public void Remove() Group.Projects.Remove(this); } - public EffectivePermissions GetEffectivePermissions() + public EffectivePermissions GetEffectivePermissions() => GetEffectivePermissions(includeInheritedPermissions: true); + + public EffectivePermissions GetEffectivePermissions(bool includeInheritedPermissions) { var result = new Dictionary(); - foreach (var effectivePermission in Group.GetEffectivePermissions().Permissions) + if (includeInheritedPermissions) { - Add(effectivePermission.User, effectivePermission.AccessLevel); + foreach (var effectivePermission in Group.GetEffectivePermissions(includeInheritedPermissions).Permissions) + { + Add(effectivePermission.User, effectivePermission.AccessLevel); + } } foreach (var permission in Permissions) @@ -171,7 +176,7 @@ public EffectivePermissions GetEffectivePermissions() } else { - foreach (var effectivePermission in permission.Group.GetEffectivePermissions().Permissions) + foreach (var effectivePermission in permission.Group.GetEffectivePermissions(includeInheritedPermissions).Permissions) { Add(effectivePermission.User, effectivePermission.AccessLevel); } @@ -399,6 +404,25 @@ public Project Fork(Group group, User user, string projectName) return newProject; } + /// + /// https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html#bot-users-for-projects + /// + /// Name of the token + /// AccessLevel to give to the bot user + /// Bot user that have been added to the project + public User CreateBotUser(string tokenName, AccessLevel accessLevel) + { + var botUsername = $"project_{Id}_bot_{Guid.NewGuid():D}"; + var bot = new User(botUsername) + { + Name = tokenName, + Email = $"{botUsername}@noreply.example.com", + }; + Permissions.Add(new Permission(bot, accessLevel)); + Server.Users.Add(bot); + return bot; + } + public Models.Project ToClientProject(User currentUser) { var kind = Group.IsUserNamespace ? "user" : "group"; diff --git a/NGitLab.Mock/PublicAPI.Unshipped.txt b/NGitLab.Mock/PublicAPI.Unshipped.txt index 02648e5f..2aa6ffc9 100644 --- a/NGitLab.Mock/PublicAPI.Unshipped.txt +++ b/NGitLab.Mock/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -abstract NGitLab.Mock.File.Content.get -> byte[] +abstract NGitLab.Mock.File.Content.get -> byte[] abstract NGitLab.Mock.Note.NoteableType.get -> string abstract NGitLab.Mock.Note.NoticableId.get -> int abstract NGitLab.Mock.Note.NoticableIid.get -> int @@ -134,6 +134,7 @@ NGitLab.Mock.Config.GitLabConfig.DefaultVisibility.set -> void NGitLab.Mock.Config.GitLabConfig.Serialize() -> string NGitLab.Mock.Config.GitLabConfig.Url.get -> string NGitLab.Mock.Config.GitLabConfig.Url.set -> void +NGitLab.Mock.Config.GitLabGroup.Milestones.get -> NGitLab.Mock.Config.GitLabMilestonesCollection NGitLab.Mock.Config.GitLabGroup.Visibility.get -> NGitLab.Models.VisibilityLevel? NGitLab.Mock.Config.GitLabIssue.Comments.get -> NGitLab.Mock.Config.GitLabCommentsCollection NGitLab.Mock.Config.GitLabIssue.CreatedAt.get -> System.DateTime? @@ -163,6 +164,8 @@ NGitLab.Mock.Config.GitLabJob.TagList.set -> void NGitLab.Mock.Config.GitLabJobsCollection NGitLab.Mock.Config.GitLabMergeRequest.Comments.get -> NGitLab.Mock.Config.GitLabCommentsCollection NGitLab.Mock.Config.GitLabMergeRequest.CreatedAt.get -> System.DateTime? +NGitLab.Mock.Config.GitLabMergeRequest.Milestone.get -> string +NGitLab.Mock.Config.GitLabMergeRequest.Milestone.set -> void NGitLab.Mock.Config.GitLabMergeRequest.UpdatedAt.get -> System.DateTime? NGitLab.Mock.Config.GitLabMilestone NGitLab.Mock.Config.GitLabMilestone.ClosedAt.get -> System.DateTime? @@ -429,6 +432,7 @@ NGitLab.Mock.Group.CanUserAddProject(NGitLab.Mock.User user) -> bool NGitLab.Mock.Group.CanUserDeleteGroup(NGitLab.Mock.User user) -> bool NGitLab.Mock.Group.CanUserEditGroup(NGitLab.Mock.User user) -> bool NGitLab.Mock.Group.CanUserViewGroup(NGitLab.Mock.User user) -> bool +NGitLab.Mock.Group.CreateBotUser(NGitLab.Models.AccessLevel accessLevel) -> NGitLab.Mock.User NGitLab.Mock.Group.DescendantGroups.get -> System.Collections.Generic.IEnumerable NGitLab.Mock.Group.Description.get -> string NGitLab.Mock.Group.Description.set -> void @@ -436,6 +440,7 @@ NGitLab.Mock.Group.ExtraSharedRunnersLimit.get -> System.TimeSpan NGitLab.Mock.Group.ExtraSharedRunnersLimit.set -> void NGitLab.Mock.Group.FullName.get -> string NGitLab.Mock.Group.GetEffectivePermissions() -> NGitLab.Mock.EffectivePermissions +NGitLab.Mock.Group.GetEffectivePermissions(bool includeInheritedPermissions) -> NGitLab.Mock.EffectivePermissions NGitLab.Mock.Group.Group() -> void NGitLab.Mock.Group.Group(NGitLab.Mock.User user) -> void NGitLab.Mock.Group.Group(string name) -> void @@ -447,6 +452,7 @@ NGitLab.Mock.Group.IsUserOwner(NGitLab.Mock.User user) -> bool NGitLab.Mock.Group.Labels.get -> NGitLab.Mock.LabelsCollection NGitLab.Mock.Group.LfsEnabled.get -> bool NGitLab.Mock.Group.LfsEnabled.set -> void +NGitLab.Mock.Group.MergeRequests.get -> System.Collections.Generic.IEnumerable NGitLab.Mock.Group.Milestones.get -> NGitLab.Mock.MilestoneCollection NGitLab.Mock.Group.Name.get -> string NGitLab.Mock.Group.Name.set -> void @@ -617,6 +623,8 @@ NGitLab.Mock.MergeRequest.MergedAt.set -> void NGitLab.Mock.MergeRequest.MergeRequest() -> void NGitLab.Mock.MergeRequest.MergeWhenPipelineSucceeds.get -> bool NGitLab.Mock.MergeRequest.MergeWhenPipelineSucceeds.set -> void +NGitLab.Mock.MergeRequest.Milestone.get -> NGitLab.Mock.Milestone +NGitLab.Mock.MergeRequest.Milestone.set -> void NGitLab.Mock.MergeRequest.Project.get -> NGitLab.Mock.Project NGitLab.Mock.MergeRequest.Rebase(NGitLab.Mock.User user) -> NGitLab.Models.RebaseResult NGitLab.Mock.MergeRequest.RebaseInProgress.get -> bool @@ -663,6 +671,7 @@ NGitLab.Mock.Milestone.Description.get -> string NGitLab.Mock.Milestone.Description.set -> void NGitLab.Mock.Milestone.DueDate.get -> System.DateTimeOffset NGitLab.Mock.Milestone.DueDate.set -> void +NGitLab.Mock.Milestone.Group.get -> NGitLab.Mock.Group NGitLab.Mock.Milestone.Id.get -> int NGitLab.Mock.Milestone.Id.set -> void NGitLab.Mock.Milestone.Iid.get -> int @@ -792,6 +801,7 @@ NGitLab.Mock.Project.CanUserViewConfidentialIssues(NGitLab.Mock.User user) -> bo NGitLab.Mock.Project.CanUserViewProject(NGitLab.Mock.User user) -> bool NGitLab.Mock.Project.CommitInfos.get -> NGitLab.Mock.CommitInfoCollection NGitLab.Mock.Project.CommitStatuses.get -> NGitLab.Mock.CommitStatusCollection +NGitLab.Mock.Project.CreateBotUser(string tokenName, NGitLab.Models.AccessLevel accessLevel) -> NGitLab.Mock.User NGitLab.Mock.Project.CreateMergeRequest(NGitLab.Mock.User user, string title, string description, string targetBranchName, string sourceBranchName, NGitLab.Mock.Project sourceProject = null) -> NGitLab.Mock.MergeRequest NGitLab.Mock.Project.CreateNewMergeRequest(NGitLab.Mock.User user, string branchName, string targetBranch, string title, string description) -> NGitLab.Mock.MergeRequest NGitLab.Mock.Project.DefaultBranch.get -> string @@ -806,6 +816,7 @@ NGitLab.Mock.Project.ForkingAccessLevel.get -> NGitLab.Models.RepositoryAccessLe NGitLab.Mock.Project.ForkingAccessLevel.set -> void NGitLab.Mock.Project.FullName.get -> string NGitLab.Mock.Project.GetEffectivePermissions() -> NGitLab.Mock.EffectivePermissions +NGitLab.Mock.Project.GetEffectivePermissions(bool includeInheritedPermissions) -> NGitLab.Mock.EffectivePermissions NGitLab.Mock.Project.Group.get -> NGitLab.Mock.Group NGitLab.Mock.Project.Hooks.get -> NGitLab.Mock.ProjectHookCollection NGitLab.Mock.Project.HttpUrl.get -> string @@ -1119,6 +1130,7 @@ NGitLab.Mock.TextFile.TextFile(string path, string content, System.Text.Encoding NGitLab.Mock.User NGitLab.Mock.User.AvatarUrl.get -> string NGitLab.Mock.User.AvatarUrl.set -> void +NGitLab.Mock.User.Bot.get -> bool NGitLab.Mock.User.CreatedAt.get -> System.DateTime NGitLab.Mock.User.CreatedAt.set -> void NGitLab.Mock.User.Email.get -> string @@ -1215,8 +1227,10 @@ static NGitLab.Mock.Config.GitLabHelpers.WithIssue(this NGitLab.Mock.Config.GitL static NGitLab.Mock.Config.GitLabHelpers.WithIssue(this NGitLab.Mock.Config.GitLabProject project, string title, string author, System.Action configure) -> NGitLab.Mock.Config.GitLabProject static NGitLab.Mock.Config.GitLabHelpers.WithJob(this NGitLab.Mock.Config.GitLabPipeline pipeline, string name = null, string stage = null, NGitLab.JobStatus status = NGitLab.JobStatus.Unknown, System.DateTime? createdAt = null, System.DateTime? startedAt = null, System.DateTime? finishedAt = null, bool allowFailure = false, NGitLab.Mock.Config.GitLabPipeline downstreamPipeline = null, System.Action configure = null) -> NGitLab.Mock.Config.GitLabPipeline static NGitLab.Mock.Config.GitLabHelpers.WithMergeCommit(this NGitLab.Mock.Config.GitLabProject project, string sourceBranch, string targetBranch = null, string user = null, bool deleteSourceBranch = false, System.Collections.Generic.IEnumerable tags = null, System.Action configure = null) -> NGitLab.Mock.Config.GitLabProject -static NGitLab.Mock.Config.GitLabHelpers.WithMergeRequest(this NGitLab.Mock.Config.GitLabProject project, string sourceBranch = null, string title = null, int id = 0, string targetBranch = null, string description = null, string author = null, string assignee = null, System.DateTime? createdAt = null, System.DateTime? updatedAt = null, System.DateTime? closedAt = null, System.DateTime? mergedAt = null, System.Collections.Generic.IEnumerable approvers = null, System.Collections.Generic.IEnumerable labels = null, System.Action configure = null) -> NGitLab.Mock.Config.GitLabProject +static NGitLab.Mock.Config.GitLabHelpers.WithMergeRequest(this NGitLab.Mock.Config.GitLabProject project, string sourceBranch = null, string title = null, int id = 0, string targetBranch = null, string description = null, string author = null, string assignee = null, System.DateTime? createdAt = null, System.DateTime? updatedAt = null, System.DateTime? closedAt = null, System.DateTime? mergedAt = null, System.Collections.Generic.IEnumerable approvers = null, System.Collections.Generic.IEnumerable labels = null, string milestone = null, System.Action configure = null) -> NGitLab.Mock.Config.GitLabProject static NGitLab.Mock.Config.GitLabHelpers.WithMergeRequest(this NGitLab.Mock.Config.GitLabProject project, string sourceBranch, string title, string author, System.Action configure) -> NGitLab.Mock.Config.GitLabProject +static NGitLab.Mock.Config.GitLabHelpers.WithMilestone(this NGitLab.Mock.Config.GitLabGroup group, string title, int id = 0, string description = null, System.DateTime? dueDate = null, System.DateTime? startDate = null, System.DateTime? createdAt = null, System.DateTime? updatedAt = null, System.DateTime? closedAt = null, System.Action configure = null) -> NGitLab.Mock.Config.GitLabGroup +static NGitLab.Mock.Config.GitLabHelpers.WithMilestone(this NGitLab.Mock.Config.GitLabGroup group, string title, System.Action configure) -> NGitLab.Mock.Config.GitLabGroup static NGitLab.Mock.Config.GitLabHelpers.WithMilestone(this NGitLab.Mock.Config.GitLabProject project, string title, int id = 0, string description = null, System.DateTime? dueDate = null, System.DateTime? startDate = null, System.DateTime? createdAt = null, System.DateTime? updatedAt = null, System.DateTime? closedAt = null, System.Action configure = null) -> NGitLab.Mock.Config.GitLabProject static NGitLab.Mock.Config.GitLabHelpers.WithMilestone(this NGitLab.Mock.Config.GitLabProject project, string title, System.Action configure) -> NGitLab.Mock.Config.GitLabProject static NGitLab.Mock.Config.GitLabHelpers.WithPipeline(this NGitLab.Mock.Config.GitLabProject project, string ref, System.Action configure) -> NGitLab.Mock.Config.GitLabProject diff --git a/NGitLab.Mock/Repository.cs b/NGitLab.Mock/Repository.cs index 11b07f48..a6c9372c 100644 --- a/NGitLab.Mock/Repository.cs +++ b/NGitLab.Mock/Repository.cs @@ -502,7 +502,7 @@ public FileData GetFile(string filePath, string @ref) Ref = @ref, BlobId = ((Blob)commit[filePath].Target).Id.ToString(), CommitId = commit.Sha, - LastCommitId = repo.GetLastCommitForFileChanges(filePath).Sha, + LastCommitId = repo.GetLastCommitForFileChanges(filePath)?.Sha, }; } diff --git a/NGitLab.Mock/User.cs b/NGitLab.Mock/User.cs index a42436f8..a5d2f8d7 100644 --- a/NGitLab.Mock/User.cs +++ b/NGitLab.Mock/User.cs @@ -34,6 +34,25 @@ public User(string userName) public string WebUrl => Server.MakeUrl(UserName); + public bool Bot + { + get + { + if (string.IsNullOrEmpty(UserName)) + return false; + + var nameParts = UserName.Split('_'); + + if (nameParts.Length != 4) + return false; + + if (!string.Equals(nameParts[0], "project", StringComparison.Ordinal) && !string.Equals(nameParts[0], "group", StringComparison.Ordinal)) + return false; + + return int.TryParse(nameParts[1], out var _) && string.Equals("bot", nameParts[2], StringComparison.Ordinal); + } + } + public Models.User ToClientUser() { var user = new Models.User(); @@ -59,6 +78,7 @@ private void CopyTo(T instance) instance.AvatarURL = AvatarUrl; instance.CreatedAt = CreatedAt; instance.Identities = Identities; + instance.Bot = Bot; if (IsAdmin) { diff --git a/NGitLab.Tests/CommitsTests.cs b/NGitLab.Tests/CommitsTests.cs index 356965b3..b77dd7d5 100644 --- a/NGitLab.Tests/CommitsTests.cs +++ b/NGitLab.Tests/CommitsTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NGitLab.Models; @@ -72,7 +73,10 @@ public async Task Test_can_get_merge_request_associated_to_commit() Title = mergeRequestTitle, }); - var mergeRequests = context.Client.GetCommits(project.Id).GetRelatedMergeRequestsAsync(new RelatedMergeRequestsQuery { Sha = commit.Id }); + var mergeRequests = await GitLabTestContext.RetryUntilAsync( + () => context.Client.GetCommits(project.Id).GetRelatedMergeRequestsAsync(new RelatedMergeRequestsQuery { Sha = commit.Id }), + mergeRequests => mergeRequests.Any(), + TimeSpan.FromSeconds(10)); var mergeRequest = mergeRequests.Single(); Assert.AreEqual(mergeRequestTitle, mergeRequest.Title); diff --git a/NGitLab.Tests/Docker/GitLabDockerContainer.cs b/NGitLab.Tests/Docker/GitLabDockerContainer.cs index 5561ed8a..faa595f7 100644 --- a/NGitLab.Tests/Docker/GitLabDockerContainer.cs +++ b/NGitLab.Tests/Docker/GitLabDockerContainer.cs @@ -27,7 +27,7 @@ public class GitLabDockerContainer public const string ImageName = "gitlab/gitlab-ee"; // https://hub.docker.com/r/gitlab/gitlab-ee/tags/ - public const string GitLabDockerVersion = "14.10.5-ee.0"; // Keep in sync with .github/workflows/ci.yml + public const string GitLabDockerVersion = "15.4.6-ee.0"; // Keep in sync with .github/workflows/ci.yml private static string s_creationErrorMessage; private static readonly SemaphoreSlim s_setupLock = new(initialCount: 1, maxCount: 1); @@ -107,6 +107,10 @@ private static async Task ValidateDockerIsEnabled(DockerClient client) { await client.Images.ListImagesAsync(new ImagesListParameters()).ConfigureAwait(false); } + catch (ArgumentOutOfRangeException ex) when (ex.Message.StartsWith("The added or subtracted value results in an un-representable DateTime.", StringComparison.Ordinal)) + { + // Ignore https://github.com/rancher-sandbox/rancher-desktop/issues/5145 + } catch (Exception ex) { s_creationErrorMessage = "Cannot connect to Docker service. Make sure it's running on your machine before launching any tests.\nDetails: " + ex; diff --git a/NGitLab.Tests/Docker/GitLabTestContext.cs b/NGitLab.Tests/Docker/GitLabTestContext.cs index be631d67..421c64cd 100644 --- a/NGitLab.Tests/Docker/GitLabTestContext.cs +++ b/NGitLab.Tests/Docker/GitLabTestContext.cs @@ -199,10 +199,10 @@ public Group CreateGroup(Action configure = null) return client.Groups.Create(groupCreate); } - public (Project Project, MergeRequest MergeRequest) CreateMergeRequest() + public (Project Project, MergeRequest MergeRequest) CreateMergeRequest(Action configure = null, Action configureProject = null) { var client = Client; - var project = CreateProject(initializeWithCommits: true); + var project = CreateProject(configureProject, initializeWithCommits: true); const string BranchForMRName = "branch-for-mr"; s_gitlabRetryPolicy.Execute(() => client.GetRepository(project.Id).Files.Create(new FileUpsert { Branch = project.DefaultBranch, CommitMessage = "test", Content = "test", Path = "test.md" })); @@ -224,12 +224,15 @@ public Group CreateGroup(Action configure = null) s_gitlabRetryPolicy.Execute(() => client.GetRepository(project.Id).Files.Update(new FileUpsert { Branch = BranchForMRName, CommitMessage = "test", Content = "test2", Path = "test.md" })); - var mr = client.GetMergeRequest(project.Id).Create(new MergeRequestCreate + var mergeRequestCreate = new MergeRequestCreate { SourceBranch = BranchForMRName, TargetBranch = project.DefaultBranch, Title = "test", - }); + }; + + configure?.Invoke(mergeRequestCreate); + var mr = client.GetMergeRequest(project.Id).Create(mergeRequestCreate); return (project, mr); } @@ -255,13 +258,20 @@ public int GetRandomNumber() return RandomNumberGenerator.GetInt32(0, int.MaxValue); } - public void ReportTestAsInconclusiveIfVersionOutOfRange(VersionRange versionRange) + public bool IsGitLabVersionInRange(VersionRange versionRange, out string gitLabVersion) + { + var currentVersion = Client.Version.Get(); + gitLabVersion = currentVersion.Version; + + // Although a GitLab version is not a NuGet version, let's consider it as one to determine range inclusion + return NuGetVersion.TryParse(gitLabVersion, out var nuGetVersion) && + versionRange.Satisfies(nuGetVersion); + } + + public void ReportTestAsInconclusiveIfGitLabVersionOutOfRange(VersionRange versionRange) { - var gitLabVersion = Client.Version.Get(); - if (!NuGetVersion.TryParse(gitLabVersion.Version, out var currentVersion)) - return; - if (!versionRange.Satisfies(currentVersion)) - Assert.Inconclusive($"Test supported in version range '{versionRange}', but currently running against '{currentVersion}'"); + if (!IsGitLabVersionInRange(versionRange, out var gitLabVersion)) + Assert.Inconclusive($"Test supported by GitLab '{versionRange}', but currently running against '{gitLabVersion}'"); } private IGitLabClient CreateClient(string token) diff --git a/NGitLab.Tests/Docker/GitLabTestContextRequestOptions.cs b/NGitLab.Tests/Docker/GitLabTestContextRequestOptions.cs index 32d10a47..f7d45b65 100644 --- a/NGitLab.Tests/Docker/GitLabTestContextRequestOptions.cs +++ b/NGitLab.Tests/Docker/GitLabTestContextRequestOptions.cs @@ -199,13 +199,21 @@ private static void LogHeaders(StringBuilder sb, WebHeaderCollection headers) foreach (var headerValue in headerValues) { + sb.Append(headerName).Append(": "); if (string.Equals(headerName, "Private-Token", StringComparison.OrdinalIgnoreCase)) { - sb.Append("Private-Token").Append(": ****** ").AppendLine(); + sb.AppendLine("******"); + } + else if (string.Equals(headerName, "Authorization", StringComparison.OrdinalIgnoreCase)) + { + const string BearerTokenPrefix = "Bearer "; + if (headerValue.StartsWith(BearerTokenPrefix, StringComparison.Ordinal)) + sb.Append(BearerTokenPrefix); + sb.AppendLine("******"); } else { - sb.Append(headerName).Append(": ").Append(headerValue).AppendLine(); + sb.AppendLine(headerValue); } } } diff --git a/NGitLab.Tests/HttpRequestorTests.cs b/NGitLab.Tests/HttpRequestorTests.cs index 85288e50..83b9a515 100644 --- a/NGitLab.Tests/HttpRequestorTests.cs +++ b/NGitLab.Tests/HttpRequestorTests.cs @@ -125,6 +125,22 @@ public async Task Test_impersonation_via_sudo_and_user_id() Assert.AreEqual(commonUserSession.Id, issue.Author.Id); } + [Test] + public async Task Test_authorization_header_uses_bearer() + { + // Arrange + using var context = await GitLabTestContext.CreateAsync(); + var commonUserClient = context.Client; + string expectedHeaderValue = string.Concat("Bearer ", context.DockerContainer.Credentials.UserToken); + + // Act + var project = commonUserClient.Projects.Accessible.First(); + + // Assert + var actualHeaderValue = context.LastRequest.Headers[HttpRequestHeader.Authorization]; + Assert.AreEqual(expectedHeaderValue, actualHeaderValue); + } + private sealed class MockRequestOptions : RequestOptions { public string HttpRequestSudoHeader { get; set; } diff --git a/NGitLab.Tests/IssueTests.cs b/NGitLab.Tests/IssueTests.cs index b82e1113..cb975f6d 100644 --- a/NGitLab.Tests/IssueTests.cs +++ b/NGitLab.Tests/IssueTests.cs @@ -293,5 +293,23 @@ public async Task Test_get_new_and_updated_issue_with_duedate() Assert.AreEqual(updatedDueDate, updatedIssue.DueDate); } + + [Test] + [NGitLabRetry] + public async Task Test_get_linked_issue() + { + using var context = await GitLabTestContext.CreateAsync(); + var project = context.CreateProject(); + var issuesClient = context.Client.Issues; + + var issue1 = await issuesClient.CreateAsync(new IssueCreate { ProjectId = project.Id, Title = "title1" }); + var issue2 = await issuesClient.CreateAsync(new IssueCreate { ProjectId = project.Id, Title = "title2", Description = "related to #1" }); + var linked = issuesClient.CreateLinkBetweenIssues(project.Id, issue1.IssueId, project.Id, issue2.IssueId); + Assert.IsTrue(linked, "Expected true for create Link between issues"); + var issues = issuesClient.LinkedToAsync(project.Id, issue1.IssueId).ToList(); + + // for now, no API to link issues so not links exist but API should not throw + Assert.AreEqual(1, issues.Count, "Expected 1. Got {0}", issues.Count); + } } } diff --git a/NGitLab.Tests/JobTests.cs b/NGitLab.Tests/JobTests.cs index 4911446c..247cdf6b 100644 --- a/NGitLab.Tests/JobTests.cs +++ b/NGitLab.Tests/JobTests.cs @@ -10,7 +10,7 @@ namespace NGitLab.Tests { public class JobTests { - internal static void AddGitLabCiFile(IGitLabClient client, Project project, int jobCount = 1, bool manualAction = false, string branch = null) + internal static void AddGitLabCiFile(IGitLabClient client, Project project, int jobCount = 1, bool manualAction = false, string branch = null, bool pipelineSucceeds = true) { var content = @" variables: @@ -24,6 +24,7 @@ internal static void AddGitLabCiFile(IGitLabClient client, Project project, int script: - echo test - echo test > file{i.ToString(CultureInfo.InvariantCulture)}.txt + - exit {(pipelineSucceeds ? "0" : "1")} artifacts: paths: - '*.txt' diff --git a/NGitLab.Tests/MergeRequest/MergeRequestChangesClientTests.cs b/NGitLab.Tests/MergeRequest/MergeRequestChangesClientTests.cs index fcb44a07..90f96152 100644 --- a/NGitLab.Tests/MergeRequest/MergeRequestChangesClientTests.cs +++ b/NGitLab.Tests/MergeRequest/MergeRequestChangesClientTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Linq; using System.Threading.Tasks; using NGitLab.Tests.Docker; using NUnit.Framework; @@ -14,7 +16,12 @@ public async Task GetChangesOnMergeRequest() var (project, mergeRequest) = context.CreateMergeRequest(); var mergeRequestClient = context.Client.GetMergeRequest(project.Id); var mergeRequestChanges = mergeRequestClient.Changes(mergeRequest.Iid); - var changes = mergeRequestChanges.MergeRequestChange.Changes; + + var changes = await GitLabTestContext.RetryUntilAsync( + () => mergeRequestChanges.MergeRequestChange.Changes, + changes => changes.Any(), + TimeSpan.FromSeconds(10)); + Assert.AreEqual(1, changes.Length); Assert.AreEqual(100644, changes[0].AMode); Assert.AreEqual(100644, changes[0].BMode); diff --git a/NGitLab.Tests/MergeRequest/MergeRequestClientTests.cs b/NGitLab.Tests/MergeRequest/MergeRequestClientTests.cs index 3a25c236..859fb59b 100644 --- a/NGitLab.Tests/MergeRequest/MergeRequestClientTests.cs +++ b/NGitLab.Tests/MergeRequest/MergeRequestClientTests.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Net; using System.Threading.Tasks; -using Meziantou.Framework.Versioning; using NGitLab.Models; using NGitLab.Tests.Docker; using NuGet.Versioning; @@ -26,6 +25,7 @@ public async Task Test_merge_request_api() ListMergeRequest(mergeRequestClient, mergeRequest); mergeRequest = UpdateMergeRequest(mergeRequestClient, mergeRequest); + Test_can_update_labels_with_delta(mergeRequestClient, mergeRequest); Test_can_update_a_subset_of_merge_request_fields(mergeRequestClient, mergeRequest); await GitLabTestContext.RetryUntilAsync( @@ -49,6 +49,11 @@ await GitLabTestContext.RetryUntilAsync( // .ConfigureAwait(false); var commits = mergeRequestClient.Commits(mergeRequest.Iid).All; Assert.IsTrue(commits.Any(), "Can return the commits"); + + if (context.IsGitLabVersionInRange(VersionRange.Parse("[15.6,)"), out _)) + Assert.IsNotNull(mergeRequest.DetailedMergeStatus.EnumValue); + else + Assert.IsNull(mergeRequest.DetailedMergeStatus.EnumValue); } [Test] @@ -83,7 +88,12 @@ public async Task Test_merge_request_rebase() "There should be a 1-commit divergence between the default branch NOW and its state at the moment the MR was created"); RebaseMergeRequest(mergeRequestClient, mergeRequest); - var commits = mergeRequestClient.Commits(mergeRequest.Iid).All; + + var commits = await GitLabTestContext.RetryUntilAsync( + () => mergeRequestClient.Commits(mergeRequest.Iid).All, + commits => commits.Any(), + TimeSpan.FromSeconds(10)); + Assert.IsTrue(commits.Any(), "Can return the commits"); } @@ -121,7 +131,11 @@ public async Task Test_merge_request_rebaseasync_skip_ci() var rebaseResult = await mergeRequestClient.RebaseAsync(mergeRequest.Iid, new MergeRequestRebase { SkipCi = true }); Assert.IsTrue(rebaseResult.RebaseInProgress); - var commits = mergeRequestClient.Commits(mergeRequest.Iid).All; + var commits = await GitLabTestContext.RetryUntilAsync( + () => mergeRequestClient.Commits(mergeRequest.Iid).All, + commits => commits.Any(), + TimeSpan.FromSeconds(10)); + Assert.IsTrue(commits.Any(), "Can return the commits"); } @@ -178,7 +192,7 @@ public async Task Test_merge_request_approvers() using var context = await GitLabTestContext.CreateAsync(); // https://about.gitlab.com/releases/2021/04/22/gitlab-13-11-released/#removal-of-merge-request-approvers-endpoint-in-favor-of-approval-rules-api - context.ReportTestAsInconclusiveIfVersionOutOfRange(VersionRange.Parse("[,13.11)")); + context.ReportTestAsInconclusiveIfGitLabVersionOutOfRange(VersionRange.Parse("[,13.11)")); var (project, mergeRequest) = context.CreateMergeRequest(); var mergeRequestClient = context.Client.GetMergeRequest(project.Id); @@ -276,7 +290,11 @@ public async Task Test_merge_request_versions() var (project, mergeRequest) = context.CreateMergeRequest(); var mergeRequestClient = context.Client.GetMergeRequest(project.Id); - var versions = mergeRequestClient.GetVersionsAsync(mergeRequest.Iid); + var versions = await GitLabTestContext.RetryUntilAsync( + () => mergeRequestClient.GetVersionsAsync(mergeRequest.Iid), + versions => versions.Any(), + TimeSpan.FromSeconds(10)); + var version = versions.First(); Assert.AreEqual(mergeRequest.Sha, version.HeadCommitSha); @@ -335,6 +353,20 @@ private static void Test_can_update_a_subset_of_merge_request_fields(IMergeReque Assert.AreEqual(mergeRequest.Description, updated.Description); } + private static void Test_can_update_labels_with_delta(IMergeRequestClient mergeRequestClient, MergeRequest mergeRequest) + { + // Ensure original labels are "a,b" + CollectionAssert.AreEqual(new[] { "a", "b" }, mergeRequest.Labels); + + var updated = mergeRequestClient.Update(mergeRequest.Iid, new MergeRequestUpdate + { + RemoveLabels = "b", + AddLabels = "c,d", + }); + + CollectionAssert.AreEqual(new[] { "a", "c", "d" }, updated.Labels); + } + public static void AcceptMergeRequest(IMergeRequestClient mergeRequestClient, MergeRequest request) { Policy diff --git a/NGitLab.Tests/MergeRequest/MergeRequestDiscussionsClientTests.cs b/NGitLab.Tests/MergeRequest/MergeRequestDiscussionsClientTests.cs index fca5277a..bb4200ce 100644 --- a/NGitLab.Tests/MergeRequest/MergeRequestDiscussionsClientTests.cs +++ b/NGitLab.Tests/MergeRequest/MergeRequestDiscussionsClientTests.cs @@ -34,6 +34,26 @@ public async Task AddDiscussionToMergeRequest_DiscussionCreated() CollectionAssert.IsNotEmpty(discussions); } + [Test] + [NGitLabRetry] + public async Task GetDiscussion_DiscussionFound() + { + using var context = await GitLabTestContext.CreateAsync(); + var (project, mergeRequest) = context.CreateMergeRequest(); + var mergeRequestClient = context.Client.GetMergeRequest(project.Id); + var mergeRequestDiscussions = mergeRequestClient.Discussions(mergeRequest.Iid); + + const string discussionMessage = "Discussion for MR"; + var newDiscussion = new MergeRequestDiscussionCreate + { + Body = discussionMessage, + }; + var discussion = mergeRequestDiscussions.Add(newDiscussion); + + var gotDiscussion = await mergeRequestDiscussions.GetAsync(discussion.Id); + Assert.NotNull(gotDiscussion); + } + [Test] [NGitLabRetry] public async Task EditCommentFromDiscussion_CommentEdited() diff --git a/NGitLab.Tests/Milestone/MilestoneClientTests.cs b/NGitLab.Tests/Milestone/MilestoneClientTests.cs index 24ad7444..b5cc7e5a 100644 --- a/NGitLab.Tests/Milestone/MilestoneClientTests.cs +++ b/NGitLab.Tests/Milestone/MilestoneClientTests.cs @@ -63,6 +63,41 @@ public async Task Test_group_milestone_api() DeleteMilestone(context, MilestoneScope.Groups, group.Id, milestone); } + [Test] + [NGitLabRetry] + public async Task Test_project_milestone_merge_requests() + { + using var context = await GitLabTestContext.CreateAsync(); + var (project, mergeRequest) = context.CreateMergeRequest(); + + var milestoneClient = context.Client.GetMilestone(project.Id); + var milestone = CreateMilestone(context, MilestoneScope.Projects, project.Id, "my-super-milestone"); + + var mergeRequestClient = context.Client.GetMergeRequest(project.Id); + mergeRequestClient.Update(mergeRequest.Iid, new MergeRequestUpdate { MilestoneId = milestone.Id }); + + var mergeRequests = milestoneClient.GetMergeRequests(milestone.Id).ToArray(); + Assert.AreEqual(1, mergeRequests.Length, "The query retrieved all merged requests that assigned to the milestone."); + } + + [Test] + [NGitLabRetry] + public async Task Test_group_milestone_merge_requests() + { + using var context = await GitLabTestContext.CreateAsync(); + var group = context.CreateGroup(); + var (project, mergeRequest) = context.CreateMergeRequest(configureProject: project => project.NamespaceId = group.Id.ToString()); + + var milestoneClient = context.Client.GetGroupMilestone(group.Id); + var milestone = CreateMilestone(context, MilestoneScope.Groups, group.Id, "my-super-milestone"); + + var mergeRequestClient = context.Client.GetMergeRequest(project.Id); + mergeRequestClient.Update(mergeRequest.Iid, new MergeRequestUpdate { MilestoneId = milestone.Id }); + + var mergeRequests = milestoneClient.GetMergeRequests(milestone.Id).ToArray(); + Assert.AreEqual(1, mergeRequests.Length, "The query retrieved all merged requests that assigned to the milestone."); + } + private static Models.Milestone CreateMilestone(GitLabTestContext context, MilestoneScope scope, int id, string title) { var milestoneClient = scope == MilestoneScope.Projects ? context.Client.GetMilestone(id) : context.Client.GetGroupMilestone(id); @@ -79,6 +114,8 @@ private static Models.Milestone CreateMilestone(GitLabTestContext context, Miles Assert.That(milestone.Description, Is.EqualTo($"{title} description")); Assert.That(milestone.StartDate, Is.EqualTo("2017-08-20")); Assert.That(milestone.DueDate, Is.EqualTo("2017-09-20")); + Assert.That(milestone.ProjectId, scope == MilestoneScope.Projects ? Is.EqualTo(id) : Is.Null); + Assert.That(milestone.GroupId, scope == MilestoneScope.Groups ? Is.EqualTo(id) : Is.Null); return milestone; } @@ -100,6 +137,8 @@ private static Models.Milestone UpdateMilestone(GitLabTestContext context, Miles Assert.That(updatedMilestone.StartDate, Is.EqualTo("2018-08-20")); Assert.That(updatedMilestone.DueDate, Is.EqualTo("2018-09-20")); Assert.That(updatedMilestone.State, Is.EqualTo(milestone.State)); + Assert.That(milestone.ProjectId, scope == MilestoneScope.Projects ? Is.EqualTo(id) : Is.Null); + Assert.That(milestone.GroupId, scope == MilestoneScope.Groups ? Is.EqualTo(id) : Is.Null); return updatedMilestone; } @@ -118,6 +157,8 @@ private static Models.Milestone UpdatePartialMilestone(GitLabTestContext context Assert.That(updatedMilestone.StartDate, Is.EqualTo(milestone.StartDate)); Assert.That(updatedMilestone.DueDate, Is.EqualTo(milestone.DueDate)); Assert.That(updatedMilestone.State, Is.EqualTo(milestone.State)); + Assert.That(updatedMilestone.ProjectId, scope == MilestoneScope.Projects ? Is.EqualTo(id) : Is.Null); + Assert.That(updatedMilestone.GroupId, scope == MilestoneScope.Groups ? Is.EqualTo(id) : Is.Null); return updatedMilestone; } @@ -125,15 +166,17 @@ private static Models.Milestone UpdatePartialMilestone(GitLabTestContext context private static Models.Milestone ActivateMilestone(GitLabTestContext context, MilestoneScope scope, int id, Models.Milestone milestone) { var milestoneClient = scope == MilestoneScope.Projects ? context.Client.GetMilestone(id) : context.Client.GetGroupMilestone(id); - var closedMilestone = milestoneClient.Activate(milestone.Id); + var activeMilestone = milestoneClient.Activate(milestone.Id); - Assert.That(closedMilestone.State, Is.EqualTo(nameof(MilestoneState.active))); - Assert.That(closedMilestone.Title, Is.EqualTo(milestone.Title)); - Assert.That(closedMilestone.Description, Is.EqualTo(milestone.Description)); - Assert.That(closedMilestone.StartDate, Is.EqualTo(milestone.StartDate)); - Assert.That(closedMilestone.DueDate, Is.EqualTo(milestone.DueDate)); + Assert.That(activeMilestone.State, Is.EqualTo(nameof(MilestoneState.active))); + Assert.That(activeMilestone.Title, Is.EqualTo(milestone.Title)); + Assert.That(activeMilestone.Description, Is.EqualTo(milestone.Description)); + Assert.That(activeMilestone.StartDate, Is.EqualTo(milestone.StartDate)); + Assert.That(activeMilestone.DueDate, Is.EqualTo(milestone.DueDate)); + Assert.That(activeMilestone.ProjectId, scope == MilestoneScope.Projects ? Is.EqualTo(id) : Is.Null); + Assert.That(activeMilestone.GroupId, scope == MilestoneScope.Groups ? Is.EqualTo(id) : Is.Null); - return closedMilestone; + return activeMilestone; } private static Models.Milestone CloseMilestone(GitLabTestContext context, MilestoneScope scope, int id, Models.Milestone milestone) @@ -146,6 +189,8 @@ private static Models.Milestone CloseMilestone(GitLabTestContext context, Milest Assert.That(closedMilestone.Description, Is.EqualTo(milestone.Description)); Assert.That(closedMilestone.StartDate, Is.EqualTo(milestone.StartDate)); Assert.That(closedMilestone.DueDate, Is.EqualTo(milestone.DueDate)); + Assert.That(closedMilestone.ProjectId, scope == MilestoneScope.Projects ? Is.EqualTo(id) : Is.Null); + Assert.That(closedMilestone.GroupId, scope == MilestoneScope.Groups ? Is.EqualTo(id) : Is.Null); return closedMilestone; } diff --git a/NGitLab.Tests/NGitLab.Tests.csproj b/NGitLab.Tests/NGitLab.Tests.csproj index 73cb1933..bab4a6bb 100644 --- a/NGitLab.Tests/NGitLab.Tests.csproj +++ b/NGitLab.Tests/NGitLab.Tests.csproj @@ -14,14 +14,14 @@ - - - + + + - - - + + + all runtime; build; native; contentfiles; analyzers diff --git a/NGitLab.Tests/PipelineTests.cs b/NGitLab.Tests/PipelineTests.cs index bdf2ff38..80c004f9 100644 --- a/NGitLab.Tests/PipelineTests.cs +++ b/NGitLab.Tests/PipelineTests.cs @@ -200,13 +200,40 @@ public async Task Test_get_triggered_pipeline_variables() var trigger = triggers.Create("Test Trigger"); var ciJobToken = trigger.Token; - var pipeline = pipelineClient.CreatePipelineWithTrigger(ciJobToken, project.DefaultBranch, new Dictionary(StringComparer.InvariantCulture) { { "Test", "HelloWorld" } }); + var pipeline = pipelineClient.CreatePipelineWithTrigger(ciJobToken, project.DefaultBranch, new Dictionary(StringComparer.Ordinal) { { "Test", "HelloWorld" } }); var variables = pipelineClient.GetVariables(pipeline.Id); Assert.IsTrue(variables.Any(v => - v.Key.Equals("Test", StringComparison.InvariantCulture) && - v.Value.Equals("HelloWorld", StringComparison.InvariantCulture))); + v.Key.Equals("Test", StringComparison.Ordinal) && + v.Value.Equals("HelloWorld", StringComparison.Ordinal))); + } + + [Test] + [NGitLabRetry] + public async Task Test_retry() + { + using var context = await GitLabTestContext.CreateAsync(); + var project = context.CreateProject(); + var pipelineClient = context.Client.GetPipelines(project.Id); + + using (await context.StartRunnerForOneJobAsync(project.Id)) + { + JobTests.AddGitLabCiFile(context.Client, project, pipelineSucceeds: false); + var pipeline = await GitLabTestContext.RetryUntilAsync(() => pipelineClient.All.FirstOrDefault(), pipeline => + { + if (pipeline != null) + { + TestContext.WriteLine("Pipeline status: " + pipeline.Status); + return pipeline.Status is JobStatus.Failed; + } + + return false; + }, TimeSpan.FromMinutes(2)); + + var retriedPipeline = await pipelineClient.RetryAsync(pipeline.Id); + Assert.AreNotEqual(JobStatus.Failed, retriedPipeline.Status); // Should be created or running + } } } } diff --git a/NGitLab.Tests/ProjectLevelApprovalRulesClientTests.cs b/NGitLab.Tests/ProjectLevelApprovalRulesClientTests.cs index 3383c59a..846c3031 100644 --- a/NGitLab.Tests/ProjectLevelApprovalRulesClientTests.cs +++ b/NGitLab.Tests/ProjectLevelApprovalRulesClientTests.cs @@ -16,7 +16,7 @@ public class ProjectLevelApprovalRulesClientTests public async Task SetUp() { context = await GitLabTestContext.CreateAsync(); - context.ReportTestAsInconclusiveIfVersionOutOfRange(SupportedVersionRange); + context.ReportTestAsInconclusiveIfGitLabVersionOutOfRange(SupportedVersionRange); } [TearDown] diff --git a/NGitLab.Tests/ProtectedBranchTests.cs b/NGitLab.Tests/ProtectedBranchTests.cs index eb8055c8..260d72f9 100644 --- a/NGitLab.Tests/ProtectedBranchTests.cs +++ b/NGitLab.Tests/ProtectedBranchTests.cs @@ -33,7 +33,7 @@ public async Task ProtectBranch_Test() { new AccessLevelInfo { - AccessLevel = AccessLevel.NoAccess, + AccessLevel = AccessLevel.Admin, Description = "Example", }, }, diff --git a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs index 125adc57..0f756ccb 100644 --- a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs +++ b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs @@ -130,6 +130,102 @@ public async Task GetCommitBySha1Range() Assert.AreEqual(allCommits[3].Id, commits[1].Id); } + [Test] + [NGitLabRetry] + public async Task GetCommitsSince() + { + // Arrange + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 5); + + var defaultBranch = context.Project.DefaultBranch; + var since = DateTime.UtcNow; + var expectedSinceValue = Uri.EscapeDataString(since.ToString("s", CultureInfo.InvariantCulture)); + var commitRequest = new GetCommitsRequest + { + RefName = defaultBranch, + Since = since, + }; + + // Act + var commits = context.RepositoryClient.GetCommits(commitRequest).ToArray(); + + // Assert + var lastRequestQueryString = context.Context.LastRequest.RequestUri.Query; + + Assert.True(lastRequestQueryString.Contains($"since={expectedSinceValue}")); + } + + [Test] + [NGitLabRetry] + public async Task GetCommitsDoesntIncludeSinceWhenNotSpecified() + { + // Arrange + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 5); + + var defaultBranch = context.Project.DefaultBranch; + var commitRequest = new GetCommitsRequest + { + RefName = defaultBranch, + Since = null, + }; + + // Act + var commits = context.RepositoryClient.GetCommits(commitRequest).ToArray(); + + // Assert + var lastRequestQueryString = context.Context.LastRequest.RequestUri.Query; + + Assert.False(lastRequestQueryString.Contains("since=")); + } + + [Test] + [NGitLabRetry] + public async Task GetCommitsUntil() + { + // Arrange + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 5); + + var defaultBranch = context.Project.DefaultBranch; + var until = DateTime.UtcNow; + var expectedUntilValue = Uri.EscapeDataString(until.ToString("s", CultureInfo.InvariantCulture)); + var commitRequest = new GetCommitsRequest + { + RefName = defaultBranch, + Until = until, + }; + + // Act + var commits = context.RepositoryClient.GetCommits(commitRequest).ToArray(); + + // Assert + var lastRequestQueryString = context.Context.LastRequest.RequestUri.Query; + + Assert.True(lastRequestQueryString.Contains($"until={expectedUntilValue}")); + } + + [Test] + [NGitLabRetry] + public async Task GetCommitsDoesntIncludeUntilWhenNotSpecified() + { + // Arrange + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 5); + + var defaultBranch = context.Project.DefaultBranch; + var commitRequest = new GetCommitsRequest + { + RefName = defaultBranch, + Until = null, + }; + + // Act + var commits = context.RepositoryClient.GetCommits(commitRequest).ToArray(); + + // Assert + var lastRequestQueryString = context.Context.LastRequest.RequestUri.Query; + + Assert.False(lastRequestQueryString.Contains("until=")); + } + [Test] [NGitLabRetry] public async Task GetCommitDiff() diff --git a/NGitLab/GetCommitsRequest.cs b/NGitLab/GetCommitsRequest.cs index 3f3a9a75..ec24a860 100644 --- a/NGitLab/GetCommitsRequest.cs +++ b/NGitLab/GetCommitsRequest.cs @@ -1,4 +1,6 @@ -namespace NGitLab +using System; + +namespace NGitLab { public class GetCommitsRequest { @@ -13,5 +15,9 @@ public class GetCommitsRequest public int MaxResults { get; set; } public uint PerPage { get; set; } = DefaultPerPage; + + public DateTime? Since { get; set; } + + public DateTime? Until { get; set; } } } diff --git a/NGitLab/IIssueClient.cs b/NGitLab/IIssueClient.cs index 12e23ce9..a5145f6b 100644 --- a/NGitLab/IIssueClient.cs +++ b/NGitLab/IIssueClient.cs @@ -134,6 +134,23 @@ public interface IIssueClient /// The list of MR that are related this issue. IEnumerable RelatedTo(int projectId, int issueIid); + /// + /// Get all Issues that are linked to a particular issue of particular project. + /// + /// The project id. + /// The id of the issue in the project's scope. + /// The list of Issues linked to this issue. + GitLabCollectionResponse LinkedToAsync(int projectId, int issueId); + + /// + /// Create links between Issues. + /// + /// The project id. + /// The id of the issue in the project's scope. + /// The target project id. + /// The target id of the issue to link to. + bool CreateLinkBetweenIssues(int sourceProjectId, int sourceIssueId, int targetProjectId, int targetIssueId); + GitLabCollectionResponse RelatedToAsync(int projectId, int issueIid); /// diff --git a/NGitLab/IMergeRequestDiscussionClient.cs b/NGitLab/IMergeRequestDiscussionClient.cs index 65c9d3f9..0b735a49 100644 --- a/NGitLab/IMergeRequestDiscussionClient.cs +++ b/NGitLab/IMergeRequestDiscussionClient.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using NGitLab.Models; namespace NGitLab @@ -7,6 +9,10 @@ public interface IMergeRequestDiscussionClient { IEnumerable All { get; } + MergeRequestDiscussion Get(string id); + + Task GetAsync(string id, CancellationToken cancellationToken = default); + MergeRequestDiscussion Add(MergeRequestDiscussionCreate discussion); MergeRequestDiscussion Resolve(MergeRequestDiscussionResolve resolve); diff --git a/NGitLab/IMilestoneClient.cs b/NGitLab/IMilestoneClient.cs index fbeec980..d4fe4fbf 100644 --- a/NGitLab/IMilestoneClient.cs +++ b/NGitLab/IMilestoneClient.cs @@ -25,5 +25,7 @@ public interface IMilestoneClient Milestone Close(int milestoneId); Milestone Activate(int milestoneId); + + IEnumerable GetMergeRequests(int milestoneId); } } diff --git a/NGitLab/IPipelineClient.cs b/NGitLab/IPipelineClient.cs index 151d0ee5..5240b9d0 100644 --- a/NGitLab/IPipelineClient.cs +++ b/NGitLab/IPipelineClient.cs @@ -115,5 +115,7 @@ public interface IPipelineClient /// /// GitLabCollectionResponse GetBridgesAsync(PipelineBridgeQuery query); + + Task RetryAsync(int pipelineId, CancellationToken cancellationToken = default); } } diff --git a/NGitLab/Impl/ContributorClient.cs b/NGitLab/Impl/ContributorClient.cs index 0f88f056..56e6871c 100644 --- a/NGitLab/Impl/ContributorClient.cs +++ b/NGitLab/Impl/ContributorClient.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using NGitLab.Extensions; +using System; +using System.Collections.Generic; using NGitLab.Models; namespace NGitLab.Impl @@ -8,18 +8,22 @@ internal sealed class ContributorClient : IContributorClient { private readonly API _api; private readonly string _contributorPath; - private readonly int _projectId; - public ContributorClient(API api, string repoPath, int projectId) + public ContributorClient(API api, string repoPath) { _api = api; _contributorPath = repoPath + Contributor.Url; - _projectId = projectId; + } + + [Obsolete("Argument projectId is redundant, please use ContributorClient(API api, string repoPath) instead.")] + public ContributorClient(API api, string repoPath, int projectId) + : this(api, repoPath) + { } /// /// HACK: We force the order_by and sort due to a pagination bug from GitLab /// - public IEnumerable All => _api.Get().GetAll(_contributorPath + $"?id={_projectId.ToStringInvariant()}&order_by=commits&sort=desc"); + public IEnumerable All => _api.Get().GetAll(_contributorPath + $"?order_by=commits&sort=desc"); } } diff --git a/NGitLab/Impl/HttpRequestor.GitLabRequest.cs b/NGitLab/Impl/HttpRequestor.GitLabRequest.cs index 850980f3..54972426 100644 --- a/NGitLab/Impl/HttpRequestor.GitLabRequest.cs +++ b/NGitLab/Impl/HttpRequestor.GitLabRequest.cs @@ -48,7 +48,11 @@ public GitLabRequest(Uri url, MethodType method, object data, string apiToken, R if (apiToken != null) { - Headers.Add("Private-Token", apiToken); + // Use the 'Authorization: Bearer token' header as this provides flexibility to use + // personal, project, group and OAuth tokens. The 'PRIVATE-TOKEN' header does not + // provide OAuth token support. + // Reference: https://docs.gitlab.com/ee/api/rest/#personalprojectgroup-access-tokens + Headers.Add(HttpRequestHeader.Authorization, string.Concat("Bearer ", apiToken)); } if (!string.IsNullOrEmpty(options?.UserAgent)) diff --git a/NGitLab/Impl/IssueClient.cs b/NGitLab/Impl/IssueClient.cs index 4a1bb103..7835988e 100644 --- a/NGitLab/Impl/IssueClient.cs +++ b/NGitLab/Impl/IssueClient.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Threading; using System.Threading.Tasks; @@ -10,6 +11,8 @@ public class IssueClient : IIssueClient { private const string IssuesUrl = "/issues"; private const string IssueByIdUrl = "/issues/{0}"; + private const string LinkedIssuesByIdUrl = "/projects/{0}/issues/{1}/links"; + private const string CreateLinkBetweenIssuesUrl = "/projects/{0}/issues/{1}/links?target_project_id={2}&target_issue_iid={3}"; private const string GroupIssuesUrl = "/groups/{0}/issues"; private const string ProjectIssuesUrl = "/projects/{0}/issues"; private const string SingleIssueUrl = "/projects/{0}/issues/{1}"; @@ -145,6 +148,25 @@ public IEnumerable RelatedTo(int projectId, int issueIid) return _api.Get().GetAll(string.Format(CultureInfo.InvariantCulture, RelatedToUrl, projectId, issueIid)); } + public GitLabCollectionResponse LinkedToAsync(int projectId, int issueId) + { + return _api.Get().GetAllAsync(string.Format(CultureInfo.InvariantCulture, LinkedIssuesByIdUrl, projectId, issueId)); + } + + public bool CreateLinkBetweenIssues(int sourceProjectId, int sourceIssueId, int targetProjectId, + int targetIssueId) + { + try + { + _api.Post().Execute(string.Format(CultureInfo.InvariantCulture, CreateLinkBetweenIssuesUrl, sourceProjectId, sourceIssueId, targetProjectId, targetIssueId)); + return true; + } + catch (Exception) + { + return false; + } + } + public GitLabCollectionResponse RelatedToAsync(int projectId, int issueIid) { return _api.Get().GetAllAsync(string.Format(CultureInfo.InvariantCulture, RelatedToUrl, projectId, issueIid)); diff --git a/NGitLab/Impl/MergeRequestDiscussionClient.cs b/NGitLab/Impl/MergeRequestDiscussionClient.cs index 48db25df..7f0d457d 100644 --- a/NGitLab/Impl/MergeRequestDiscussionClient.cs +++ b/NGitLab/Impl/MergeRequestDiscussionClient.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.Globalization; +using System.Threading; +using System.Threading.Tasks; using NGitLab.Models; namespace NGitLab.Impl @@ -17,6 +19,10 @@ public MergeRequestDiscussionClient(API api, string projectPath, int mergeReques public IEnumerable All => _api.Get().GetAll(_discussionsPath); + public MergeRequestDiscussion Get(string id) => _api.Get().To($"{_discussionsPath}/{id}"); + + public Task GetAsync(string id, CancellationToken cancellationToken = default) => _api.Get().ToAsync($"{_discussionsPath}/{id}", cancellationToken); + public MergeRequestDiscussion Add(MergeRequestDiscussionCreate comment) => _api.Post().With(comment).To(_discussionsPath); public MergeRequestDiscussion Resolve(MergeRequestDiscussionResolve resolve) => _api.Put().With(resolve).To(_discussionsPath + "/" + resolve.Id); diff --git a/NGitLab/Impl/MilestoneClient.cs b/NGitLab/Impl/MilestoneClient.cs index 22387e36..5e0c00e6 100644 --- a/NGitLab/Impl/MilestoneClient.cs +++ b/NGitLab/Impl/MilestoneClient.cs @@ -57,6 +57,9 @@ public Milestone Activate(int milestoneId) => _api .Put().With(new MilestoneUpdateState { NewState = nameof(MilestoneStateEvent.activate) }) .To($"{_milestonePath}/{milestoneId.ToStringInvariant()}"); + public IEnumerable GetMergeRequests(int milestoneId) => _api + .Get().GetAll($"{_milestonePath}/{milestoneId.ToStringInvariant()}/merge_requests"); + public void Delete(int milestoneId) => _api .Delete() .Execute($"{_milestonePath}/{milestoneId.ToStringInvariant()}"); diff --git a/NGitLab/Impl/PipelineClient.cs b/NGitLab/Impl/PipelineClient.cs index aa9743dc..6f9305ac 100644 --- a/NGitLab/Impl/PipelineClient.cs +++ b/NGitLab/Impl/PipelineClient.cs @@ -209,5 +209,11 @@ private string CreateGetBridgesUrl(PipelineBridgeQuery query) url = Utils.AddParameter(url, "scope", query.Scope); return url; } + + public Task RetryAsync(int pipelineId, CancellationToken cancellationToken = default) + { + var url = $"{_pipelinesPath}/{pipelineId.ToStringInvariant()}/retry"; + return _api.Post().ToAsync(url, cancellationToken); + } } } diff --git a/NGitLab/Impl/RepositoryClient.cs b/NGitLab/Impl/RepositoryClient.cs index 27abfc2f..56a0c0ea 100644 --- a/NGitLab/Impl/RepositoryClient.cs +++ b/NGitLab/Impl/RepositoryClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using NGitLab.Extensions; @@ -24,7 +25,7 @@ public RepositoryClient(API api, int projectId) public ITagClient Tags => new TagClient(_api, _repoPath); - public IContributorClient Contributors => new ContributorClient(_api, _repoPath, _projectId); + public IContributorClient Contributors => new ContributorClient(_api, _repoPath); public IEnumerable Tree => _api.Get().GetAll(_repoPath + "/tree"); @@ -85,6 +86,16 @@ public IEnumerable GetCommits(GetCommitsRequest request) lst.Add($"first_parent={Uri.EscapeDataString(request.FirstParent.ToString())}"); } + if (request.Since.HasValue) + { + lst.Add($"since={Uri.EscapeDataString(request.Since.Value.ToString("s", CultureInfo.InvariantCulture))}"); + } + + if (request.Until.HasValue) + { + lst.Add($"until={Uri.EscapeDataString(request.Until.Value.ToString("s", CultureInfo.InvariantCulture))}"); + } + var perPage = request.MaxResults > 0 ? Math.Min(request.MaxResults, request.PerPage) : request.PerPage; lst.Add($"per_page={perPage.ToStringInvariant()}"); diff --git a/NGitLab/Models/DetailedMergeStatus.cs b/NGitLab/Models/DetailedMergeStatus.cs new file mode 100644 index 00000000..54f3504e --- /dev/null +++ b/NGitLab/Models/DetailedMergeStatus.cs @@ -0,0 +1,36 @@ +using System.Runtime.Serialization; + +namespace NGitLab.Models; + +/// +/// Values that represent the 'detailed_merge_status' potential values. +/// +public enum DetailedMergeStatus +{ + [EnumMember(Value = "blocked_status")] + BlockedStatus, + [EnumMember(Value = "broken_status")] + BrokenStatus, + [EnumMember(Value = "checking")] + Checking, + [EnumMember(Value = "unchecked")] + Unchecked, + [EnumMember(Value = "ci_must_pass")] + CiMustPass, + [EnumMember(Value = "ci_still_running")] + CiStillRunning, + [EnumMember(Value = "discussions_not_resolved")] + DiscussionsNotResolved, + [EnumMember(Value = "draft_status")] + DraftStatus, + [EnumMember(Value = "external_status_checks")] + ExternalStatusChecks, + [EnumMember(Value = "mergeable")] + Mergeable, + [EnumMember(Value = "not_approved")] + NotApproved, + [EnumMember(Value = "not_open")] + NotOpen, + [EnumMember(Value = "policies_denied")] + PoliciesDenied, +} diff --git a/NGitLab/Models/MergeRequest.cs b/NGitLab/Models/MergeRequest.cs index 5700a68e..ff8095f8 100644 --- a/NGitLab/Models/MergeRequest.cs +++ b/NGitLab/Models/MergeRequest.cs @@ -2,144 +2,143 @@ using System.Text.Json.Serialization; using NGitLab.Extensions; -namespace NGitLab.Models +namespace NGitLab.Models; + +public class MergeRequest { - public class MergeRequest - { - public const string Url = "/merge_requests"; + public const string Url = "/merge_requests"; + + [JsonPropertyName("id")] + public int Id; - [JsonPropertyName("id")] - public int Id; + [JsonPropertyName("iid")] + public int Iid; - [JsonPropertyName("iid")] - public int Iid; + [JsonPropertyName("state")] + public string State; - [JsonPropertyName("state")] - public string State; + [JsonPropertyName("title")] + public string Title; - [JsonPropertyName("title")] - public string Title; + [JsonPropertyName("assignee")] + public User Assignee; - [JsonPropertyName("assignee")] - public User Assignee; + [JsonPropertyName("author")] + public User Author; - [JsonPropertyName("author")] - public User Author; + [JsonPropertyName("created_at")] + public DateTime CreatedAt; - [JsonPropertyName("created_at")] - public DateTime CreatedAt; + [JsonPropertyName("description")] + public string Description; - [JsonPropertyName("description")] - public string Description; + [JsonPropertyName("downvotes")] + public int Downvotes; - [JsonPropertyName("downvotes")] - public int Downvotes; + [JsonPropertyName("upvotes")] + public int Upvotes; - [JsonPropertyName("upvotes")] - public int Upvotes; + [JsonPropertyName("updated_at")] + public DateTime UpdatedAt; - [JsonPropertyName("updated_at")] - public DateTime UpdatedAt; + [JsonPropertyName("target_branch")] + public string TargetBranch; - [JsonPropertyName("target_branch")] - public string TargetBranch; + [JsonPropertyName("source_branch")] + public string SourceBranch; - [JsonPropertyName("source_branch")] - public string SourceBranch; + [JsonPropertyName("project_id")] + public int ProjectId; - [JsonPropertyName("project_id")] - public int ProjectId; + [JsonPropertyName("source_project_id")] + public int SourceProjectId; - [JsonPropertyName("source_project_id")] - public int SourceProjectId; + [JsonPropertyName("target_project_id")] + public int TargetProjectId; - [JsonPropertyName("target_project_id")] - public int TargetProjectId; + [JsonPropertyName("work_in_progress")] + public bool? WorkInProgress; - [JsonPropertyName("work_in_progress")] - public bool? WorkInProgress; + [JsonPropertyName("milestone")] + public Milestone Milestone; - [JsonPropertyName("milestone")] - public Milestone Milestone; + [JsonPropertyName("labels")] + public string[] Labels; - [JsonPropertyName("labels")] - public string[] Labels; + [JsonPropertyName("merge_when_pipeline_succeeds")] + public bool MergeWhenPipelineSucceeds; - [JsonPropertyName("merge_when_pipeline_succeeds")] - public bool MergeWhenPipelineSucceeds; + [JsonPropertyName("merge_status")] + public string MergeStatus; - [JsonPropertyName("merge_status")] - public string MergeStatus; + [JsonPropertyName("sha")] + public string Sha; - [JsonPropertyName("sha")] - public string Sha; + [JsonPropertyName("merge_commit_sha")] + public string MergeCommitSha; - [JsonPropertyName("merge_commit_sha")] - public string MergeCommitSha; + [JsonPropertyName("squash_commit_sha")] + public string SquashCommitSha; - [JsonPropertyName("squash_commit_sha")] - public string SquashCommitSha; + [JsonPropertyName("diff_refs")] + public DiffRefs DiffRefs; - [JsonPropertyName("diff_refs")] - public DiffRefs DiffRefs; + [JsonPropertyName("should_remove_source_branch")] + public bool? ShouldRemoveSourceBranch; - [JsonPropertyName("should_remove_source_branch")] - public bool? ShouldRemoveSourceBranch; + [JsonPropertyName("force_remove_source_branch")] + public bool ForceRemoveSourceBranch; - [JsonPropertyName("force_remove_source_branch")] - public bool ForceRemoveSourceBranch; + [JsonPropertyName("squash")] + public bool Squash; - [JsonPropertyName("squash")] - public bool Squash; + [JsonPropertyName("changes_count")] + public string ChangesCount; - [JsonPropertyName("changes_count")] - public string ChangesCount; + [JsonPropertyName("web_url")] + public string WebUrl; - [JsonPropertyName("web_url")] - public string WebUrl; + [JsonPropertyName("merged_by")] + public User MergedBy; - [JsonPropertyName("merged_by")] - public User MergedBy; + [JsonPropertyName("merged_at")] + public DateTime? MergedAt; - [JsonPropertyName("merged_at")] - public DateTime? MergedAt; + [JsonPropertyName("closed_at")] + public DateTime? ClosedAt; - [JsonPropertyName("closed_at")] - public DateTime? ClosedAt; + [JsonPropertyName("closed_by")] + public User ClosedBy; - [JsonPropertyName("closed_by")] - public User ClosedBy; + [JsonPropertyName("assignees")] + public User[] Assignees; - [JsonPropertyName("assignees")] - public User[] Assignees; + [JsonPropertyName("reviewers")] + public User[] Reviewers; - [JsonPropertyName("reviewers")] - public User[] Reviewers; + [JsonPropertyName("allow_collaboration")] + public bool? AllowCollaboration; - [JsonPropertyName("allow_collaboration")] - public bool? AllowCollaboration; + [JsonPropertyName("head_pipeline")] + public Pipeline HeadPipeline; - [JsonPropertyName("head_pipeline")] - public Pipeline HeadPipeline; + [JsonPropertyName("rebase_in_progress")] + public bool RebaseInProgress; - [JsonPropertyName("rebase_in_progress")] - public bool RebaseInProgress; + [JsonPropertyName("diverged_commits_count")] + public int? DivergedCommitsCount { get; set; } - [JsonPropertyName("diverged_commits_count")] - public int? DivergedCommitsCount { get; set; } + [JsonPropertyName("has_conflicts")] + public bool HasConflicts { get; set; } - [JsonPropertyName("has_conflicts")] - public bool HasConflicts { get; set; } + [JsonPropertyName("blocking_discussions_resolved")] + public bool BlockingDiscussionsResolved { get; set; } - [JsonPropertyName("blocking_discussions_resolved")] - public bool BlockingDiscussionsResolved { get; set; } + [JsonPropertyName("user")] + public MergeRequestUserInfo User { get; set; } - [JsonPropertyName("user")] - public MergeRequestUserInfo User { get; set; } + [JsonPropertyName("detailed_merge_status")] + public DynamicEnum DetailedMergeStatus { get; set; } - public override string ToString() - { - return $"!{Id.ToStringInvariant()}: {Title}"; - } - } + public override string ToString() => $"!{Id.ToStringInvariant()}: {Title}"; } diff --git a/NGitLab/Models/MergeRequestUpdate.cs b/NGitLab/Models/MergeRequestUpdate.cs index 7d048d7e..6a71ce36 100644 --- a/NGitLab/Models/MergeRequestUpdate.cs +++ b/NGitLab/Models/MergeRequestUpdate.cs @@ -31,6 +31,12 @@ public class MergeRequestUpdate [JsonPropertyName("labels")] public string Labels; + [JsonPropertyName("add_labels")] + public string AddLabels; + + [JsonPropertyName("remove_labels")] + public string RemoveLabels; + [JsonPropertyName("milestone_id")] public int? MilestoneId; diff --git a/NGitLab/Models/Milestone.cs b/NGitLab/Models/Milestone.cs index 6d9b8e87..2f7ed799 100644 --- a/NGitLab/Models/Milestone.cs +++ b/NGitLab/Models/Milestone.cs @@ -20,6 +20,12 @@ public class Milestone [JsonPropertyName("due_date")] public string DueDate; + [JsonPropertyName("group_id")] + public int? GroupId; + + [JsonPropertyName("project_id")] + public int? ProjectId; + [JsonPropertyName("start_date")] public string StartDate; diff --git a/NGitLab/Models/Project.cs b/NGitLab/Models/Project.cs index b4fc76d4..4a64c029 100644 --- a/NGitLab/Models/Project.cs +++ b/NGitLab/Models/Project.cs @@ -69,6 +69,9 @@ public class Project [JsonPropertyName("issues_access_level")] public string IssuesAccessLevel; + [JsonPropertyName("merge_pipelines_enabled")] + public bool MergePipelinesEnabled; + [JsonPropertyName("merge_requests_enabled")] [Obsolete("Deprecated by GitLab. Use MergeRequestsAccessLevel instead")] public bool MergeRequestsEnabled; @@ -76,6 +79,9 @@ public class Project [JsonPropertyName("merge_requests_access_level")] public string MergeRequestsAccessLevel; + [JsonPropertyName("merge_trains_enabled")] + public bool MergeTrainsEnabled; + [JsonPropertyName("repository_access_level")] public RepositoryAccessLevel RepositoryAccessLevel; diff --git a/NGitLab/Models/ProjectCreate.cs b/NGitLab/Models/ProjectCreate.cs index 71e9ed70..71dd1963 100644 --- a/NGitLab/Models/ProjectCreate.cs +++ b/NGitLab/Models/ProjectCreate.cs @@ -11,7 +11,6 @@ public class ProjectCreate [JsonPropertyName("name")] public string Name; - [Required] [JsonPropertyName("namespace_id")] public string NamespaceId; @@ -35,6 +34,9 @@ public class ProjectCreate [JsonIgnore] public bool WallEnabled; + [JsonPropertyName("merge_pipelines_enabled")] + public bool MergePipelinesEnabled; + [JsonPropertyName("merge_requests_enabled")] [Obsolete("Deprecated by GitLab. Use MergeRequestsAccessLevel instead")] public bool MergeRequestsEnabled; @@ -42,6 +44,9 @@ public class ProjectCreate [JsonPropertyName("merge_requests_access_level")] public string MergeRequestsAccessLevel; + [JsonPropertyName("merge_trains_enabled")] + public bool MergeTrainsEnabled; + [JsonPropertyName("snippets_enabled")] [Obsolete("Deprecated by GitLab. Use SnippetsAccessLevel instead")] public bool SnippetsEnabled; diff --git a/NGitLab/Models/ProjectUpdate.cs b/NGitLab/Models/ProjectUpdate.cs index 0eda8afa..cd78f8b9 100644 --- a/NGitLab/Models/ProjectUpdate.cs +++ b/NGitLab/Models/ProjectUpdate.cs @@ -25,6 +25,9 @@ public sealed class ProjectUpdate [JsonPropertyName("issues_access_level")] public string IssuesAccessLeve { get; set; } + [JsonPropertyName("merge_pipelines_enabled")] + public bool MergePipelinesEnabled { get; set; } + [JsonPropertyName("merge_requests_enabled")] [Obsolete("Deprecated by GitLab. Use MergeRequestsAccessLevel instead")] public bool? MergeRequestsEnabled { get; set; } @@ -32,6 +35,9 @@ public sealed class ProjectUpdate [JsonPropertyName("merge_requests_access_level")] public string MergeRequestsAccessLevel { get; set; } + [JsonPropertyName("merge_trains_enabled")] + public bool MergeTrainsEnabled { get; set; } + [JsonPropertyName("jobs_enabled")] [Obsolete("Deprecated by GitLab. Use BuildsAccessLevel instead")] public bool? JobsEnabled { get; set; } diff --git a/NGitLab/NGitLab.csproj b/NGitLab/NGitLab.csproj index 4fa8492c..00e52c6f 100644 --- a/NGitLab/NGitLab.csproj +++ b/NGitLab/NGitLab.csproj @@ -1,4 +1,4 @@ - + net461;netstandard2.0 @@ -22,6 +22,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/NGitLab/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI.Unshipped.txt index 28e05a85..4f9b3455 100644 --- a/NGitLab/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI.Unshipped.txt @@ -35,6 +35,10 @@ NGitLab.GetCommitsRequest.PerPage.get -> uint NGitLab.GetCommitsRequest.PerPage.set -> void NGitLab.GetCommitsRequest.RefName.get -> string NGitLab.GetCommitsRequest.RefName.set -> void +NGitLab.GetCommitsRequest.Since.get -> System.DateTime? +NGitLab.GetCommitsRequest.Since.set -> void +NGitLab.GetCommitsRequest.Until.get -> System.DateTime? +NGitLab.GetCommitsRequest.Until.set -> void NGitLab.GitLabClient NGitLab.GitLabClient.AdvancedSearch.get -> NGitLab.ISearchClient NGitLab.GitLabClient.Deployments.get -> NGitLab.IDeploymentClient @@ -249,6 +253,7 @@ NGitLab.IIssueClient.ClosedBy(int projectId, int issueIid) -> System.Collections NGitLab.IIssueClient.ClosedByAsync(int projectId, int issueIid) -> NGitLab.GitLabCollectionResponse NGitLab.IIssueClient.Create(NGitLab.Models.IssueCreate issueCreate) -> NGitLab.Models.Issue NGitLab.IIssueClient.CreateAsync(NGitLab.Models.IssueCreate issueCreate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +NGitLab.IIssueClient.CreateLinkBetweenIssues(int sourceProjectId, int sourceIssueId, int targetProjectId, int targetIssueId) -> bool NGitLab.IIssueClient.Edit(NGitLab.Models.IssueEdit issueEdit) -> NGitLab.Models.Issue NGitLab.IIssueClient.EditAsync(NGitLab.Models.IssueEdit issueEdit, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task NGitLab.IIssueClient.ForGroupsAsync(int groupId) -> NGitLab.GitLabCollectionResponse @@ -263,6 +268,7 @@ NGitLab.IIssueClient.GetAsync(int projectId, NGitLab.Models.IssueQuery query) -> NGitLab.IIssueClient.GetAsync(NGitLab.Models.IssueQuery query) -> NGitLab.GitLabCollectionResponse NGitLab.IIssueClient.GetById(int issueId) -> NGitLab.Models.Issue NGitLab.IIssueClient.GetByIdAsync(int issueId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +NGitLab.IIssueClient.LinkedToAsync(int projectId, int issueId) -> NGitLab.GitLabCollectionResponse NGitLab.IIssueClient.Owned.get -> System.Collections.Generic.IEnumerable NGitLab.IIssueClient.RelatedTo(int projectId, int issueIid) -> System.Collections.Generic.IEnumerable NGitLab.IIssueClient.RelatedToAsync(int projectId, int issueIid) -> NGitLab.GitLabCollectionResponse @@ -362,6 +368,8 @@ NGitLab.IMergeRequestDiscussionClient NGitLab.IMergeRequestDiscussionClient.Add(NGitLab.Models.MergeRequestDiscussionCreate discussion) -> NGitLab.Models.MergeRequestDiscussion NGitLab.IMergeRequestDiscussionClient.All.get -> System.Collections.Generic.IEnumerable NGitLab.IMergeRequestDiscussionClient.Delete(string discussionId, long commentId) -> void +NGitLab.IMergeRequestDiscussionClient.Get(string id) -> NGitLab.Models.MergeRequestDiscussion +NGitLab.IMergeRequestDiscussionClient.GetAsync(string id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task NGitLab.IMergeRequestDiscussionClient.Resolve(NGitLab.Models.MergeRequestDiscussionResolve resolve) -> NGitLab.Models.MergeRequestDiscussion NGitLab.IMilestoneClient NGitLab.IMilestoneClient.Activate(int milestoneId) -> NGitLab.Models.Milestone @@ -371,6 +379,7 @@ NGitLab.IMilestoneClient.Close(int milestoneId) -> NGitLab.Models.Milestone NGitLab.IMilestoneClient.Create(NGitLab.Models.MilestoneCreate milestone) -> NGitLab.Models.Milestone NGitLab.IMilestoneClient.Delete(int milestoneId) -> void NGitLab.IMilestoneClient.Get(NGitLab.Models.MilestoneQuery query) -> System.Collections.Generic.IEnumerable +NGitLab.IMilestoneClient.GetMergeRequests(int milestoneId) -> System.Collections.Generic.IEnumerable NGitLab.IMilestoneClient.Scope.get -> NGitLab.Impl.MilestoneScope NGitLab.IMilestoneClient.this[int id].get -> NGitLab.Models.Milestone NGitLab.IMilestoneClient.Update(int milestoneId, NGitLab.Models.MilestoneUpdate milestone) -> NGitLab.Models.Milestone @@ -498,6 +507,7 @@ NGitLab.Impl.IssueClient.ClosedBy(int projectId, int issueIid) -> System.Collect NGitLab.Impl.IssueClient.ClosedByAsync(int projectId, int issueIid) -> NGitLab.GitLabCollectionResponse NGitLab.Impl.IssueClient.Create(NGitLab.Models.IssueCreate issueCreate) -> NGitLab.Models.Issue NGitLab.Impl.IssueClient.CreateAsync(NGitLab.Models.IssueCreate issueCreate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +NGitLab.Impl.IssueClient.CreateLinkBetweenIssues(int sourceProjectId, int sourceIssueId, int targetProjectId, int targetIssueId) -> bool NGitLab.Impl.IssueClient.Edit(NGitLab.Models.IssueEdit issueEdit) -> NGitLab.Models.Issue NGitLab.Impl.IssueClient.EditAsync(NGitLab.Models.IssueEdit issueEdit, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task NGitLab.Impl.IssueClient.ForGroupsAsync(int groupId) -> NGitLab.GitLabCollectionResponse @@ -513,6 +523,7 @@ NGitLab.Impl.IssueClient.GetAsync(NGitLab.Models.IssueQuery query) -> NGitLab.Gi NGitLab.Impl.IssueClient.GetById(int issueId) -> NGitLab.Models.Issue NGitLab.Impl.IssueClient.GetByIdAsync(int issueId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task NGitLab.Impl.IssueClient.IssueClient(NGitLab.Impl.API api) -> void +NGitLab.Impl.IssueClient.LinkedToAsync(int projectId, int issueId) -> NGitLab.GitLabCollectionResponse NGitLab.Impl.IssueClient.Owned.get -> System.Collections.Generic.IEnumerable NGitLab.Impl.IssueClient.RelatedTo(int projectId, int issueIid) -> System.Collections.Generic.IEnumerable NGitLab.Impl.IssueClient.RelatedToAsync(int projectId, int issueIid) -> NGitLab.GitLabCollectionResponse @@ -619,6 +630,8 @@ NGitLab.Impl.MergeRequestDiscussionClient NGitLab.Impl.MergeRequestDiscussionClient.Add(NGitLab.Models.MergeRequestDiscussionCreate comment) -> NGitLab.Models.MergeRequestDiscussion NGitLab.Impl.MergeRequestDiscussionClient.All.get -> System.Collections.Generic.IEnumerable NGitLab.Impl.MergeRequestDiscussionClient.Delete(string discussionId, long commentId) -> void +NGitLab.Impl.MergeRequestDiscussionClient.Get(string id) -> NGitLab.Models.MergeRequestDiscussion +NGitLab.Impl.MergeRequestDiscussionClient.GetAsync(string id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task NGitLab.Impl.MergeRequestDiscussionClient.MergeRequestDiscussionClient(NGitLab.Impl.API api, string projectPath, int mergeRequestIid) -> void NGitLab.Impl.MergeRequestDiscussionClient.Resolve(NGitLab.Models.MergeRequestDiscussionResolve resolve) -> NGitLab.Models.MergeRequestDiscussion NGitLab.Impl.MethodType @@ -638,6 +651,7 @@ NGitLab.Impl.MilestoneClient.Close(int milestoneId) -> NGitLab.Models.Milestone NGitLab.Impl.MilestoneClient.Create(NGitLab.Models.MilestoneCreate milestone) -> NGitLab.Models.Milestone NGitLab.Impl.MilestoneClient.Delete(int milestoneId) -> void NGitLab.Impl.MilestoneClient.Get(NGitLab.Models.MilestoneQuery query) -> System.Collections.Generic.IEnumerable +NGitLab.Impl.MilestoneClient.GetMergeRequests(int milestoneId) -> System.Collections.Generic.IEnumerable NGitLab.Impl.MilestoneClient.MilestoneClient(NGitLab.Impl.API api, int projectId) -> void NGitLab.Impl.MilestoneClient.Scope.get -> NGitLab.Impl.MilestoneScope NGitLab.Impl.MilestoneClient.this[int id].get -> NGitLab.Models.Milestone @@ -671,6 +685,7 @@ NGitLab.Impl.PipelineClient.GetTestReportsSummary(int pipelineId) -> NGitLab.Mod NGitLab.Impl.PipelineClient.GetVariables(int pipelineId) -> System.Collections.Generic.IEnumerable NGitLab.Impl.PipelineClient.GetVariablesAsync(int pipelineId) -> NGitLab.GitLabCollectionResponse NGitLab.Impl.PipelineClient.PipelineClient(NGitLab.Impl.API api, int projectId) -> void +NGitLab.Impl.PipelineClient.RetryAsync(int pipelineId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task NGitLab.Impl.PipelineClient.Search(NGitLab.Models.PipelineQuery query) -> System.Collections.Generic.IEnumerable NGitLab.Impl.PipelineClient.SearchAsync(NGitLab.Models.PipelineQuery query) -> NGitLab.GitLabCollectionResponse NGitLab.Impl.PipelineClient.this[int id].get -> NGitLab.Models.Pipeline @@ -843,6 +858,7 @@ NGitLab.IPipelineClient.GetTestReports(int pipelineId) -> NGitLab.Models.TestRep NGitLab.IPipelineClient.GetTestReportsSummary(int pipelineId) -> NGitLab.Models.TestReportSummary NGitLab.IPipelineClient.GetVariables(int pipelineId) -> System.Collections.Generic.IEnumerable NGitLab.IPipelineClient.GetVariablesAsync(int pipelineId) -> NGitLab.GitLabCollectionResponse +NGitLab.IPipelineClient.RetryAsync(int pipelineId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task NGitLab.IPipelineClient.Search(NGitLab.Models.PipelineQuery query) -> System.Collections.Generic.IEnumerable NGitLab.IPipelineClient.SearchAsync(NGitLab.Models.PipelineQuery query) -> NGitLab.GitLabCollectionResponse NGitLab.IPipelineClient.this[int id].get -> NGitLab.Models.Pipeline @@ -1365,6 +1381,20 @@ NGitLab.Models.DeploymentStatus.created = 0 -> NGitLab.Models.DeploymentStatus NGitLab.Models.DeploymentStatus.failed = 3 -> NGitLab.Models.DeploymentStatus NGitLab.Models.DeploymentStatus.running = 1 -> NGitLab.Models.DeploymentStatus NGitLab.Models.DeploymentStatus.success = 2 -> NGitLab.Models.DeploymentStatus +NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.BlockedStatus = 0 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.BrokenStatus = 1 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.Checking = 2 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.CiMustPass = 4 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.CiStillRunning = 5 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.DiscussionsNotResolved = 6 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.DraftStatus = 7 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.ExternalStatusChecks = 8 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.Mergeable = 9 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.NotApproved = 10 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.NotOpen = 11 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.PoliciesDenied = 12 -> NGitLab.Models.DetailedMergeStatus +NGitLab.Models.DetailedMergeStatus.Unchecked = 3 -> NGitLab.Models.DetailedMergeStatus NGitLab.Models.Diff NGitLab.Models.Diff.AMode -> string NGitLab.Models.Diff.BMode -> string @@ -2049,6 +2079,8 @@ NGitLab.Models.MergeRequest.ClosedAt -> System.DateTime? NGitLab.Models.MergeRequest.ClosedBy -> NGitLab.Models.User NGitLab.Models.MergeRequest.CreatedAt -> System.DateTime NGitLab.Models.MergeRequest.Description -> string +NGitLab.Models.MergeRequest.DetailedMergeStatus.get -> NGitLab.DynamicEnum +NGitLab.Models.MergeRequest.DetailedMergeStatus.set -> void NGitLab.Models.MergeRequest.DiffRefs -> NGitLab.Models.DiffRefs NGitLab.Models.MergeRequest.DivergedCommitsCount.get -> int? NGitLab.Models.MergeRequest.DivergedCommitsCount.set -> void @@ -2261,6 +2293,7 @@ NGitLab.Models.MergeRequestStateEvent.close = 0 -> NGitLab.Models.MergeRequestSt NGitLab.Models.MergeRequestStateEvent.merge = 2 -> NGitLab.Models.MergeRequestStateEvent NGitLab.Models.MergeRequestStateEvent.reopen = 1 -> NGitLab.Models.MergeRequestStateEvent NGitLab.Models.MergeRequestUpdate +NGitLab.Models.MergeRequestUpdate.AddLabels -> string NGitLab.Models.MergeRequestUpdate.AllowCollaboration -> bool? NGitLab.Models.MergeRequestUpdate.AssigneeId -> int? NGitLab.Models.MergeRequestUpdate.AssigneeIds -> int[] @@ -2269,6 +2302,7 @@ NGitLab.Models.MergeRequestUpdate.Labels -> string NGitLab.Models.MergeRequestUpdate.MergeRequestUpdate() -> void NGitLab.Models.MergeRequestUpdate.MilestoneId -> int? NGitLab.Models.MergeRequestUpdate.NewState -> string +NGitLab.Models.MergeRequestUpdate.RemoveLabels -> string NGitLab.Models.MergeRequestUpdate.RemoveSourceBranch -> bool? NGitLab.Models.MergeRequestUpdate.ReviewerIds -> int[] NGitLab.Models.MergeRequestUpdate.SourceBranch -> string @@ -2305,9 +2339,11 @@ NGitLab.Models.Milestone NGitLab.Models.Milestone.CreatedAt -> System.DateTime NGitLab.Models.Milestone.Description -> string NGitLab.Models.Milestone.DueDate -> string +NGitLab.Models.Milestone.GroupId -> int? NGitLab.Models.Milestone.Id -> int NGitLab.Models.Milestone.Iid -> int NGitLab.Models.Milestone.Milestone() -> void +NGitLab.Models.Milestone.ProjectId -> int? NGitLab.Models.Milestone.StartDate -> string NGitLab.Models.Milestone.State -> string NGitLab.Models.Milestone.Title -> string @@ -2532,8 +2568,10 @@ NGitLab.Models.Project.LastActivityAt -> System.DateTime NGitLab.Models.Project.LfsEnabled -> bool NGitLab.Models.Project.Links -> NGitLab.Models.ProjectLinks NGitLab.Models.Project.MergeMethod -> string +NGitLab.Models.Project.MergePipelinesEnabled -> bool NGitLab.Models.Project.MergeRequestsAccessLevel -> string NGitLab.Models.Project.MergeRequestsEnabled -> bool +NGitLab.Models.Project.MergeTrainsEnabled -> bool NGitLab.Models.Project.Mirror -> bool NGitLab.Models.Project.MirrorOverwritesDivergedBranches -> bool NGitLab.Models.Project.MirrorTriggerBuilds -> bool @@ -2580,8 +2618,10 @@ NGitLab.Models.ProjectCreate.Description -> string NGitLab.Models.ProjectCreate.ImportUrl -> string NGitLab.Models.ProjectCreate.IssuesAccessLevel -> string NGitLab.Models.ProjectCreate.IssuesEnabled -> bool +NGitLab.Models.ProjectCreate.MergePipelinesEnabled -> bool NGitLab.Models.ProjectCreate.MergeRequestsAccessLevel -> string NGitLab.Models.ProjectCreate.MergeRequestsEnabled -> bool +NGitLab.Models.ProjectCreate.MergeTrainsEnabled -> bool NGitLab.Models.ProjectCreate.Name -> string NGitLab.Models.ProjectCreate.NamespaceId -> string NGitLab.Models.ProjectCreate.Path -> string @@ -2748,10 +2788,14 @@ NGitLab.Models.ProjectUpdate.JobsEnabled.get -> bool? NGitLab.Models.ProjectUpdate.JobsEnabled.set -> void NGitLab.Models.ProjectUpdate.LfsEnabled.get -> bool? NGitLab.Models.ProjectUpdate.LfsEnabled.set -> void +NGitLab.Models.ProjectUpdate.MergePipelinesEnabled.get -> bool +NGitLab.Models.ProjectUpdate.MergePipelinesEnabled.set -> void NGitLab.Models.ProjectUpdate.MergeRequestsAccessLevel.get -> string NGitLab.Models.ProjectUpdate.MergeRequestsAccessLevel.set -> void NGitLab.Models.ProjectUpdate.MergeRequestsEnabled.get -> bool? NGitLab.Models.ProjectUpdate.MergeRequestsEnabled.set -> void +NGitLab.Models.ProjectUpdate.MergeTrainsEnabled.get -> bool +NGitLab.Models.ProjectUpdate.MergeTrainsEnabled.set -> void NGitLab.Models.ProjectUpdate.Name.get -> string NGitLab.Models.ProjectUpdate.Name.set -> void NGitLab.Models.ProjectUpdate.OnlyAllowMergeIfAllDiscussionsAreResolved.get -> bool? diff --git a/README.md b/README.md index 561643b1..608a6ea3 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ## What is NGitLab? -*NGitLab* is a .NET REST client implementation of GitLab API with no external dependencies. +`NGitLab` is a .NET REST client implementation for the GitLab API. ## Usage -It's a wrapper of REST api. Read the [GitLab docs](https://github.com/gitlabhq/gitlabhq/tree/master/doc/api) and start using by creating a GitLabClient instance: +Start by creating a `GitLabClient` instance: ```csharp var client = new GitLabClient("https://mygitlab.example.com", "your_private_token"); @@ -14,13 +14,17 @@ var client = new GitLabClient("https://mygitlab.example.com", "your_private_toke Then use its properties. You can obtain the private token in your account page. You may want to create a custom user for the API usage. +For further info about the GitLab API, refer to [the official documentation](https://docs.gitlab.com/ee/api/rest/) + ## Where can I get it? -Get it from [NuGet](https://www.nuget.org/packages/NGitLab). You can simply install it with the Package Manager console: +Get it from [nuget.org](https://www.nuget.org/packages/NGitLab). You can simply install it using the `dotnet` CLI: - PM> Install-Package NGitLab +```PowerShell +dotnet add package NGitLab +``` -## Unit-Test +## Running Unit Tests locally - Install Docker on your machine - It's recommended to use WSL version 2: https://docs.microsoft.com/en-us/windows/wsl/install-win10