From f42eb75923c922e984ec2ddf97b702cfcab4c236 Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Wed, 1 Apr 2026 14:11:04 +0300 Subject: [PATCH 01/12] Added file folder picker --- Apps.GitLab/Apps.GitLab.csproj | 4 +- .../GitLabFilePickerDataHandler.cs | 108 ++++++++++++++++++ .../GitLabFolderPickerDataHandler.cs | 96 ++++++++++++++++ .../DataSourceHandlers/GitLabPathHelper.cs | 22 ++++ .../Commit/Requests/DeleteFileRequest.cs | 7 +- .../Requests/FolderContentRequest.cs | 8 +- .../Respository/Requests/GetFileRequest.cs | 7 +- 7 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 Apps.GitLab/DataSourceHandlers/GitLabFilePickerDataHandler.cs create mode 100644 Apps.GitLab/DataSourceHandlers/GitLabFolderPickerDataHandler.cs create mode 100644 Apps.GitLab/DataSourceHandlers/GitLabPathHelper.cs diff --git a/Apps.GitLab/Apps.GitLab.csproj b/Apps.GitLab/Apps.GitLab.csproj index 966969e..70a4470 100644 --- a/Apps.GitLab/Apps.GitLab.csproj +++ b/Apps.GitLab/Apps.GitLab.csproj @@ -6,13 +6,13 @@ enable GitLab Software development & version control - 1.0.10 + 1.0.11 Apps.GitLab - + diff --git a/Apps.GitLab/DataSourceHandlers/GitLabFilePickerDataHandler.cs b/Apps.GitLab/DataSourceHandlers/GitLabFilePickerDataHandler.cs new file mode 100644 index 0000000..f330eda --- /dev/null +++ b/Apps.GitLab/DataSourceHandlers/GitLabFilePickerDataHandler.cs @@ -0,0 +1,108 @@ +using Apps.Gitlab.Models.Branch.Requests; +using Apps.Gitlab.Models.Respository.Requests; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Authentication; +using Blackbird.Applications.Sdk.Common.Exceptions; +using Blackbird.Applications.Sdk.Common.Invocation; +using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; +using Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems; +using GitLabApiClient.Internal.Paths; +using File = Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems.File; + +namespace Apps.Gitlab.DataSourceHandlers; + +public class GitLabFilePickerDataHandler : BaseInvocable, IAsyncFileDataSourceItemHandler +{ + private IEnumerable Creds => + InvocationContext.AuthenticationCredentialsProviders; + + private readonly GetRepositoryRequest _repositoryRequest; + private readonly GetOptionalBranchRequest _branchRequest; + + public GitLabFilePickerDataHandler( + InvocationContext invocationContext, + [ActionParameter] GetRepositoryRequest repositoryRequest, + [ActionParameter] GetOptionalBranchRequest branchRequest) : base(invocationContext) + { + _repositoryRequest = repositoryRequest; + _branchRequest = branchRequest; + } + + public async Task> GetFolderContentAsync( + FolderContentDataSourceContext context, + CancellationToken cancellationToken) + { + var projectId = GetProjectId(); + var client = new BlackbirdGitlabClient(Creds).Client; + var folderPath = GitLabPathHelper.NormalizeFolderId(context?.FolderId); + + var tree = await client.Trees.GetAsync(projectId, options => + { + options.Path = string.IsNullOrEmpty(folderPath) ? "/" : folderPath; + options.Reference = _branchRequest.Name; + }); + + return tree + .OrderBy(x => x.Type == "blob") + .ThenBy(x => GetDisplayName(x.Path), StringComparer.OrdinalIgnoreCase) + .Select(x => x.Type == "tree" + ? (FileDataItem)new Folder + { + Id = GitLabPathHelper.NormalizePath(x.Path), + DisplayName = GetDisplayName(x.Path), + IsSelectable = false + } + : new File + { + Id = GitLabPathHelper.NormalizePath(x.Path), + DisplayName = GetDisplayName(x.Path), + IsSelectable = true + }) + .ToList(); + } + + public Task> GetFolderPathAsync( + FolderPathDataSourceContext context, + CancellationToken cancellationToken) + { + var path = new List + { + new() { Id = GitLabPathHelper.RootId, DisplayName = GitLabPathHelper.RootDisplayName } + }; + + var normalizedPath = GitLabPathHelper.NormalizePath(context?.FileDataItemId); + if (string.IsNullOrEmpty(normalizedPath)) + return Task.FromResult>(path); + + var directoryPath = Path.GetDirectoryName(normalizedPath)?.Replace('\\', '/'); + if (string.IsNullOrWhiteSpace(directoryPath)) + return Task.FromResult>(path); + + var currentPath = string.Empty; + foreach (var part in directoryPath.Split('/', StringSplitOptions.RemoveEmptyEntries)) + { + currentPath = string.IsNullOrEmpty(currentPath) ? part : $"{currentPath}/{part}"; + path.Add(new FolderPathItem + { + Id = currentPath, + DisplayName = part + }); + } + + return Task.FromResult>(path); + } + + private ProjectId GetProjectId() + { + if (string.IsNullOrWhiteSpace(_repositoryRequest.RepositoryId)) + throw new PluginMisconfigurationException("You should select a repository first."); + + return (ProjectId)int.Parse(_repositoryRequest.RepositoryId); + } + + private static string GetDisplayName(string path) + { + var normalizedPath = GitLabPathHelper.NormalizePath(path); + return normalizedPath.Split('/').LastOrDefault() ?? normalizedPath; + } +} diff --git a/Apps.GitLab/DataSourceHandlers/GitLabFolderPickerDataHandler.cs b/Apps.GitLab/DataSourceHandlers/GitLabFolderPickerDataHandler.cs new file mode 100644 index 0000000..ba90078 --- /dev/null +++ b/Apps.GitLab/DataSourceHandlers/GitLabFolderPickerDataHandler.cs @@ -0,0 +1,96 @@ +using Apps.Gitlab.Models.Branch.Requests; +using Apps.Gitlab.Models.Respository.Requests; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Authentication; +using Blackbird.Applications.Sdk.Common.Exceptions; +using Blackbird.Applications.Sdk.Common.Invocation; +using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; +using Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems; +using GitLabApiClient.Internal.Paths; + +namespace Apps.Gitlab.DataSourceHandlers; + +public class GitLabFolderPickerDataHandler : BaseInvocable, IAsyncFileDataSourceItemHandler +{ + private IEnumerable Creds => + InvocationContext.AuthenticationCredentialsProviders; + + private readonly GetRepositoryRequest _repositoryRequest; + private readonly GetOptionalBranchRequest _branchRequest; + + public GitLabFolderPickerDataHandler( + InvocationContext invocationContext, + [ActionParameter] GetRepositoryRequest repositoryRequest, + [ActionParameter] GetOptionalBranchRequest branchRequest) : base(invocationContext) + { + _repositoryRequest = repositoryRequest; + _branchRequest = branchRequest; + } + + public async Task> GetFolderContentAsync( + FolderContentDataSourceContext context, + CancellationToken cancellationToken) + { + var projectId = GetProjectId(); + var client = new BlackbirdGitlabClient(Creds).Client; + var folderPath = GitLabPathHelper.NormalizeFolderId(context?.FolderId); + + var tree = await client.Trees.GetAsync(projectId, options => + { + options.Path = string.IsNullOrEmpty(folderPath) ? "/" : folderPath; + options.Reference = _branchRequest.Name; + }); + + return tree + .Where(x => x.Type == "tree") + .OrderBy(x => GetDisplayName(x.Path), StringComparer.OrdinalIgnoreCase) + .Select(x => (FileDataItem)new Folder + { + Id = GitLabPathHelper.NormalizePath(x.Path), + DisplayName = GetDisplayName(x.Path), + IsSelectable = true + }) + .ToList(); + } + + public Task> GetFolderPathAsync( + FolderPathDataSourceContext context, + CancellationToken cancellationToken) + { + var path = new List + { + new() { Id = GitLabPathHelper.RootId, DisplayName = GitLabPathHelper.RootDisplayName } + }; + + var normalizedPath = GitLabPathHelper.NormalizePath(context?.FileDataItemId); + if (string.IsNullOrEmpty(normalizedPath)) + return Task.FromResult>(path); + + var currentPath = string.Empty; + foreach (var part in normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries)) + { + currentPath = string.IsNullOrEmpty(currentPath) ? part : $"{currentPath}/{part}"; + path.Add(new FolderPathItem + { + Id = currentPath, + DisplayName = part + }); + } + + return Task.FromResult>(path); + } + + private ProjectId GetProjectId() + { + if (string.IsNullOrWhiteSpace(_repositoryRequest.RepositoryId)) + throw new PluginMisconfigurationException("You should select a repository first."); + + return (ProjectId)int.Parse(_repositoryRequest.RepositoryId); + } + + private static string GetDisplayName(string path) + { + var normalizedPath = GitLabPathHelper.NormalizePath(path); + return normalizedPath.Split('/').LastOrDefault() ?? normalizedPath; + } +} diff --git a/Apps.GitLab/DataSourceHandlers/GitLabPathHelper.cs b/Apps.GitLab/DataSourceHandlers/GitLabPathHelper.cs new file mode 100644 index 0000000..2863781 --- /dev/null +++ b/Apps.GitLab/DataSourceHandlers/GitLabPathHelper.cs @@ -0,0 +1,22 @@ +namespace Apps.Gitlab.DataSourceHandlers; + +public static class GitLabPathHelper +{ + public const string RootId = "root"; + public const string RootDisplayName = "Root"; + + public static string NormalizeFolderId(string? folderId) + { + if (string.IsNullOrWhiteSpace(folderId) || folderId == RootId || folderId == "/") + return string.Empty; + + return folderId.Trim().Trim('/'); + } + + public static string NormalizePath(string? path) + { + return string.IsNullOrWhiteSpace(path) + ? string.Empty + : path.Trim().Trim('/'); + } +} diff --git a/Apps.GitLab/Models/Commit/Requests/DeleteFileRequest.cs b/Apps.GitLab/Models/Commit/Requests/DeleteFileRequest.cs index e2b0619..6b94f3c 100644 --- a/Apps.GitLab/Models/Commit/Requests/DeleteFileRequest.cs +++ b/Apps.GitLab/Models/Commit/Requests/DeleteFileRequest.cs @@ -1,12 +1,15 @@ -using Blackbird.Applications.Sdk.Common; +using Apps.Gitlab.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems; namespace Apps.Gitlab.Models.Commit.Requests; public class DeleteFileRequest { [Display("File path")] + [FileDataSource(typeof(GitLabFilePickerDataHandler))] public string FilePath { get; set; } [Display("Commit message")] public string CommitMessage { get; set; } -} \ No newline at end of file +} diff --git a/Apps.GitLab/Models/Respository/Requests/FolderContentRequest.cs b/Apps.GitLab/Models/Respository/Requests/FolderContentRequest.cs index df99233..8f70d09 100644 --- a/Apps.GitLab/Models/Respository/Requests/FolderContentRequest.cs +++ b/Apps.GitLab/Models/Respository/Requests/FolderContentRequest.cs @@ -1,4 +1,7 @@ -using Blackbird.Applications.Sdk.Common; +using Apps.Gitlab.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems; + namespace Apps.Gitlab.Models.Respository.Requests; public class FolderContentRequest @@ -14,8 +17,9 @@ public FolderContentRequest(string? path, bool? includeSubfolders) } [Display("Folder path (e.g. \"Folder1/Folder2\")")] + [FileDataSource(typeof(GitLabFolderPickerDataHandler))] public string? Path { get; set; } [Display("Include subfolders")] public bool? IncludeSubfolders { get; set; } -} \ No newline at end of file +} diff --git a/Apps.GitLab/Models/Respository/Requests/GetFileRequest.cs b/Apps.GitLab/Models/Respository/Requests/GetFileRequest.cs index 2ae947c..eabc681 100644 --- a/Apps.GitLab/Models/Respository/Requests/GetFileRequest.cs +++ b/Apps.GitLab/Models/Respository/Requests/GetFileRequest.cs @@ -1,9 +1,12 @@ -using Blackbird.Applications.Sdk.Common; +using Apps.Gitlab.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems; namespace Apps.Gitlab.Models.Respository.Requests; public class GetFileRequest { [Display("File path")] + [FileDataSource(typeof(GitLabFilePickerDataHandler))] public string FilePath { get; set; } -} \ No newline at end of file +} From d28aeb3eab4a68c5d61bccc23dab0b73cb18ba47 Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Wed, 1 Apr 2026 16:10:13 +0300 Subject: [PATCH 02/12] Added file folder picker --- ...ataHandler.cs => FilePickerDataHandler.cs} | 4 +-- ...aHandler.cs => FolderPickerDataHandler.cs} | 4 +-- .../Commit/Requests/DeleteFileRequest.cs | 2 +- .../Requests/FolderContentRequest.cs | 2 +- .../Respository/Requests/GetFileRequest.cs | 2 +- Tests.GitLab/GitLabPickerDataHandlerTests.cs | 30 +++++++++++++++++++ 6 files changed, 37 insertions(+), 7 deletions(-) rename Apps.GitLab/DataSourceHandlers/{GitLabFilePickerDataHandler.cs => FilePickerDataHandler.cs} (96%) rename Apps.GitLab/DataSourceHandlers/{GitLabFolderPickerDataHandler.cs => FolderPickerDataHandler.cs} (96%) create mode 100644 Tests.GitLab/GitLabPickerDataHandlerTests.cs diff --git a/Apps.GitLab/DataSourceHandlers/GitLabFilePickerDataHandler.cs b/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs similarity index 96% rename from Apps.GitLab/DataSourceHandlers/GitLabFilePickerDataHandler.cs rename to Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs index f330eda..e53a3de 100644 --- a/Apps.GitLab/DataSourceHandlers/GitLabFilePickerDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs @@ -11,7 +11,7 @@ namespace Apps.Gitlab.DataSourceHandlers; -public class GitLabFilePickerDataHandler : BaseInvocable, IAsyncFileDataSourceItemHandler +public class FilePickerDataHandler : BaseInvocable, IAsyncFileDataSourceItemHandler { private IEnumerable Creds => InvocationContext.AuthenticationCredentialsProviders; @@ -19,7 +19,7 @@ public class GitLabFilePickerDataHandler : BaseInvocable, IAsyncFileDataSourceIt private readonly GetRepositoryRequest _repositoryRequest; private readonly GetOptionalBranchRequest _branchRequest; - public GitLabFilePickerDataHandler( + public FilePickerDataHandler( InvocationContext invocationContext, [ActionParameter] GetRepositoryRequest repositoryRequest, [ActionParameter] GetOptionalBranchRequest branchRequest) : base(invocationContext) diff --git a/Apps.GitLab/DataSourceHandlers/GitLabFolderPickerDataHandler.cs b/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs similarity index 96% rename from Apps.GitLab/DataSourceHandlers/GitLabFolderPickerDataHandler.cs rename to Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs index ba90078..868517c 100644 --- a/Apps.GitLab/DataSourceHandlers/GitLabFolderPickerDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs @@ -10,7 +10,7 @@ namespace Apps.Gitlab.DataSourceHandlers; -public class GitLabFolderPickerDataHandler : BaseInvocable, IAsyncFileDataSourceItemHandler +public class FolderPickerDataHandler : BaseInvocable, IAsyncFileDataSourceItemHandler { private IEnumerable Creds => InvocationContext.AuthenticationCredentialsProviders; @@ -18,7 +18,7 @@ public class GitLabFolderPickerDataHandler : BaseInvocable, IAsyncFileDataSource private readonly GetRepositoryRequest _repositoryRequest; private readonly GetOptionalBranchRequest _branchRequest; - public GitLabFolderPickerDataHandler( + public FolderPickerDataHandler( InvocationContext invocationContext, [ActionParameter] GetRepositoryRequest repositoryRequest, [ActionParameter] GetOptionalBranchRequest branchRequest) : base(invocationContext) diff --git a/Apps.GitLab/Models/Commit/Requests/DeleteFileRequest.cs b/Apps.GitLab/Models/Commit/Requests/DeleteFileRequest.cs index 6b94f3c..925b501 100644 --- a/Apps.GitLab/Models/Commit/Requests/DeleteFileRequest.cs +++ b/Apps.GitLab/Models/Commit/Requests/DeleteFileRequest.cs @@ -7,7 +7,7 @@ namespace Apps.Gitlab.Models.Commit.Requests; public class DeleteFileRequest { [Display("File path")] - [FileDataSource(typeof(GitLabFilePickerDataHandler))] + [FileDataSource(typeof(FilePickerDataHandler))] public string FilePath { get; set; } [Display("Commit message")] diff --git a/Apps.GitLab/Models/Respository/Requests/FolderContentRequest.cs b/Apps.GitLab/Models/Respository/Requests/FolderContentRequest.cs index 8f70d09..97b169d 100644 --- a/Apps.GitLab/Models/Respository/Requests/FolderContentRequest.cs +++ b/Apps.GitLab/Models/Respository/Requests/FolderContentRequest.cs @@ -17,7 +17,7 @@ public FolderContentRequest(string? path, bool? includeSubfolders) } [Display("Folder path (e.g. \"Folder1/Folder2\")")] - [FileDataSource(typeof(GitLabFolderPickerDataHandler))] + [FileDataSource(typeof(FolderPickerDataHandler))] public string? Path { get; set; } [Display("Include subfolders")] diff --git a/Apps.GitLab/Models/Respository/Requests/GetFileRequest.cs b/Apps.GitLab/Models/Respository/Requests/GetFileRequest.cs index eabc681..af7ee36 100644 --- a/Apps.GitLab/Models/Respository/Requests/GetFileRequest.cs +++ b/Apps.GitLab/Models/Respository/Requests/GetFileRequest.cs @@ -7,6 +7,6 @@ namespace Apps.Gitlab.Models.Respository.Requests; public class GetFileRequest { [Display("File path")] - [FileDataSource(typeof(GitLabFilePickerDataHandler))] + [FileDataSource(typeof(FilePickerDataHandler))] public string FilePath { get; set; } } diff --git a/Tests.GitLab/GitLabPickerDataHandlerTests.cs b/Tests.GitLab/GitLabPickerDataHandlerTests.cs new file mode 100644 index 0000000..f19be54 --- /dev/null +++ b/Tests.GitLab/GitLabPickerDataHandlerTests.cs @@ -0,0 +1,30 @@ +using Apps.Gitlab.DataSourceHandlers; +using Apps.GitLab.Constants; +using Blackbird.Applications.Sdk.Common.Invocation; +using Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems; +using Tests.GitLab.Base; + +namespace Tests.GitLab; + +[TestClass] +public class PickerDataHandlerTests : TestBaseWithContext +{ + [TestMethod, ContextDataSource(ConnectionTypes.PersonalAccessToken)] + public async Task FilePicker_GetFolderContentAsync_ReturnsMappedRootItems(InvocationContext context) + { + var handler = new FilePickerDataHandler(context, new Apps.Gitlab.Models.Respository.Requests.GetRepositoryRequest { RepositoryId= "71835863" }, + new Apps.Gitlab.Models.Branch.Requests.GetOptionalBranchRequest { }); + + var result = await handler.GetFolderContentAsync(new FolderContentDataSourceContext + { + + }, CancellationToken.None); + + foreach (var item in result) + { + Console.WriteLine($"Name: {item.DisplayName}, Id: {item.Id}"); + } + + Assert.IsNotNull(result); + } +} From 22cc75004d275a7a8b1999b6407df2a03546171a Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Wed, 1 Apr 2026 16:17:48 +0300 Subject: [PATCH 03/12] Added file folder picker --- Apps.GitLab/Apps.GitLab.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Apps.GitLab/Apps.GitLab.csproj b/Apps.GitLab/Apps.GitLab.csproj index 70a4470..c53c2af 100644 --- a/Apps.GitLab/Apps.GitLab.csproj +++ b/Apps.GitLab/Apps.GitLab.csproj @@ -15,7 +15,6 @@ - all From a4c4251512363a4bcbc67f0b0c5ec7c88fd94619 Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Wed, 1 Apr 2026 16:33:05 +0300 Subject: [PATCH 04/12] Checking cicd --- Apps.GitLab/Apps.GitLab.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Apps.GitLab/Apps.GitLab.csproj b/Apps.GitLab/Apps.GitLab.csproj index c53c2af..e7e5804 100644 --- a/Apps.GitLab/Apps.GitLab.csproj +++ b/Apps.GitLab/Apps.GitLab.csproj @@ -15,7 +15,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 71032044f903c70739d3e786ed9d4b642ea7844b Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Wed, 1 Apr 2026 17:16:24 +0300 Subject: [PATCH 05/12] Checking nuget packages --- Apps.GitLab/Apps.GitLab.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Apps.GitLab/Apps.GitLab.csproj b/Apps.GitLab/Apps.GitLab.csproj index e7e5804..70a4470 100644 --- a/Apps.GitLab/Apps.GitLab.csproj +++ b/Apps.GitLab/Apps.GitLab.csproj @@ -15,7 +15,8 @@ - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 2b42760d919f48600773817836cd8aa900470f69 Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Wed, 1 Apr 2026 18:37:36 +0300 Subject: [PATCH 06/12] Checking nuget packages --- Apps.GitLab/Apps.GitLab.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Apps.GitLab/Apps.GitLab.csproj b/Apps.GitLab/Apps.GitLab.csproj index 70a4470..8e27cb1 100644 --- a/Apps.GitLab/Apps.GitLab.csproj +++ b/Apps.GitLab/Apps.GitLab.csproj @@ -14,7 +14,7 @@ - + From d1a57b38ede882969a79843b1995d1a3c32ca8a0 Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Wed, 1 Apr 2026 18:48:40 +0300 Subject: [PATCH 07/12] Checking nuget packages --- Apps.GitLab/Apps.GitLab.csproj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Apps.GitLab/Apps.GitLab.csproj b/Apps.GitLab/Apps.GitLab.csproj index 8e27cb1..90edc59 100644 --- a/Apps.GitLab/Apps.GitLab.csproj +++ b/Apps.GitLab/Apps.GitLab.csproj @@ -11,16 +11,17 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive + From 7cd9b65c3e713278e8b397e2b94cb56beba8933c Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Wed, 1 Apr 2026 19:13:38 +0300 Subject: [PATCH 08/12] Checking nuget packages --- Tests.GitLab/Tests.GitLab.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests.GitLab/Tests.GitLab.csproj b/Tests.GitLab/Tests.GitLab.csproj index cfbd27b..d5fde1f 100644 --- a/Tests.GitLab/Tests.GitLab.csproj +++ b/Tests.GitLab/Tests.GitLab.csproj @@ -15,6 +15,7 @@ + From 8c6a161bc0b46f902775c90bede0c15e5f18ba2d Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Thu, 2 Apr 2026 20:54:30 +0300 Subject: [PATCH 09/12] Removed Gitlab sdk client --- Apps.GitLab/Actions/Base/GitLabActions.cs | 15 ++- Apps.GitLab/Actions/BranchActions.cs | 39 +++--- Apps.GitLab/Actions/CommitActions.cs | 98 ++++++++------- Apps.GitLab/Actions/PullRequestActions.cs | 59 ++++----- Apps.GitLab/Actions/RepositoryActions.cs | 119 ++++++++++-------- .../Auth/OAuth2/OAuth2AuthorizeService.cs | 7 +- Apps.GitLab/Auth/OAuth2/OAuth2TokenService.cs | 7 +- Apps.GitLab/BlackbirdGitLabClient.cs | 76 ++++++----- .../Connections/ConnectionDefinition.cs | 21 ++-- .../Connections/ConnectionValidator.cs | 8 +- .../DataSourceHandlers/BranchDataHandler.cs | 18 ++- .../FilePickerDataHandler.cs | 20 +-- .../FolderPickerDataHandler.cs | 20 +-- .../MergeRequestDataHandler.cs | 16 ++- .../RepositoryDataHandler.cs | 12 +- .../DataSourceHandlers/UsersDataHandler.cs | 20 ++- Apps.GitLab/Utils/ErrorHandler.cs | 69 ---------- .../Webhooks/Handlers/PushEventHandler.cs | 69 ++++++---- 18 files changed, 366 insertions(+), 327 deletions(-) delete mode 100644 Apps.GitLab/Utils/ErrorHandler.cs diff --git a/Apps.GitLab/Actions/Base/GitLabActions.cs b/Apps.GitLab/Actions/Base/GitLabActions.cs index 62276b9..043f3fe 100644 --- a/Apps.GitLab/Actions/Base/GitLabActions.cs +++ b/Apps.GitLab/Actions/Base/GitLabActions.cs @@ -1,7 +1,6 @@ -using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Invocation; -using GitLabApiClient; namespace Apps.Gitlab.Actions.Base; @@ -9,14 +8,14 @@ public class GitLabActions : BaseInvocable { protected IEnumerable Creds => InvocationContext.AuthenticationCredentialsProviders; - - private BlackbirdGitlabClient GitLabClient { get; set; } - protected GitLabClient Client => GitLabClient.Client; - protected BlackbirdGitlabClient RestClient => GitLabClient; + protected BlackbirdGitlabClient RestClient { get; } public GitLabActions(InvocationContext invocationContext) : base(invocationContext) { - GitLabClient = new(Creds); + RestClient = new(Creds); } -} \ No newline at end of file + + protected static int ParseProjectId(string repositoryId) + => int.Parse(repositoryId); +} diff --git a/Apps.GitLab/Actions/BranchActions.cs b/Apps.GitLab/Actions/BranchActions.cs index 7aa090e..2e58f28 100644 --- a/Apps.GitLab/Actions/BranchActions.cs +++ b/Apps.GitLab/Actions/BranchActions.cs @@ -1,34 +1,32 @@ -using Apps.Gitlab.Actions.Base; +using Apps.Gitlab.Actions.Base; using Apps.Gitlab.Dtos; using Apps.Gitlab.Models.Branch.Requests; using Apps.Gitlab.Models.Branch.Responses; using Apps.Gitlab.Models.Respository.Requests; -using Apps.GitLab.Utils; using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Actions; using Blackbird.Applications.Sdk.Common.Invocation; using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; -using GitLabApiClient.Internal.Paths; +using GitLabApiClient.Models.Branches.Responses; +using RestSharp; namespace Apps.Gitlab.Actions; [ActionList("Branch")] public class BranchActions : GitLabActions { - private readonly IFileManagementClient _fileManagementClient; - public BranchActions(InvocationContext invocationContext, IFileManagementClient fileManagementClient) : base(invocationContext) { - _fileManagementClient = fileManagementClient; } [Action("List branches", Description = "List respository branches")] public async Task ListRepositoryBranches([ActionParameter] GetRepositoryRequest input) { - var projectId = (ProjectId)int.Parse(input.RepositoryId); - var branches = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Branches.GetAsync(projectId, _ => { })); + var projectId = ParseProjectId(input.RepositoryId); + var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/branches", Method.Get); + var branches = await RestClient.ExecuteWithErrorHandling>(request); + return new ListRepositoryBranchesResponse { Branches = branches.Select(b => new BranchDto(b)) @@ -40,10 +38,11 @@ public async Task GetBranch( [ActionParameter] GetRepositoryRequest repositoryRequest, [ActionParameter] GetBranchRequest input) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - - var branch = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Branches.GetAsync(projectId, input.Name)); + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var request = RestClient.CreateRequest( + $"/api/v4/projects/{projectId}/repository/branches/{Uri.EscapeDataString(input.Name)}", + Method.Get); + var branch = await RestClient.ExecuteWithErrorHandling(request); return new BranchDto(branch); } @@ -53,14 +52,12 @@ public async Task CreateBranch( [ActionParameter] GetRepositoryRequest repositoryRequest, [ActionParameter] Models.Branch.Requests.CreateBranchRequest input) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - - var request = new GitLabApiClient.Models.Branches.Requests.CreateBranchRequest( - input.NewBranchName, - input.BaseBranchName); + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/branches", Method.Post); + request.AddParameter("branch", input.NewBranchName); + request.AddParameter("ref", input.BaseBranchName); - var branch = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Branches.CreateAsync(projectId, request)); + var branch = await RestClient.ExecuteWithErrorHandling(request); return new BranchDto(branch); } -} \ No newline at end of file +} diff --git a/Apps.GitLab/Actions/CommitActions.cs b/Apps.GitLab/Actions/CommitActions.cs index 677ecb0..433a092 100644 --- a/Apps.GitLab/Actions/CommitActions.cs +++ b/Apps.GitLab/Actions/CommitActions.cs @@ -1,4 +1,4 @@ -using Apps.Gitlab.Actions.Base; +using Apps.Gitlab.Actions.Base; using Apps.Gitlab.Models.Branch.Requests; using Apps.Gitlab.Models.Commit.Requests; using Apps.Gitlab.Models.Respository.Requests; @@ -7,15 +7,14 @@ using Apps.GitLab.Models.Commit.Requests; using Apps.GitLab.Models.Commit.Responses; using Apps.GitLab.Models.Respository.Requests; -using Apps.GitLab.Utils; using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Actions; using Blackbird.Applications.Sdk.Common.Exceptions; using Blackbird.Applications.Sdk.Common.Invocation; using Blackbird.Applications.Sdk.Utils.Extensions.Files; using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; -using GitLabApiClient.Internal.Paths; using GitLabApiClient.Models.Commits.Responses; +using RestSharp; namespace Apps.Gitlab.Actions; @@ -35,13 +34,13 @@ public async Task ListRepositoryCommits( [ActionParameter] GetRepositoryRequest repositoryRequest, [ActionParameter] GetOptionalBranchRequest branchRequest) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - var commits = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Commits.GetAsync(projectId, options => - { - if (!string.IsNullOrWhiteSpace(branchRequest.Name)) - options.RefName = branchRequest.Name; - })); + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/commits", Method.Get); + + if (!string.IsNullOrWhiteSpace(branchRequest.Name)) + request.AddQueryParameter("ref_name", branchRequest.Name); + + var commits = await RestClient.ExecuteWithErrorHandling>(request); return new() { Commits = commits @@ -53,10 +52,12 @@ public async Task GetCommit( [ActionParameter] GetRepositoryRequest repositoryRequest, [ActionParameter] GetCommitRequest input) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - var commit = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Commits.GetAsync(projectId, input.CommitId)); - return commit; + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var request = RestClient.CreateRequest( + $"/api/v4/projects/{projectId}/repository/commits/{Uri.EscapeDataString(input.CommitId)}", + Method.Get); + + return await RestClient.ExecuteWithErrorHandling(request); } [Action("List added or modified files in X hours", Description = "List added or modified files in X hours")] @@ -68,21 +69,23 @@ public async Task ListAddedOrModifiedInHours { if (hoursRequest.Hours <= 0) throw new ArgumentException("Specify more than 0 hours!"); - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - - var commits = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Commits.GetAsync(projectId, options => - { - options.Since = DateTime.Now.AddHours(-hoursRequest.Hours); - if (!string.IsNullOrWhiteSpace(branchRequest.Name)) - options.RefName = branchRequest.Name; - })); + + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/commits", Method.Get); + request.AddQueryParameter("since", DateTime.Now.AddHours(-hoursRequest.Hours).ToString("O")); + + if (!string.IsNullOrWhiteSpace(branchRequest.Name)) + request.AddQueryParameter("ref_name", branchRequest.Name); + + var commits = await RestClient.ExecuteWithErrorHandling>(request); var files = new List(); - foreach (var c in commits) + foreach (var commit in commits) { - var diffs = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Commits.GetDiffsAsync(projectId, c.Id)); + var diffRequest = RestClient.CreateRequest( + $"/api/v4/projects/{projectId}/repository/commits/{Uri.EscapeDataString(commit.Id)}/diff", + Method.Get); + var diffs = await RestClient.ExecuteWithErrorHandling>(diffRequest); files.AddRange( diffs @@ -92,7 +95,11 @@ public async Task ListAddedOrModifiedInHours PushWebhooks.IsFilePathMatchingPattern(folderInput.FolderPath, f.NewPath)) .Select(x => new AddedOrModifiedFile(x))); } - return new ListAddedOrModifiedInHoursResponse() { Files = files.DistinctBy(x => x.Filename).ToList() }; + + return new ListAddedOrModifiedInHoursResponse + { + Files = files.DistinctBy(x => x.Filename).ToList() + }; } [Action("Create or update file", Description = "Create or update file")] @@ -101,18 +108,18 @@ public async Task PushFile( [ActionParameter] GetOptionalBranchRequest branchRequest, [ActionParameter] PushFileRequest input) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); + var projectId = ParseProjectId(repositoryRequest.RepositoryId); var repContent = await new RepositoryActions(InvocationContext, _fileManagementClient).ListRepositoryContent( - repositoryRequest, branchRequest, new FolderContentWithTypeRequest() { IncludeSubfolders = true }); - if (repContent.Content.Where(x => x.Type == "blob") - .Any(p => p.Path == input.DestinationFilePath)) // update in case of existing file + repositoryRequest, branchRequest, new FolderContentWithTypeRequest { IncludeSubfolders = true }); + + if (repContent.Content.Where(x => x.Type == "blob").Any(p => p.Path == input.DestinationFilePath)) { return await UpdateFile( repositoryRequest, branchRequest, - new() + new PushFileRequest { DestinationFilePath = input.DestinationFilePath, File = input.File, @@ -122,13 +129,14 @@ public async Task PushFile( if (repContent.Content.Where(x => x.Type == "tree").Select(x => x.Path) .Contains(input.DestinationFilePath.Trim('/'))) + { throw new PluginApplicationException("Destination file path is invalid!"); + } var file = await _fileManagementClient.DownloadAsync(input.File); var fileBytes = await file.GetByteData(); - var pushFileResult = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - RestClient.PushChanges(projectId, branchRequest.Name, input.CommitMessage, - input.DestinationFilePath, fileBytes, "create")); + var pushFileResult = await RestClient.PushChanges(projectId, branchRequest.Name, input.CommitMessage, + input.DestinationFilePath, fileBytes, "create"); return new(pushFileResult); } @@ -139,20 +147,21 @@ public async Task UpdateFile( [ActionParameter] GetOptionalBranchRequest branchRequest, [ActionParameter] PushFileRequest input) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); + var projectId = ParseProjectId(repositoryRequest.RepositoryId); var repContent = await new RepositoryActions(InvocationContext, _fileManagementClient).ListRepositoryContent( repositoryRequest, branchRequest, - new FolderContentWithTypeRequest() { IncludeSubfolders = true, ContentType = "blob" }); + new FolderContentWithTypeRequest { IncludeSubfolders = true, ContentType = "blob" }); + if (!repContent.Content.Select(x => x.Path).Contains(input.DestinationFilePath.Trim('/'))) throw new PluginApplicationException("File does not exist by specified file path!"); var file = await _fileManagementClient.DownloadAsync(input.File); var fileBytes = await file.GetByteData(); - var fileUpload = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - RestClient.PushChanges(projectId, branchRequest.Name, input.CommitMessage, - input.DestinationFilePath, fileBytes, "update")); + var fileUpload = await RestClient.PushChanges(projectId, branchRequest.Name, input.CommitMessage, + input.DestinationFilePath, fileBytes, "update"); + return new(fileUpload); } @@ -162,11 +171,10 @@ public async Task DeleteFile( [ActionParameter] GetOptionalBranchRequest branchRequest, [ActionParameter] DeleteFileRequest input) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var fileDelete = await RestClient.PushChanges(projectId, branchRequest.Name, input.CommitMessage, + input.FilePath, null!, "delete"); - var fileDelete = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - RestClient.PushChanges(projectId, branchRequest.Name, input.CommitMessage, - input.FilePath, null, "delete")); return new() { CommitId = fileDelete.Id, @@ -174,4 +182,4 @@ public async Task DeleteFile( Message = fileDelete.Message }; } -} \ No newline at end of file +} diff --git a/Apps.GitLab/Actions/PullRequestActions.cs b/Apps.GitLab/Actions/PullRequestActions.cs index b28a289..268a318 100644 --- a/Apps.GitLab/Actions/PullRequestActions.cs +++ b/Apps.GitLab/Actions/PullRequestActions.cs @@ -1,36 +1,32 @@ -using Apps.Gitlab.Actions.Base; +using Apps.Gitlab.Actions.Base; using Apps.Gitlab.Models.PullRequest.Requests; using Apps.Gitlab.Models.PullRequest.Responses; using Apps.Gitlab.Models.Respository.Requests; -using Apps.GitLab.Utils; using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Actions; using Blackbird.Applications.Sdk.Common.Invocation; using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; -using GitLabApiClient.Internal.Paths; -using GitLabApiClient.Models.MergeRequests.Requests; using GitLabApiClient.Models.MergeRequests.Responses; +using RestSharp; namespace Apps.Gitlab.Actions; [ActionList("Pull request")] public class PullRequestActions : GitLabActions { - private readonly IFileManagementClient _fileManagementClient; - public PullRequestActions(InvocationContext invocationContext, IFileManagementClient fileManagementClient) : base(invocationContext) { - _fileManagementClient = fileManagementClient; } [Action("List merge requests", Description = "List merge requests")] public async Task ListPullRequests( [ActionParameter] GetRepositoryRequest repositoryRequest) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - var pulls = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.MergeRequests.GetAsync(projectId, _ => { })); + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/merge_requests", Method.Get); + var pulls = await RestClient.ExecuteWithErrorHandling>(request); + return new ListPullRequestsResponse { PullRequests = pulls @@ -42,10 +38,12 @@ public async Task GetPullRequest( [ActionParameter] GetRepositoryRequest repositoryRequest, [ActionParameter] GetPullRequest input) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - var pull = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.MergeRequests.GetAsync(projectId, int.Parse(input.PullRequestId))); - return pull; + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var request = RestClient.CreateRequest( + $"/api/v4/projects/{projectId}/merge_requests/{int.Parse(input.PullRequestId)}", + Method.Get); + + return await RestClient.ExecuteWithErrorHandling(request); } [Action("Create merge request", Description = "Create merge request")] @@ -53,16 +51,17 @@ public async Task CreatePullRequest( [ActionParameter] GetRepositoryRequest repositoryRequest, [ActionParameter] CreatePullRequest input) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - var request = new CreateMergeRequest(input.BaseBranch, input.HeadBranch, input.Title) + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/merge_requests", Method.Post); + request.AddJsonBody(new { - Description = input.Description - }; - - var pull = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.MergeRequests.CreateAsync(projectId, request)); + source_branch = input.HeadBranch, + target_branch = input.BaseBranch, + title = input.Title, + description = input.Description + }); - return pull; + return await RestClient.ExecuteWithErrorHandling(request); } [Action("Complete merge request", Description = "Complete merge request")] @@ -71,13 +70,15 @@ public async Task MergePullRequest( [ActionParameter] GetPullRequest mergeRequest, [ActionParameter] MergePullRequest input) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - var acceptRequest = new AcceptMergeRequest + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var request = RestClient.CreateRequest( + $"/api/v4/projects/{projectId}/merge_requests/{int.Parse(mergeRequest.PullRequestId)}/merge", + Method.Put); + request.AddJsonBody(new { - MergeCommitMessage = input.MergeCommitMessage - }; + merge_commit_message = input.MergeCommitMessage + }); - return await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.MergeRequests.AcceptAsync(projectId, int.Parse(mergeRequest.PullRequestId), acceptRequest)); + return await RestClient.ExecuteWithErrorHandling(request); } -} \ No newline at end of file +} diff --git a/Apps.GitLab/Actions/RepositoryActions.cs b/Apps.GitLab/Actions/RepositoryActions.cs index ef37ce9..5b0fbb2 100644 --- a/Apps.GitLab/Actions/RepositoryActions.cs +++ b/Apps.GitLab/Actions/RepositoryActions.cs @@ -1,4 +1,4 @@ -using Apps.Gitlab.Actions.Base; +using Apps.Gitlab.Actions.Base; using Apps.Gitlab.Constants; using Apps.Gitlab.Dtos; using Apps.Gitlab.Models.Branch.Requests; @@ -7,7 +7,6 @@ using Apps.Gitlab.Models.Respository.Responses; using Apps.GitLab; using Apps.GitLab.Models.Respository.Requests; -using Apps.GitLab.Utils; using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Actions; using Blackbird.Applications.Sdk.Common.Exceptions; @@ -17,8 +16,8 @@ using Blackbird.Applications.Sdk.Utils.Extensions.Http; using Blackbird.Applications.Sdk.Utils.Models; using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; -using GitLabApiClient.Internal.Paths; using GitLabApiClient.Models.Projects.Responses; +using GitLabApiClient.Models.Trees.Responses; using RestSharp; using System.Net.Mime; @@ -55,13 +54,16 @@ public async Task GetFile( [ActionParameter] GetOptionalBranchRequest branchRequest, [ActionParameter] GetFileRequest getFileRequest) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - var repository = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => Client.Projects.GetAsync(projectId)); - + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var repository = await GetProject(projectId); var branch = branchRequest.Name ?? repository.DefaultBranch; - var fileData = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Files.GetAsync(projectId, getFileRequest.FilePath, branch)); + var request = RestClient.CreateRequest( + $"/api/v4/projects/{projectId}/repository/files/{Uri.EscapeDataString(getFileRequest.FilePath)}", + Method.Get); + request.AddQueryParameter("ref", branch); + + var fileData = await RestClient.ExecuteWithErrorHandling(request); if (fileData == null) throw new ArgumentException($"File does not exist ({getFileRequest.FilePath})"); @@ -70,10 +72,9 @@ public async Task GetFile( mimeType = MediaTypeNames.Application.Octet; FileReference file; - File.WriteAllBytes("test", Convert.FromBase64String(fileData.Content)); using (var stream = new MemoryStream(Convert.FromBase64String(fileData.Content))) { - file = _fileManagementClient.UploadAsync(stream, mimeType, filename).Result; + file = await _fileManagementClient.UploadAsync(stream, mimeType, filename); } return new GetFileResponse @@ -90,32 +91,32 @@ public async Task GetAllFilesInFolder( [ActionParameter] GetOptionalBranchRequest branchRequest, [ActionParameter] FolderContentRequest folderContentRequest) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); + var projectId = ParseProjectId(repositoryRequest.RepositoryId); var resultFiles = new List(); - var content = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - RestClient.GetArchive(projectId, branchRequest.Name)); - if (content == null || content.Length == 0) - { + var content = await RestClient.GetArchive(projectId, branchRequest.Name); + if (content.Length == 0) throw new PluginApplicationException("Repository is empty!"); - } - var filesFromZip = new List(); + List filesFromZip; using (var stream = new MemoryStream(content)) { filesFromZip = (await stream.GetFilesFromZip()).ToList(); } - var includeSubFolders = folderContentRequest.IncludeSubfolders.HasValue && folderContentRequest.IncludeSubfolders.Value; + + var includeSubFolders = folderContentRequest.IncludeSubfolders.GetValueOrDefault(); foreach (var file in filesFromZip) { file.Path = file.Path.Substring(file.Path.IndexOf('/') + 1); if (file.FileStream.Length == 0) - { continue; - } - else if (!string.IsNullOrEmpty(folderContentRequest.Path)) + + if (!string.IsNullOrEmpty(folderContentRequest.Path)) { + var normalizedFolderPath = folderContentRequest.Path.Trim('/'); + var normalizedDirectory = Path.GetDirectoryName(file.Path)?.TrimStart('\\').Replace('\\', '/'); + if ((includeSubFolders && !file.Path.StartsWith(folderContentRequest.Path)) || - (!includeSubFolders && Path.GetDirectoryName(file.Path).TrimStart('\\').Replace('\\', '/') != folderContentRequest.Path.Trim('/'))) + (!includeSubFolders && normalizedDirectory != normalizedFolderPath)) { continue; } @@ -124,34 +125,33 @@ public async Task GetAllFilesInFolder( { continue; } + var filename = Path.GetFileName(file.Path); if (!MimeTypes.TryGetMimeType(filename, out var mimeType)) mimeType = MediaTypeNames.Application.Octet; + var uploadedFile = await _fileManagementClient.UploadAsync(file.FileStream, mimeType, filename); - resultFiles.Add(new GitLabFile() + resultFiles.Add(new GitLabFile { File = uploadedFile, FilePath = file.Path }); } + return new GetRepositoryFilesFromFilepathsResponse { Files = resultFiles }; } [Action("Get repository", Description = "Get repository info")] - public async Task GetRepositoryById([ActionParameter] GetRepositoryRequest input) - { - var projectId = (ProjectId)int.Parse(input.RepositoryId); - var repository = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Projects.GetAsync(projectId)); - return repository; - } + public Task GetRepositoryById([ActionParameter] GetRepositoryRequest input) + => GetProject(ParseProjectId(input.RepositoryId)); [Action("Get repository issues", Description = "Get opened issues against repository")] public async Task GetIssuesInRepository([ActionParameter] RepositoryRequest input) { - var projectId = (ProjectId)int.Parse(input.RepositoryId); - var issues = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Issues.GetAllAsync(projectId)); + var projectId = ParseProjectId(input.RepositoryId); + var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/issues", Method.Get); + var issues = await RestClient.ExecuteWithErrorHandling>(request); + return new() { Issues = issues.Select(issue => new IssueDto(issue)) @@ -161,9 +161,10 @@ public async Task GetIssuesInRepository([ActionParameter] Rep [Action("Get repository merge requests", Description = "Get opened merge requests in a repository")] public async Task GetPullRequestsInRepository([ActionParameter] RepositoryRequest input) { - var projectId = (ProjectId)int.Parse(input.RepositoryId); - var pullRequests = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.MergeRequests.GetAsync(projectId, _ => { })); + var projectId = ParseProjectId(input.RepositoryId); + var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/merge_requests", Method.Get); + var pullRequests = await RestClient.ExecuteWithErrorHandling>(request); + return new() { PullRequests = pullRequests.Select(p => new PullRequestDto(p)) @@ -176,17 +177,18 @@ public async Task ListRepositoryContent( [ActionParameter] GetOptionalBranchRequest branchRequest, [ActionParameter] FolderContentWithTypeRequest input) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - var tree = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Trees.GetAsync(projectId, options => - { - options.Recursive = input.IncludeSubfolders ?? false; - options.Path = input.Path ?? "/"; - if (!string.IsNullOrWhiteSpace(branchRequest.Name)) - options.Reference = branchRequest.Name; - })); + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/tree", Method.Get); + request.AddQueryParameter("recursive", (input.IncludeSubfolders ?? false).ToString().ToLowerInvariant()); + request.AddQueryParameter("path", input.Path ?? "/"); + + if (!string.IsNullOrWhiteSpace(branchRequest.Name)) + request.AddQueryParameter("ref", branchRequest.Name); + + var tree = await RestClient.ExecuteWithErrorHandling>(request); if (!string.IsNullOrEmpty(input.ContentType)) tree = tree.Where(x => input.ContentType.Split(' ').Contains(x.Type)).ToList(); + return new() { Content = tree @@ -196,8 +198,10 @@ public async Task ListRepositoryContent( [Action("List repositories", Description = "List all repositories")] public async Task ListRepositories() { - var projects = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Projects.GetAsync(options => { options.IsMemberOf = true; })); + var request = RestClient.CreateRequest("/api/v4/projects", Method.Get); + request.AddQueryParameter("membership", "true"); + + var projects = await RestClient.ExecuteWithErrorHandling>(request); return new(projects.ToArray()); } @@ -207,7 +211,6 @@ public async Task GetRepositoryFilesFro [ActionParameter] GetOptionalBranchRequest branchRequest, [ActionParameter] GetRepositoryFilesFromFilepathsRequest input) { - var files = new List(); foreach (var filePath in input.FilePaths) { @@ -234,10 +237,22 @@ public async Task BranchExists( [ActionParameter] GetRepositoryRequest repositoryRequest, [ActionParameter][Display("Branch name")] string branchNameRequest) { - var projectId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); - var branches = await ErrorHandler.ExecuteWithErrorHandlingAsync(() => - Client.Branches.GetAsync(projectId, options => { options.Search = branchNameRequest; })); + var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/branches", Method.Get); + request.AddQueryParameter("search", branchNameRequest); + var branches = await RestClient.ExecuteWithErrorHandling>(request); return branches.Any(x => x.Name == branchNameRequest); } -} \ No newline at end of file + + private Task GetProject(int projectId) + { + var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}", Method.Get); + return RestClient.ExecuteWithErrorHandling(request); + } + + private class RepositoryFileResponse + { + public string Content { get; set; } = string.Empty; + } +} diff --git a/Apps.GitLab/Auth/OAuth2/OAuth2AuthorizeService.cs b/Apps.GitLab/Auth/OAuth2/OAuth2AuthorizeService.cs index d437f4d..e5ff0a4 100644 --- a/Apps.GitLab/Auth/OAuth2/OAuth2AuthorizeService.cs +++ b/Apps.GitLab/Auth/OAuth2/OAuth2AuthorizeService.cs @@ -49,7 +49,7 @@ private static string GetAuthorizationEndpoint(Dictionary values return connectionType switch { ConnectionTypes.OAuth => "https://gitlab.com/oauth/authorize", - ConnectionTypes.OAuthSelfManaged => $"{values[CredNames.BaseUrl].TrimEnd('/')}/oauth/authorize", + ConnectionTypes.OAuthSelfManaged => $"{GetSelfManagedBaseUrl(values)}/oauth/authorize", _ => throw new Exception($"Unsupported connection type for OAuth authorization: {connectionType}") }; } @@ -63,4 +63,7 @@ private static string GetClientId(Dictionary values, string conn _ => throw new Exception($"Unsupported connection type for OAuth authorization: {connectionType}") }; } -} \ No newline at end of file + + private static string GetSelfManagedBaseUrl(Dictionary values) + => values[CredNames.BaseUrl].TrimEnd('/'); +} diff --git a/Apps.GitLab/Auth/OAuth2/OAuth2TokenService.cs b/Apps.GitLab/Auth/OAuth2/OAuth2TokenService.cs index 8dec154..8cd6ff7 100644 --- a/Apps.GitLab/Auth/OAuth2/OAuth2TokenService.cs +++ b/Apps.GitLab/Auth/OAuth2/OAuth2TokenService.cs @@ -131,7 +131,7 @@ private static string GetTokenEndpoint(Dictionary values, string return connectionType switch { ConnectionTypes.OAuth => "https://gitlab.com/oauth/token", - ConnectionTypes.OAuthSelfManaged => $"{values[CredNames.BaseUrl].TrimEnd('/')}/oauth/token", + ConnectionTypes.OAuthSelfManaged => $"{GetSelfManagedBaseUrl(values)}/oauth/token", _ => throw new Exception($"Unsupported connection type for OAuth token exchange: {connectionType}") }; } @@ -155,4 +155,7 @@ private static string GetClientSecret(Dictionary values, string _ => throw new Exception($"Unsupported connection type for OAuth client secret: {connectionType}") }; } -} \ No newline at end of file + + private static string GetSelfManagedBaseUrl(Dictionary values) + => values[CredNames.BaseUrl].TrimEnd('/'); +} diff --git a/Apps.GitLab/BlackbirdGitLabClient.cs b/Apps.GitLab/BlackbirdGitLabClient.cs index b66d242..0176ad5 100644 --- a/Apps.GitLab/BlackbirdGitLabClient.cs +++ b/Apps.GitLab/BlackbirdGitLabClient.cs @@ -1,13 +1,13 @@ -using Apps.GitLab.Constants; +using Apps.GitLab.Constants; using Apps.GitLab.Dtos; +using Apps.Gitlab.Constants; using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Exceptions; using Blackbird.Applications.Sdk.Utils.Extensions.Sdk; using Blackbird.Applications.Sdk.Utils.Extensions.String; using Blackbird.Applications.Sdk.Utils.RestSharp; -using GitLabApiClient; -using GitLabApiClient.Internal.Paths; using GitLabApiClient.Models.Commits.Responses; +using Newtonsoft.Json; using RestSharp; namespace Apps.Gitlab; @@ -15,10 +15,10 @@ namespace Apps.Gitlab; public class BlackbirdGitlabClient : BlackBirdRestClient { private readonly IEnumerable _authenticationCredentials; - private readonly string _baseUrl; - public readonly GitLabClient _client; - public GitLabClient Client => _client; + protected override JsonSerializerSettings? JsonSettings => JsonConfig.JsonSettings; + + public string BaseUrl { get; } public BlackbirdGitlabClient(IEnumerable authenticationCredentialsProviders) : base(new() @@ -27,13 +27,10 @@ public BlackbirdGitlabClient(IEnumerable auth }) { _authenticationCredentials = authenticationCredentialsProviders; - _baseUrl = GetBaseUrl(authenticationCredentialsProviders); - - var apiToken = authenticationCredentialsProviders.Get(CredNames.Authorization).Value; - _client = new GitLabClient(_baseUrl, apiToken); + BaseUrl = GetBaseUrl(authenticationCredentialsProviders); } - private static string GetBaseUrl(IEnumerable creds) + public static string GetBaseUrl(IEnumerable creds) { var connectionType = creds.Get(CredNames.ConnectionType).Value; @@ -46,27 +43,31 @@ private static string GetBaseUrl(IEnumerable }; } - public async Task GetArchive(ProjectId projectId, string? branchName) + public GitLabRequest CreateRequest(string resource, Method method) + => new(resource, method, _authenticationCredentials); + + public async Task ExecuteForBytesWithErrorHandling(RestRequest request) { - var branchCommit = !string.IsNullOrWhiteSpace(branchName) ? $"?sha={branchName}" : ""; - var request = new RestRequest($"/api/v4/projects/{projectId}/repository/archive.zip{branchCommit}", Method.Get); - request.AddHeader("Authorization", $"Bearer {_authenticationCredentials.Get(CredNames.Authorization).Value}"); + var response = await ExecuteWithErrorHandling(request); - var result = await new RestClient(_baseUrl).ExecuteAsync(request); + return response.RawBytes ?? []; + } - if (!result.IsSuccessStatusCode) - throw ConfigureErrorException(result); + public async Task GetArchive(int projectId, string? branchName) + { + var branchCommit = !string.IsNullOrWhiteSpace(branchName) ? $"?sha={Uri.EscapeDataString(branchName)}" : ""; + var request = CreateRequest($"/api/v4/projects/{projectId}/repository/archive.zip{branchCommit}", Method.Get); - return result.RawBytes; + return await ExecuteForBytesWithErrorHandling(request); } - public async Task PushChanges(ProjectId projectId, string? branchName, string commitMessage, - string filePath, byte[] file, string action) + public async Task PushChanges(int projectId, string? branchName, string commitMessage, + string filePath, byte[]? file, string action) { - var repository = await Client.Projects.GetAsync(projectId); + var repository = await ExecuteWithErrorHandling( + CreateRequest($"/api/v4/projects/{projectId}", Method.Get)); - var request = new RestRequest($"/api/v4/projects/{projectId}/repository/commits", Method.Post); - request.AddHeader("Authorization", $"Bearer {_authenticationCredentials.Get(CredNames.Authorization).Value}"); + var request = CreateRequest($"/api/v4/projects/{projectId}/repository/commits", Method.Post); request.AddJsonBody(new { branch = branchName ?? repository.DefaultBranch, @@ -77,16 +78,27 @@ public async Task PushChanges(ProjectId projectId, string? branchName, s } }); - var result = await new RestClient(_baseUrl).ExecuteAsync(request); - - if (!result.IsSuccessStatusCode) - throw ConfigureErrorException(result); - - return result.Data; + return await ExecuteWithErrorHandling(request); } protected override Exception ConfigureErrorException(RestResponse response) { - return new PluginApplicationException(response.Content); + return response.StatusCode switch + { + System.Net.HttpStatusCode.Unauthorized or System.Net.HttpStatusCode.Forbidden => + new PluginMisconfigurationException( + "GitLab credentials are invalid or do not have sufficient permissions. Please check your access token and scopes."), + System.Net.HttpStatusCode.NotFound => + new PluginApplicationException("Resource was not found in GitLab. Please verify the repository ID / path / branch."), + (System.Net.HttpStatusCode)429 => + new PluginApplicationException("GitLab rate limit reached. Please try again later."), + _ => new PluginApplicationException(response.Content ?? "GitLab request failed.") + }; + } + + private class RepositoryInfo + { + [JsonProperty("default_branch")] + public string? DefaultBranch { get; set; } } -} \ No newline at end of file +} diff --git a/Apps.GitLab/Connections/ConnectionDefinition.cs b/Apps.GitLab/Connections/ConnectionDefinition.cs index 427e367..3d24685 100644 --- a/Apps.GitLab/Connections/ConnectionDefinition.cs +++ b/Apps.GitLab/Connections/ConnectionDefinition.cs @@ -53,15 +53,22 @@ var ct when ConnectionTypes.SupportedConnectionTypes.Contains(ct) => ct, providers.Add(new AuthenticationCredentialsProvider(CredNames.ConnectionType, connectionType)); - if (values.TryGetValue("access_token", out var accessToken)) - { - providers.Add(new AuthenticationCredentialsProvider(CredNames.Authorization, accessToken)); - } - else if (values.TryGetValue(CredNames.ApiKey, out var apiKey)) + var authorizationToken = GetAuthorizationToken(values); + if (!string.IsNullOrWhiteSpace(authorizationToken)) { - providers.Add(new AuthenticationCredentialsProvider(CredNames.Authorization, apiKey)); + providers.Add(new AuthenticationCredentialsProvider(CredNames.Authorization, authorizationToken)); } return providers; } -} \ No newline at end of file + + private static string? GetAuthorizationToken(Dictionary values) + { + if (values.TryGetValue("access_token", out var accessToken)) + return accessToken; + + return values.TryGetValue(CredNames.ApiKey, out var apiKey) + ? apiKey + : null; + } +} diff --git a/Apps.GitLab/Connections/ConnectionValidator.cs b/Apps.GitLab/Connections/ConnectionValidator.cs index 27baec4..758c2a9 100644 --- a/Apps.GitLab/Connections/ConnectionValidator.cs +++ b/Apps.GitLab/Connections/ConnectionValidator.cs @@ -1,5 +1,6 @@ -using Blackbird.Applications.Sdk.Common.Authentication; +using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Connections; +using RestSharp; namespace Apps.Gitlab.Connections; @@ -10,7 +11,8 @@ public async ValueTask ValidateConnection( { try { - await new BlackbirdGitlabClient(authProviders).Client.Projects.GetAsync((options) => { options.IsMemberOf = true; }); + var client = new BlackbirdGitlabClient(authProviders); + await client.ExecuteWithErrorHandling(client.CreateRequest("/api/v4/user", Method.Get)); return new() { @@ -26,4 +28,4 @@ public async ValueTask ValidateConnection( }; } } -} \ No newline at end of file +} diff --git a/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs b/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs index 995f021..f31ad25 100644 --- a/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs @@ -1,9 +1,10 @@ -using Apps.Gitlab.Models.Respository.Requests; +using Apps.Gitlab.Models.Respository.Requests; using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Dynamic; using Blackbird.Applications.Sdk.Common.Invocation; -using GitLabApiClient.Internal.Paths; +using GitLabApiClient.Models.Branches.Responses; +using RestSharp; namespace Apps.Gitlab.DataSourceHandlers; @@ -25,8 +26,15 @@ public async Task> GetDataAsync( { if (RepositoryRequest == null || string.IsNullOrWhiteSpace(RepositoryRequest.RepositoryId)) throw new ArgumentException("Please, specify repository first"); - var projectId = (ProjectId)int.Parse(RepositoryRequest.RepositoryId); - var branches = await new BlackbirdGitlabClient(Creds).Client.Branches.GetAsync(projectId, (options) => { }); + + var projectId = int.Parse(RepositoryRequest.RepositoryId); + var client = new BlackbirdGitlabClient(Creds); + var request = client.CreateRequest($"/api/v4/projects/{projectId}/repository/branches", Method.Get); + + if (!string.IsNullOrWhiteSpace(context.SearchString)) + request.AddQueryParameter("search", context.SearchString); + + var branches = await client.ExecuteWithErrorHandling>(request); return branches .Where(x => context.SearchString == null || @@ -34,4 +42,4 @@ public async Task> GetDataAsync( .Take(20) .ToDictionary(x => x.Name, x => x.Name); } -} \ No newline at end of file +} diff --git a/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs b/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs index e53a3de..0f3b4cf 100644 --- a/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs @@ -6,7 +6,8 @@ using Blackbird.Applications.Sdk.Common.Invocation; using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; using Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems; -using GitLabApiClient.Internal.Paths; +using GitLabApiClient.Models.Trees.Responses; +using RestSharp; using File = Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems.File; namespace Apps.Gitlab.DataSourceHandlers; @@ -33,14 +34,15 @@ public async Task> GetFolderContentAsync( CancellationToken cancellationToken) { var projectId = GetProjectId(); - var client = new BlackbirdGitlabClient(Creds).Client; + var client = new BlackbirdGitlabClient(Creds); var folderPath = GitLabPathHelper.NormalizeFolderId(context?.FolderId); + var request = client.CreateRequest($"/api/v4/projects/{projectId}/repository/tree", Method.Get); + request.AddQueryParameter("path", string.IsNullOrEmpty(folderPath) ? "/" : folderPath); - var tree = await client.Trees.GetAsync(projectId, options => - { - options.Path = string.IsNullOrEmpty(folderPath) ? "/" : folderPath; - options.Reference = _branchRequest.Name; - }); + if (!string.IsNullOrWhiteSpace(_branchRequest.Name)) + request.AddQueryParameter("ref", _branchRequest.Name); + + var tree = await client.ExecuteWithErrorHandling>(request); return tree .OrderBy(x => x.Type == "blob") @@ -92,12 +94,12 @@ public Task> GetFolderPathAsync( return Task.FromResult>(path); } - private ProjectId GetProjectId() + private int GetProjectId() { if (string.IsNullOrWhiteSpace(_repositoryRequest.RepositoryId)) throw new PluginMisconfigurationException("You should select a repository first."); - return (ProjectId)int.Parse(_repositoryRequest.RepositoryId); + return int.Parse(_repositoryRequest.RepositoryId); } private static string GetDisplayName(string path) diff --git a/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs b/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs index 868517c..7ad4730 100644 --- a/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs @@ -6,7 +6,8 @@ using Blackbird.Applications.Sdk.Common.Invocation; using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; using Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems; -using GitLabApiClient.Internal.Paths; +using GitLabApiClient.Models.Trees.Responses; +using RestSharp; namespace Apps.Gitlab.DataSourceHandlers; @@ -32,14 +33,15 @@ public async Task> GetFolderContentAsync( CancellationToken cancellationToken) { var projectId = GetProjectId(); - var client = new BlackbirdGitlabClient(Creds).Client; + var client = new BlackbirdGitlabClient(Creds); var folderPath = GitLabPathHelper.NormalizeFolderId(context?.FolderId); + var request = client.CreateRequest($"/api/v4/projects/{projectId}/repository/tree", Method.Get); + request.AddQueryParameter("path", string.IsNullOrEmpty(folderPath) ? "/" : folderPath); - var tree = await client.Trees.GetAsync(projectId, options => - { - options.Path = string.IsNullOrEmpty(folderPath) ? "/" : folderPath; - options.Reference = _branchRequest.Name; - }); + if (!string.IsNullOrWhiteSpace(_branchRequest.Name)) + request.AddQueryParameter("ref", _branchRequest.Name); + + var tree = await client.ExecuteWithErrorHandling>(request); return tree .Where(x => x.Type == "tree") @@ -80,12 +82,12 @@ public Task> GetFolderPathAsync( return Task.FromResult>(path); } - private ProjectId GetProjectId() + private int GetProjectId() { if (string.IsNullOrWhiteSpace(_repositoryRequest.RepositoryId)) throw new PluginMisconfigurationException("You should select a repository first."); - return (ProjectId)int.Parse(_repositoryRequest.RepositoryId); + return int.Parse(_repositoryRequest.RepositoryId); } private static string GetDisplayName(string path) diff --git a/Apps.GitLab/DataSourceHandlers/MergeRequestDataHandler.cs b/Apps.GitLab/DataSourceHandlers/MergeRequestDataHandler.cs index 919b1e0..5ded96e 100644 --- a/Apps.GitLab/DataSourceHandlers/MergeRequestDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/MergeRequestDataHandler.cs @@ -1,10 +1,11 @@ -using Apps.Gitlab.Models.Respository.Requests; using Apps.Gitlab; +using Apps.Gitlab.Models.Respository.Requests; +using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Dynamic; using Blackbird.Applications.Sdk.Common.Invocation; -using Blackbird.Applications.Sdk.Common; -using GitLabApiClient.Internal.Paths; +using GitLabApiClient.Models.MergeRequests.Responses; +using RestSharp; namespace Apps.GitLab.DataSourceHandlers; @@ -26,8 +27,11 @@ public async Task> GetDataAsync( { if (RepositoryRequest == null || string.IsNullOrWhiteSpace(RepositoryRequest.RepositoryId)) throw new ArgumentException("Please, specify repository first"); - var projectId = (ProjectId)int.Parse(RepositoryRequest.RepositoryId); - var mergeRequests = await new BlackbirdGitlabClient(Creds).Client.MergeRequests.GetAsync(projectId, (options) => { }); + + var projectId = int.Parse(RepositoryRequest.RepositoryId); + var client = new BlackbirdGitlabClient(Creds); + var request = client.CreateRequest($"/api/v4/projects/{projectId}/merge_requests", Method.Get); + var mergeRequests = await client.ExecuteWithErrorHandling>(request); return mergeRequests .Where(x => context.SearchString == null || @@ -35,4 +39,4 @@ public async Task> GetDataAsync( .Take(20) .ToDictionary(x => x.Iid.ToString(), x => x.Title); } -} \ No newline at end of file +} diff --git a/Apps.GitLab/DataSourceHandlers/RepositoryDataHandler.cs b/Apps.GitLab/DataSourceHandlers/RepositoryDataHandler.cs index 365cf59..bd02c34 100644 --- a/Apps.GitLab/DataSourceHandlers/RepositoryDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/RepositoryDataHandler.cs @@ -1,7 +1,9 @@ -using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Dynamic; using Blackbird.Applications.Sdk.Common.Invocation; +using GitLabApiClient.Models.Projects.Responses; +using RestSharp; namespace Apps.Gitlab.DataSourceHandlers; @@ -18,7 +20,11 @@ public async Task> GetDataAsync( DataSourceContext context, CancellationToken cancellationToken) { - var content = await new BlackbirdGitlabClient(Creds).Client.Projects.GetAsync((options) => { options.IsMemberOf = true; }); + var client = new BlackbirdGitlabClient(Creds); + var request = client.CreateRequest("/api/v4/projects", Method.Get); + request.AddQueryParameter("membership", "true"); + + var content = await client.ExecuteWithErrorHandling>(request); return content .Where(x => context.SearchString == null || x.Name.Contains(context.SearchString, StringComparison.OrdinalIgnoreCase)) @@ -26,4 +32,4 @@ public async Task> GetDataAsync( .Take(20) .ToDictionary(x => x.Id.ToString(), x => x.Name); } -} \ No newline at end of file +} diff --git a/Apps.GitLab/DataSourceHandlers/UsersDataHandler.cs b/Apps.GitLab/DataSourceHandlers/UsersDataHandler.cs index 44057c7..c2d5474 100644 --- a/Apps.GitLab/DataSourceHandlers/UsersDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/UsersDataHandler.cs @@ -1,7 +1,8 @@ -using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Dynamic; using Blackbird.Applications.Sdk.Common.Invocation; +using RestSharp; namespace Apps.Gitlab.DataSourceHandlers; @@ -21,7 +22,18 @@ public async Task> GetDataAsync( if (string.IsNullOrWhiteSpace(context.SearchString)) return new Dictionary(); - var content = await new BlackbirdGitlabClient(Creds).Client.Users.GetAsync(); - return content.Take(30).ToDictionary(x => x.Id.ToString(), x => $"{x.Username}"); + var client = new BlackbirdGitlabClient(Creds); + var request = client.CreateRequest("/api/v4/users", Method.Get); + request.AddQueryParameter("search", context.SearchString); + + var content = await client.ExecuteWithErrorHandling>(request); + return content.Take(30).ToDictionary(x => x.Id.ToString(), x => x.Username); + } + + private class UserResponse + { + public int Id { get; set; } + + public string Username { get; set; } = string.Empty; } -} \ No newline at end of file +} diff --git a/Apps.GitLab/Utils/ErrorHandler.cs b/Apps.GitLab/Utils/ErrorHandler.cs deleted file mode 100644 index 5ef9513..0000000 --- a/Apps.GitLab/Utils/ErrorHandler.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Blackbird.Applications.Sdk.Common.Exceptions; -using GitLabApiClient; - -namespace Apps.GitLab.Utils; - -public static class ErrorHandler -{ - public static async Task ExecuteWithErrorHandlingAsync(Func action) - { - try - { - await action(); - } - catch (GitLabException ex) - { - throw MapGitLabException(ex); - } - catch (Exception ex) - { - throw new PluginApplicationException(ex.Message); - } - } - - public static async Task ExecuteWithErrorHandlingAsync(Func> action) - { - try - { - return await action(); - } - catch (GitLabException ex) - { - throw MapGitLabException(ex); - } - catch (Exception ex) - { - throw new PluginApplicationException(ex.Message); - } - } - - private static Exception MapGitLabException(GitLabException ex) - { - var statusCode = TryGetStatusCode(ex); - - return statusCode switch - { - 401 or 403 => new PluginMisconfigurationException( - "GitLab credentials are invalid or do not have sufficient permissions. Please check your access token and scopes."), - 404 => new PluginApplicationException("Resource was not found in GitLab. Please verify the repository ID / path / branch."), - 429 => new PluginApplicationException("GitLab rate limit reached. Please try again later."), - _ => new PluginApplicationException(ex.Message) - }; - } - - private static int? TryGetStatusCode(GitLabException ex) - { - var type = ex.GetType(); - - var statusCodeProp = type.GetProperty("StatusCode"); - if (statusCodeProp?.GetValue(ex) is int sc1) return sc1; - - var statusProp = type.GetProperty("Status"); - if (statusProp?.GetValue(ex) is int sc2) return sc2; - - if (statusCodeProp?.GetValue(ex) is System.Net.HttpStatusCode enumSc) - return (int)enumSc; - - return null; - } -} diff --git a/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs b/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs index bf52615..189eff1 100644 --- a/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs +++ b/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs @@ -1,49 +1,76 @@ -using Blackbird.Applications.Sdk.Common.Webhooks; -using Blackbird.Applications.Sdk.Common; +using Apps.Gitlab; +using Apps.Gitlab.Models.Branch.Requests; using Apps.Gitlab.Webhooks.Payloads; +using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Invocation; -using Apps.Gitlab; -using GitLabApiClient.Internal.Paths; -using Apps.Gitlab.Models.Branch.Requests; +using Blackbird.Applications.Sdk.Common.Webhooks; +using Newtonsoft.Json; +using RestSharp; namespace Apps.GitLab.Webhooks.Handlers; public class PushEventHandler : BaseInvocable, IWebhookEventHandler { - private ProjectId RepositoryId { get; set; } + private int RepositoryId { get; set; } private GetOptionalBranchRequest BranchRequest { get; set; } - public PushEventHandler(InvocationContext invocationContext, - [WebhookParameter(true)] WebhookRepositoryInput repositoryRequest, + public PushEventHandler(InvocationContext invocationContext, + [WebhookParameter(true)] WebhookRepositoryInput repositoryRequest, [WebhookParameter(true)] GetOptionalBranchRequest branchRequest) : base(invocationContext) { - RepositoryId = (ProjectId)int.Parse(repositoryRequest.RepositoryId); + RepositoryId = int.Parse(repositoryRequest.RepositoryId); BranchRequest = branchRequest; } public async Task SubscribeAsync(IEnumerable authenticationCredentialsProviders, Dictionary values) { - var createWebhookRequest = new GitLabApiClient.Models.Webhooks.Requests.CreateWebhookRequest(values["payloadUrl"]) + var client = new BlackbirdGitlabClient(authenticationCredentialsProviders); + var request = client.CreateRequest($"/api/v4/projects/{RepositoryId}/hooks", Method.Post); + request.AddJsonBody(new CreateWebhookRequest { + Url = values["payloadUrl"], PushEvents = true, - }; - if (BranchRequest != null && !string.IsNullOrEmpty(BranchRequest.Name)) - { - createWebhookRequest.PushEventsBranchFilter = BranchRequest.Name; - } - await new BlackbirdGitlabClient(authenticationCredentialsProviders).Client.Webhooks.CreateAsync(RepositoryId, createWebhookRequest); + PushEventsBranchFilter = !string.IsNullOrEmpty(BranchRequest?.Name) ? BranchRequest.Name : null + }); + + await client.ExecuteWithErrorHandling(request); } public async Task UnsubscribeAsync(IEnumerable authenticationCredentialsProviders, Dictionary values) { - var projectWebhooks = await new BlackbirdGitlabClient(authenticationCredentialsProviders).Client.Webhooks.GetAsync(RepositoryId); + var client = new BlackbirdGitlabClient(authenticationCredentialsProviders); + var listRequest = client.CreateRequest($"/api/v4/projects/{RepositoryId}/hooks", Method.Get); + var projectWebhooks = await client.ExecuteWithErrorHandling>(listRequest); var webhook = projectWebhooks.FirstOrDefault(x => x.PushEvents); - if(webhook != null) + + if (webhook != null) { - await new BlackbirdGitlabClient(authenticationCredentialsProviders).Client.Webhooks.DeleteAsync(RepositoryId, webhook.Id); - } + var deleteRequest = client.CreateRequest($"/api/v4/projects/{RepositoryId}/hooks/{webhook.Id}", Method.Delete); + await client.ExecuteWithErrorHandling(deleteRequest); + } + } + + private class CreateWebhookRequest + { + [JsonProperty("url")] + public string Url { get; set; } = string.Empty; + + [JsonProperty("push_events")] + public bool PushEvents { get; set; } + + [JsonProperty("push_events_branch_filter")] + public string? PushEventsBranchFilter { get; set; } + } + + private class WebhookResponse + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("push_events")] + public bool PushEvents { get; set; } } -} \ No newline at end of file +} From 4836806c2f51fd9b79e3f1fcd9a73fc7660b30ca Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Fri, 3 Apr 2026 18:57:21 +0300 Subject: [PATCH 10/12] Adjustments according to comments 1 --- Apps.GitLab/Actions/Base/GitLabActions.cs | 3 +- Apps.GitLab/Actions/BranchActions.cs | 6 ++-- Apps.GitLab/Actions/CommitActions.cs | 21 +++++++------ Apps.GitLab/Actions/PullRequestActions.cs | 11 ++++--- Apps.GitLab/Actions/RepositoryActions.cs | 16 +++++----- Apps.GitLab/BlackbirdGitLabClient.cs | 31 ++++++++++--------- .../Connections/ConnectionValidator.cs | 20 ++++++++---- Apps.GitLab/Constants/GitLabCommitActions.cs | 8 +++++ Apps.GitLab/Constants/GitLabItemTypes.cs | 7 +++++ .../DataSourceHandlers/BranchDataHandler.cs | 5 +-- .../FilePickerDataHandler.cs | 5 +-- .../FolderPickerDataHandler.cs | 5 +-- .../MergeRequestDataHandler.cs | 5 +-- .../RepositoryDataHandler.cs | 2 +- .../DataSourceHandlers/UsersDataHandler.cs | 12 ++----- Apps.GitLab/Dtos/RepositoryInfo.cs | 9 ++++++ .../Models/User/Responses/UserResponse.cs | 8 +++++ Apps.GitLab/Utils/ParsingUtils.cs | 14 +++++++++ .../Webhooks/Handlers/PushEventHandler.cs | 9 +++--- 19 files changed, 129 insertions(+), 68 deletions(-) create mode 100644 Apps.GitLab/Constants/GitLabCommitActions.cs create mode 100644 Apps.GitLab/Constants/GitLabItemTypes.cs create mode 100644 Apps.GitLab/Dtos/RepositoryInfo.cs create mode 100644 Apps.GitLab/Models/User/Responses/UserResponse.cs create mode 100644 Apps.GitLab/Utils/ParsingUtils.cs diff --git a/Apps.GitLab/Actions/Base/GitLabActions.cs b/Apps.GitLab/Actions/Base/GitLabActions.cs index 043f3fe..83c1730 100644 --- a/Apps.GitLab/Actions/Base/GitLabActions.cs +++ b/Apps.GitLab/Actions/Base/GitLabActions.cs @@ -1,6 +1,7 @@ using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Invocation; +using Apps.GitLab.Utils; namespace Apps.Gitlab.Actions.Base; @@ -17,5 +18,5 @@ public GitLabActions(InvocationContext invocationContext) : base(invocationConte } protected static int ParseProjectId(string repositoryId) - => int.Parse(repositoryId); + => ParsingUtils.ParseIntOrThrow(repositoryId, "Repository ID"); } diff --git a/Apps.GitLab/Actions/BranchActions.cs b/Apps.GitLab/Actions/BranchActions.cs index 2e58f28..cba7969 100644 --- a/Apps.GitLab/Actions/BranchActions.cs +++ b/Apps.GitLab/Actions/BranchActions.cs @@ -24,7 +24,7 @@ public BranchActions(InvocationContext invocationContext, IFileManagementClient public async Task ListRepositoryBranches([ActionParameter] GetRepositoryRequest input) { var projectId = ParseProjectId(input.RepositoryId); - var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/branches", Method.Get); + var request = RestClient.CreateRequest($"/projects/{projectId}/repository/branches", Method.Get); var branches = await RestClient.ExecuteWithErrorHandling>(request); return new ListRepositoryBranchesResponse @@ -40,7 +40,7 @@ public async Task GetBranch( { var projectId = ParseProjectId(repositoryRequest.RepositoryId); var request = RestClient.CreateRequest( - $"/api/v4/projects/{projectId}/repository/branches/{Uri.EscapeDataString(input.Name)}", + $"/projects/{projectId}/repository/branches/{Uri.EscapeDataString(input.Name)}", Method.Get); var branch = await RestClient.ExecuteWithErrorHandling(request); @@ -53,7 +53,7 @@ public async Task CreateBranch( [ActionParameter] Models.Branch.Requests.CreateBranchRequest input) { var projectId = ParseProjectId(repositoryRequest.RepositoryId); - var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/branches", Method.Post); + var request = RestClient.CreateRequest($"/projects/{projectId}/repository/branches", Method.Post); request.AddParameter("branch", input.NewBranchName); request.AddParameter("ref", input.BaseBranchName); diff --git a/Apps.GitLab/Actions/CommitActions.cs b/Apps.GitLab/Actions/CommitActions.cs index 433a092..b7e25f0 100644 --- a/Apps.GitLab/Actions/CommitActions.cs +++ b/Apps.GitLab/Actions/CommitActions.cs @@ -3,6 +3,7 @@ using Apps.Gitlab.Models.Commit.Requests; using Apps.Gitlab.Models.Respository.Requests; using Apps.Gitlab.Webhooks; +using Apps.GitLab.Constants; using Apps.GitLab.Dtos; using Apps.GitLab.Models.Commit.Requests; using Apps.GitLab.Models.Commit.Responses; @@ -35,7 +36,7 @@ public async Task ListRepositoryCommits( [ActionParameter] GetOptionalBranchRequest branchRequest) { var projectId = ParseProjectId(repositoryRequest.RepositoryId); - var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/commits", Method.Get); + var request = RestClient.CreateRequest($"/projects/{projectId}/repository/commits", Method.Get); if (!string.IsNullOrWhiteSpace(branchRequest.Name)) request.AddQueryParameter("ref_name", branchRequest.Name); @@ -54,7 +55,7 @@ public async Task GetCommit( { var projectId = ParseProjectId(repositoryRequest.RepositoryId); var request = RestClient.CreateRequest( - $"/api/v4/projects/{projectId}/repository/commits/{Uri.EscapeDataString(input.CommitId)}", + $"/projects/{projectId}/repository/commits/{Uri.EscapeDataString(input.CommitId)}", Method.Get); return await RestClient.ExecuteWithErrorHandling(request); @@ -71,7 +72,7 @@ public async Task ListAddedOrModifiedInHours throw new ArgumentException("Specify more than 0 hours!"); var projectId = ParseProjectId(repositoryRequest.RepositoryId); - var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/commits", Method.Get); + var request = RestClient.CreateRequest($"/projects/{projectId}/repository/commits", Method.Get); request.AddQueryParameter("since", DateTime.Now.AddHours(-hoursRequest.Hours).ToString("O")); if (!string.IsNullOrWhiteSpace(branchRequest.Name)) @@ -83,7 +84,7 @@ public async Task ListAddedOrModifiedInHours foreach (var commit in commits) { var diffRequest = RestClient.CreateRequest( - $"/api/v4/projects/{projectId}/repository/commits/{Uri.EscapeDataString(commit.Id)}/diff", + $"/projects/{projectId}/repository/commits/{Uri.EscapeDataString(commit.Id)}/diff", Method.Get); var diffs = await RestClient.ExecuteWithErrorHandling>(diffRequest); @@ -114,7 +115,7 @@ public async Task PushFile( await new RepositoryActions(InvocationContext, _fileManagementClient).ListRepositoryContent( repositoryRequest, branchRequest, new FolderContentWithTypeRequest { IncludeSubfolders = true }); - if (repContent.Content.Where(x => x.Type == "blob").Any(p => p.Path == input.DestinationFilePath)) + if (repContent.Content.Where(x => x.Type == GitLabItemTypes.Blob).Any(p => p.Path == input.DestinationFilePath)) { return await UpdateFile( repositoryRequest, @@ -127,7 +128,7 @@ public async Task PushFile( }); } - if (repContent.Content.Where(x => x.Type == "tree").Select(x => x.Path) + if (repContent.Content.Where(x => x.Type == GitLabItemTypes.Tree).Select(x => x.Path) .Contains(input.DestinationFilePath.Trim('/'))) { throw new PluginApplicationException("Destination file path is invalid!"); @@ -136,7 +137,7 @@ public async Task PushFile( var file = await _fileManagementClient.DownloadAsync(input.File); var fileBytes = await file.GetByteData(); var pushFileResult = await RestClient.PushChanges(projectId, branchRequest.Name, input.CommitMessage, - input.DestinationFilePath, fileBytes, "create"); + input.DestinationFilePath, fileBytes, GitLabCommitActions.Create); return new(pushFileResult); } @@ -152,7 +153,7 @@ public async Task UpdateFile( var repContent = await new RepositoryActions(InvocationContext, _fileManagementClient).ListRepositoryContent( repositoryRequest, branchRequest, - new FolderContentWithTypeRequest { IncludeSubfolders = true, ContentType = "blob" }); + new FolderContentWithTypeRequest { IncludeSubfolders = true, ContentType = GitLabItemTypes.Blob }); if (!repContent.Content.Select(x => x.Path).Contains(input.DestinationFilePath.Trim('/'))) throw new PluginApplicationException("File does not exist by specified file path!"); @@ -160,7 +161,7 @@ public async Task UpdateFile( var file = await _fileManagementClient.DownloadAsync(input.File); var fileBytes = await file.GetByteData(); var fileUpload = await RestClient.PushChanges(projectId, branchRequest.Name, input.CommitMessage, - input.DestinationFilePath, fileBytes, "update"); + input.DestinationFilePath, fileBytes, GitLabCommitActions.Update); return new(fileUpload); } @@ -173,7 +174,7 @@ public async Task DeleteFile( { var projectId = ParseProjectId(repositoryRequest.RepositoryId); var fileDelete = await RestClient.PushChanges(projectId, branchRequest.Name, input.CommitMessage, - input.FilePath, null!, "delete"); + input.FilePath, null, GitLabCommitActions.Delete); return new() { diff --git a/Apps.GitLab/Actions/PullRequestActions.cs b/Apps.GitLab/Actions/PullRequestActions.cs index 268a318..c50d9a2 100644 --- a/Apps.GitLab/Actions/PullRequestActions.cs +++ b/Apps.GitLab/Actions/PullRequestActions.cs @@ -6,6 +6,7 @@ using Blackbird.Applications.Sdk.Common.Actions; using Blackbird.Applications.Sdk.Common.Invocation; using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; +using Apps.GitLab.Utils; using GitLabApiClient.Models.MergeRequests.Responses; using RestSharp; @@ -24,7 +25,7 @@ public async Task ListPullRequests( [ActionParameter] GetRepositoryRequest repositoryRequest) { var projectId = ParseProjectId(repositoryRequest.RepositoryId); - var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/merge_requests", Method.Get); + var request = RestClient.CreateRequest($"/projects/{projectId}/merge_requests", Method.Get); var pulls = await RestClient.ExecuteWithErrorHandling>(request); return new ListPullRequestsResponse @@ -39,8 +40,9 @@ public async Task GetPullRequest( [ActionParameter] GetPullRequest input) { var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var mergeRequestId = ParsingUtils.ParseIntOrThrow(input.PullRequestId, "Pull request ID"); var request = RestClient.CreateRequest( - $"/api/v4/projects/{projectId}/merge_requests/{int.Parse(input.PullRequestId)}", + $"/projects/{projectId}/merge_requests/{mergeRequestId}", Method.Get); return await RestClient.ExecuteWithErrorHandling(request); @@ -52,7 +54,7 @@ public async Task CreatePullRequest( [ActionParameter] CreatePullRequest input) { var projectId = ParseProjectId(repositoryRequest.RepositoryId); - var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/merge_requests", Method.Post); + var request = RestClient.CreateRequest($"/projects/{projectId}/merge_requests", Method.Post); request.AddJsonBody(new { source_branch = input.HeadBranch, @@ -71,8 +73,9 @@ public async Task MergePullRequest( [ActionParameter] MergePullRequest input) { var projectId = ParseProjectId(repositoryRequest.RepositoryId); + var mergeRequestId = ParsingUtils.ParseIntOrThrow(mergeRequest.PullRequestId, "Pull request ID"); var request = RestClient.CreateRequest( - $"/api/v4/projects/{projectId}/merge_requests/{int.Parse(mergeRequest.PullRequestId)}/merge", + $"/projects/{projectId}/merge_requests/{mergeRequestId}/merge", Method.Put); request.AddJsonBody(new { diff --git a/Apps.GitLab/Actions/RepositoryActions.cs b/Apps.GitLab/Actions/RepositoryActions.cs index 5b0fbb2..fe2c783 100644 --- a/Apps.GitLab/Actions/RepositoryActions.cs +++ b/Apps.GitLab/Actions/RepositoryActions.cs @@ -37,7 +37,7 @@ public RepositoryActions(InvocationContext invocationContext, IFileManagementCli [Action("Create new repository", Description = "Create new repository")] public Task CreateRepository([ActionParameter] CreateRepositoryInput input) { - var endpoint = "/api/v4/projects"; + var endpoint = "/projects"; if (input.UserId != null) endpoint += $"/user/{input.UserId}"; @@ -59,7 +59,7 @@ public async Task GetFile( var branch = branchRequest.Name ?? repository.DefaultBranch; var request = RestClient.CreateRequest( - $"/api/v4/projects/{projectId}/repository/files/{Uri.EscapeDataString(getFileRequest.FilePath)}", + $"/projects/{projectId}/repository/files/{Uri.EscapeDataString(getFileRequest.FilePath)}", Method.Get); request.AddQueryParameter("ref", branch); @@ -149,7 +149,7 @@ public Task GetRepositoryById([ActionParameter] GetRepositoryRequest in public async Task GetIssuesInRepository([ActionParameter] RepositoryRequest input) { var projectId = ParseProjectId(input.RepositoryId); - var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/issues", Method.Get); + var request = RestClient.CreateRequest($"/projects/{projectId}/issues", Method.Get); var issues = await RestClient.ExecuteWithErrorHandling>(request); return new() @@ -162,7 +162,7 @@ public async Task GetIssuesInRepository([ActionParameter] Rep public async Task GetPullRequestsInRepository([ActionParameter] RepositoryRequest input) { var projectId = ParseProjectId(input.RepositoryId); - var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/merge_requests", Method.Get); + var request = RestClient.CreateRequest($"/projects/{projectId}/merge_requests", Method.Get); var pullRequests = await RestClient.ExecuteWithErrorHandling>(request); return new() @@ -178,7 +178,7 @@ public async Task ListRepositoryContent( [ActionParameter] FolderContentWithTypeRequest input) { var projectId = ParseProjectId(repositoryRequest.RepositoryId); - var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/tree", Method.Get); + var request = RestClient.CreateRequest($"/projects/{projectId}/repository/tree", Method.Get); request.AddQueryParameter("recursive", (input.IncludeSubfolders ?? false).ToString().ToLowerInvariant()); request.AddQueryParameter("path", input.Path ?? "/"); @@ -198,7 +198,7 @@ public async Task ListRepositoryContent( [Action("List repositories", Description = "List all repositories")] public async Task ListRepositories() { - var request = RestClient.CreateRequest("/api/v4/projects", Method.Get); + var request = RestClient.CreateRequest("/projects", Method.Get); request.AddQueryParameter("membership", "true"); var projects = await RestClient.ExecuteWithErrorHandling>(request); @@ -238,7 +238,7 @@ public async Task BranchExists( [ActionParameter][Display("Branch name")] string branchNameRequest) { var projectId = ParseProjectId(repositoryRequest.RepositoryId); - var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}/repository/branches", Method.Get); + var request = RestClient.CreateRequest($"/projects/{projectId}/repository/branches", Method.Get); request.AddQueryParameter("search", branchNameRequest); var branches = await RestClient.ExecuteWithErrorHandling>(request); @@ -247,7 +247,7 @@ public async Task BranchExists( private Task GetProject(int projectId) { - var request = RestClient.CreateRequest($"/api/v4/projects/{projectId}", Method.Get); + var request = RestClient.CreateRequest($"/projects/{projectId}", Method.Get); return RestClient.ExecuteWithErrorHandling(request); } diff --git a/Apps.GitLab/BlackbirdGitLabClient.cs b/Apps.GitLab/BlackbirdGitLabClient.cs index 0176ad5..72a3264 100644 --- a/Apps.GitLab/BlackbirdGitLabClient.cs +++ b/Apps.GitLab/BlackbirdGitLabClient.cs @@ -14,6 +14,7 @@ namespace Apps.Gitlab; public class BlackbirdGitlabClient : BlackBirdRestClient { + private const string ApiPrefix = "/api/v4"; private readonly IEnumerable _authenticationCredentials; protected override JsonSerializerSettings? JsonSettings => JsonConfig.JsonSettings; @@ -44,7 +45,7 @@ public static string GetBaseUrl(IEnumerable c } public GitLabRequest CreateRequest(string resource, Method method) - => new(resource, method, _authenticationCredentials); + => new(NormalizeApiResource(resource), method, _authenticationCredentials); public async Task ExecuteForBytesWithErrorHandling(RestRequest request) { @@ -56,7 +57,7 @@ public async Task ExecuteForBytesWithErrorHandling(RestRequest request) public async Task GetArchive(int projectId, string? branchName) { var branchCommit = !string.IsNullOrWhiteSpace(branchName) ? $"?sha={Uri.EscapeDataString(branchName)}" : ""; - var request = CreateRequest($"/api/v4/projects/{projectId}/repository/archive.zip{branchCommit}", Method.Get); + var request = CreateRequest($"/projects/{projectId}/repository/archive.zip{branchCommit}", Method.Get); return await ExecuteForBytesWithErrorHandling(request); } @@ -65,9 +66,9 @@ public async Task PushChanges(int projectId, string? branchName, string string filePath, byte[]? file, string action) { var repository = await ExecuteWithErrorHandling( - CreateRequest($"/api/v4/projects/{projectId}", Method.Get)); + CreateRequest($"/projects/{projectId}", Method.Get)); - var request = CreateRequest($"/api/v4/projects/{projectId}/repository/commits", Method.Post); + var request = CreateRequest($"/projects/{projectId}/repository/commits", Method.Post); request.AddJsonBody(new { branch = branchName ?? repository.DefaultBranch, @@ -83,22 +84,24 @@ public async Task PushChanges(int projectId, string? branchName, string protected override Exception ConfigureErrorException(RestResponse response) { + var message = $"{(int)response.StatusCode}: {response.ErrorMessage ?? response.Content}"; + return response.StatusCode switch { System.Net.HttpStatusCode.Unauthorized or System.Net.HttpStatusCode.Forbidden => - new PluginMisconfigurationException( - "GitLab credentials are invalid or do not have sufficient permissions. Please check your access token and scopes."), - System.Net.HttpStatusCode.NotFound => - new PluginApplicationException("Resource was not found in GitLab. Please verify the repository ID / path / branch."), - (System.Net.HttpStatusCode)429 => - new PluginApplicationException("GitLab rate limit reached. Please try again later."), - _ => new PluginApplicationException(response.Content ?? "GitLab request failed.") + new PluginMisconfigurationException(message), + _ => new PluginApplicationException(message) }; } - private class RepositoryInfo + private static string NormalizeApiResource(string resource) { - [JsonProperty("default_branch")] - public string? DefaultBranch { get; set; } + if (string.IsNullOrWhiteSpace(resource)) + return ApiPrefix; + + if (resource.StartsWith(ApiPrefix, StringComparison.OrdinalIgnoreCase)) + return resource; + + return $"{ApiPrefix}{resource}"; } } diff --git a/Apps.GitLab/Connections/ConnectionValidator.cs b/Apps.GitLab/Connections/ConnectionValidator.cs index 758c2a9..c6825c3 100644 --- a/Apps.GitLab/Connections/ConnectionValidator.cs +++ b/Apps.GitLab/Connections/ConnectionValidator.cs @@ -1,5 +1,6 @@ using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Connections; +using Blackbird.Applications.Sdk.Common.Exceptions; using RestSharp; namespace Apps.Gitlab.Connections; @@ -12,20 +13,27 @@ public async ValueTask ValidateConnection( try { var client = new BlackbirdGitlabClient(authProviders); - await client.ExecuteWithErrorHandling(client.CreateRequest("/api/v4/user", Method.Get)); - + await client.ExecuteWithErrorHandling(client.CreateRequest("/user", Method.Get)); + } + catch (PluginMisconfigurationException ex) + { return new() { - IsValid = true + IsValid = false, + Message = ex.Message }; } - catch (Exception ex) + catch (Exception) { return new() { - IsValid = false, - Message = ex.Message + IsValid = true }; } + + return new() + { + IsValid = true + }; } } diff --git a/Apps.GitLab/Constants/GitLabCommitActions.cs b/Apps.GitLab/Constants/GitLabCommitActions.cs new file mode 100644 index 0000000..1dadd0b --- /dev/null +++ b/Apps.GitLab/Constants/GitLabCommitActions.cs @@ -0,0 +1,8 @@ +namespace Apps.GitLab.Constants; + +public static class GitLabCommitActions +{ + public const string Create = "create"; + public const string Update = "update"; + public const string Delete = "delete"; +} diff --git a/Apps.GitLab/Constants/GitLabItemTypes.cs b/Apps.GitLab/Constants/GitLabItemTypes.cs new file mode 100644 index 0000000..79e547a --- /dev/null +++ b/Apps.GitLab/Constants/GitLabItemTypes.cs @@ -0,0 +1,7 @@ +namespace Apps.GitLab.Constants; + +public static class GitLabItemTypes +{ + public const string Blob = "blob"; + public const string Tree = "tree"; +} diff --git a/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs b/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs index f31ad25..57d72dc 100644 --- a/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs @@ -3,6 +3,7 @@ using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Dynamic; using Blackbird.Applications.Sdk.Common.Invocation; +using Apps.GitLab.Utils; using GitLabApiClient.Models.Branches.Responses; using RestSharp; @@ -27,9 +28,9 @@ public async Task> GetDataAsync( if (RepositoryRequest == null || string.IsNullOrWhiteSpace(RepositoryRequest.RepositoryId)) throw new ArgumentException("Please, specify repository first"); - var projectId = int.Parse(RepositoryRequest.RepositoryId); + var projectId = ParsingUtils.ParseIntOrThrow(RepositoryRequest.RepositoryId, "Repository ID"); var client = new BlackbirdGitlabClient(Creds); - var request = client.CreateRequest($"/api/v4/projects/{projectId}/repository/branches", Method.Get); + var request = client.CreateRequest($"/projects/{projectId}/repository/branches", Method.Get); if (!string.IsNullOrWhiteSpace(context.SearchString)) request.AddQueryParameter("search", context.SearchString); diff --git a/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs b/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs index 0f3b4cf..1b15f4c 100644 --- a/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs @@ -6,6 +6,7 @@ using Blackbird.Applications.Sdk.Common.Invocation; using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; using Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems; +using Apps.GitLab.Utils; using GitLabApiClient.Models.Trees.Responses; using RestSharp; using File = Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems.File; @@ -36,7 +37,7 @@ public async Task> GetFolderContentAsync( var projectId = GetProjectId(); var client = new BlackbirdGitlabClient(Creds); var folderPath = GitLabPathHelper.NormalizeFolderId(context?.FolderId); - var request = client.CreateRequest($"/api/v4/projects/{projectId}/repository/tree", Method.Get); + var request = client.CreateRequest($"/projects/{projectId}/repository/tree", Method.Get); request.AddQueryParameter("path", string.IsNullOrEmpty(folderPath) ? "/" : folderPath); if (!string.IsNullOrWhiteSpace(_branchRequest.Name)) @@ -99,7 +100,7 @@ private int GetProjectId() if (string.IsNullOrWhiteSpace(_repositoryRequest.RepositoryId)) throw new PluginMisconfigurationException("You should select a repository first."); - return int.Parse(_repositoryRequest.RepositoryId); + return ParsingUtils.ParseIntOrThrow(_repositoryRequest.RepositoryId, "Repository ID"); } private static string GetDisplayName(string path) diff --git a/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs b/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs index 7ad4730..87d933b 100644 --- a/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs @@ -6,6 +6,7 @@ using Blackbird.Applications.Sdk.Common.Invocation; using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; using Blackbird.Applications.SDK.Extensions.FileManagement.Models.FileDataSourceItems; +using Apps.GitLab.Utils; using GitLabApiClient.Models.Trees.Responses; using RestSharp; @@ -35,7 +36,7 @@ public async Task> GetFolderContentAsync( var projectId = GetProjectId(); var client = new BlackbirdGitlabClient(Creds); var folderPath = GitLabPathHelper.NormalizeFolderId(context?.FolderId); - var request = client.CreateRequest($"/api/v4/projects/{projectId}/repository/tree", Method.Get); + var request = client.CreateRequest($"/projects/{projectId}/repository/tree", Method.Get); request.AddQueryParameter("path", string.IsNullOrEmpty(folderPath) ? "/" : folderPath); if (!string.IsNullOrWhiteSpace(_branchRequest.Name)) @@ -87,7 +88,7 @@ private int GetProjectId() if (string.IsNullOrWhiteSpace(_repositoryRequest.RepositoryId)) throw new PluginMisconfigurationException("You should select a repository first."); - return int.Parse(_repositoryRequest.RepositoryId); + return ParsingUtils.ParseIntOrThrow(_repositoryRequest.RepositoryId, "Repository ID"); } private static string GetDisplayName(string path) diff --git a/Apps.GitLab/DataSourceHandlers/MergeRequestDataHandler.cs b/Apps.GitLab/DataSourceHandlers/MergeRequestDataHandler.cs index 5ded96e..9cfdaaf 100644 --- a/Apps.GitLab/DataSourceHandlers/MergeRequestDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/MergeRequestDataHandler.cs @@ -4,6 +4,7 @@ using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Dynamic; using Blackbird.Applications.Sdk.Common.Invocation; +using Apps.GitLab.Utils; using GitLabApiClient.Models.MergeRequests.Responses; using RestSharp; @@ -28,9 +29,9 @@ public async Task> GetDataAsync( if (RepositoryRequest == null || string.IsNullOrWhiteSpace(RepositoryRequest.RepositoryId)) throw new ArgumentException("Please, specify repository first"); - var projectId = int.Parse(RepositoryRequest.RepositoryId); + var projectId = ParsingUtils.ParseIntOrThrow(RepositoryRequest.RepositoryId, "Repository ID"); var client = new BlackbirdGitlabClient(Creds); - var request = client.CreateRequest($"/api/v4/projects/{projectId}/merge_requests", Method.Get); + var request = client.CreateRequest($"/projects/{projectId}/merge_requests", Method.Get); var mergeRequests = await client.ExecuteWithErrorHandling>(request); return mergeRequests diff --git a/Apps.GitLab/DataSourceHandlers/RepositoryDataHandler.cs b/Apps.GitLab/DataSourceHandlers/RepositoryDataHandler.cs index bd02c34..e480a65 100644 --- a/Apps.GitLab/DataSourceHandlers/RepositoryDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/RepositoryDataHandler.cs @@ -21,7 +21,7 @@ public async Task> GetDataAsync( CancellationToken cancellationToken) { var client = new BlackbirdGitlabClient(Creds); - var request = client.CreateRequest("/api/v4/projects", Method.Get); + var request = client.CreateRequest("/projects", Method.Get); request.AddQueryParameter("membership", "true"); var content = await client.ExecuteWithErrorHandling>(request); diff --git a/Apps.GitLab/DataSourceHandlers/UsersDataHandler.cs b/Apps.GitLab/DataSourceHandlers/UsersDataHandler.cs index c2d5474..77718af 100644 --- a/Apps.GitLab/DataSourceHandlers/UsersDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/UsersDataHandler.cs @@ -2,6 +2,7 @@ using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Dynamic; using Blackbird.Applications.Sdk.Common.Invocation; +using Apps.GitLab.Models.User.Responses; using RestSharp; namespace Apps.Gitlab.DataSourceHandlers; @@ -23,17 +24,10 @@ public async Task> GetDataAsync( return new Dictionary(); var client = new BlackbirdGitlabClient(Creds); - var request = client.CreateRequest("/api/v4/users", Method.Get); + var request = client.CreateRequest("/users", Method.Get); request.AddQueryParameter("search", context.SearchString); var content = await client.ExecuteWithErrorHandling>(request); - return content.Take(30).ToDictionary(x => x.Id.ToString(), x => x.Username); - } - - private class UserResponse - { - public int Id { get; set; } - - public string Username { get; set; } = string.Empty; + return content.ToDictionary(x => x.Id.ToString(), x => x.Username); } } diff --git a/Apps.GitLab/Dtos/RepositoryInfo.cs b/Apps.GitLab/Dtos/RepositoryInfo.cs new file mode 100644 index 0000000..5bac2ff --- /dev/null +++ b/Apps.GitLab/Dtos/RepositoryInfo.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace Apps.GitLab.Dtos; + +public class RepositoryInfo +{ + [JsonProperty("default_branch")] + public string? DefaultBranch { get; set; } +} diff --git a/Apps.GitLab/Models/User/Responses/UserResponse.cs b/Apps.GitLab/Models/User/Responses/UserResponse.cs new file mode 100644 index 0000000..90e1880 --- /dev/null +++ b/Apps.GitLab/Models/User/Responses/UserResponse.cs @@ -0,0 +1,8 @@ +namespace Apps.GitLab.Models.User.Responses; + +public class UserResponse +{ + public int Id { get; set; } + + public string Username { get; set; } = string.Empty; +} diff --git a/Apps.GitLab/Utils/ParsingUtils.cs b/Apps.GitLab/Utils/ParsingUtils.cs new file mode 100644 index 0000000..04af4fb --- /dev/null +++ b/Apps.GitLab/Utils/ParsingUtils.cs @@ -0,0 +1,14 @@ +using Blackbird.Applications.Sdk.Common.Exceptions; + +namespace Apps.GitLab.Utils; + +public static class ParsingUtils +{ + public static int ParseIntOrThrow(string value, string fieldName) + { + if (int.TryParse(value, out var parsed)) + return parsed; + + throw new PluginMisconfigurationException($"{fieldName} should be a valid integer."); + } +} diff --git a/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs b/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs index 189eff1..ba780fc 100644 --- a/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs +++ b/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs @@ -5,6 +5,7 @@ using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Invocation; using Blackbird.Applications.Sdk.Common.Webhooks; +using Apps.GitLab.Utils; using Newtonsoft.Json; using RestSharp; @@ -19,7 +20,7 @@ public PushEventHandler(InvocationContext invocationContext, [WebhookParameter(true)] WebhookRepositoryInput repositoryRequest, [WebhookParameter(true)] GetOptionalBranchRequest branchRequest) : base(invocationContext) { - RepositoryId = int.Parse(repositoryRequest.RepositoryId); + RepositoryId = ParsingUtils.ParseIntOrThrow(repositoryRequest.RepositoryId, "Repository ID"); BranchRequest = branchRequest; } @@ -27,7 +28,7 @@ public async Task SubscribeAsync(IEnumerable Dictionary values) { var client = new BlackbirdGitlabClient(authenticationCredentialsProviders); - var request = client.CreateRequest($"/api/v4/projects/{RepositoryId}/hooks", Method.Post); + var request = client.CreateRequest($"/projects/{RepositoryId}/hooks", Method.Post); request.AddJsonBody(new CreateWebhookRequest { Url = values["payloadUrl"], @@ -42,13 +43,13 @@ public async Task UnsubscribeAsync(IEnumerable values) { var client = new BlackbirdGitlabClient(authenticationCredentialsProviders); - var listRequest = client.CreateRequest($"/api/v4/projects/{RepositoryId}/hooks", Method.Get); + var listRequest = client.CreateRequest($"/projects/{RepositoryId}/hooks", Method.Get); var projectWebhooks = await client.ExecuteWithErrorHandling>(listRequest); var webhook = projectWebhooks.FirstOrDefault(x => x.PushEvents); if (webhook != null) { - var deleteRequest = client.CreateRequest($"/api/v4/projects/{RepositoryId}/hooks/{webhook.Id}", Method.Delete); + var deleteRequest = client.CreateRequest($"/projects/{RepositoryId}/hooks/{webhook.Id}", Method.Delete); await client.ExecuteWithErrorHandling(deleteRequest); } } From 0abd47b3969675c9f8b6063e8b79e4152da1817c Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Fri, 3 Apr 2026 19:15:40 +0300 Subject: [PATCH 11/12] Adjustments according to comments 2 --- Apps.GitLab/Actions/BranchActions.cs | 7 +-- Apps.GitLab/Actions/CommitActions.cs | 2 +- Apps.GitLab/Actions/PullRequestActions.cs | 8 +--- Apps.GitLab/Actions/RepositoryActions.cs | 2 +- .../Auth/OAuth2/OAuth2AuthorizeService.cs | 5 +-- Apps.GitLab/Auth/OAuth2/OAuth2TokenService.cs | 5 +-- Apps.GitLab/Auth/OAuth2/OAuth2UrlHelper.cs | 9 ++++ .../FilePickerDataHandler.cs | 44 +++++-------------- .../FolderPickerDataHandler.cs | 44 +++++-------------- .../DataSourceHandlers/GitLabDataHandler.cs | 30 +++++++++++++ Tests.GitLab/BranchActionTests.cs | 2 +- 11 files changed, 68 insertions(+), 90 deletions(-) create mode 100644 Apps.GitLab/Auth/OAuth2/OAuth2UrlHelper.cs create mode 100644 Apps.GitLab/DataSourceHandlers/GitLabDataHandler.cs diff --git a/Apps.GitLab/Actions/BranchActions.cs b/Apps.GitLab/Actions/BranchActions.cs index cba7969..1ddd2f3 100644 --- a/Apps.GitLab/Actions/BranchActions.cs +++ b/Apps.GitLab/Actions/BranchActions.cs @@ -13,12 +13,9 @@ namespace Apps.Gitlab.Actions; [ActionList("Branch")] -public class BranchActions : GitLabActions +public class BranchActions(InvocationContext invocationContext) + : GitLabActions(invocationContext) { - public BranchActions(InvocationContext invocationContext, IFileManagementClient fileManagementClient) - : base(invocationContext) - { - } [Action("List branches", Description = "List respository branches")] public async Task ListRepositoryBranches([ActionParameter] GetRepositoryRequest input) diff --git a/Apps.GitLab/Actions/CommitActions.cs b/Apps.GitLab/Actions/CommitActions.cs index b7e25f0..d693816 100644 --- a/Apps.GitLab/Actions/CommitActions.cs +++ b/Apps.GitLab/Actions/CommitActions.cs @@ -131,7 +131,7 @@ public async Task PushFile( if (repContent.Content.Where(x => x.Type == GitLabItemTypes.Tree).Select(x => x.Path) .Contains(input.DestinationFilePath.Trim('/'))) { - throw new PluginApplicationException("Destination file path is invalid!"); + throw new PluginMisconfigurationException("Destination file path is invalid!"); } var file = await _fileManagementClient.DownloadAsync(input.File); diff --git a/Apps.GitLab/Actions/PullRequestActions.cs b/Apps.GitLab/Actions/PullRequestActions.cs index c50d9a2..4fa3530 100644 --- a/Apps.GitLab/Actions/PullRequestActions.cs +++ b/Apps.GitLab/Actions/PullRequestActions.cs @@ -5,7 +5,6 @@ using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Actions; using Blackbird.Applications.Sdk.Common.Invocation; -using Blackbird.Applications.SDK.Extensions.FileManagement.Interfaces; using Apps.GitLab.Utils; using GitLabApiClient.Models.MergeRequests.Responses; using RestSharp; @@ -13,12 +12,9 @@ namespace Apps.Gitlab.Actions; [ActionList("Pull request")] -public class PullRequestActions : GitLabActions +public class PullRequestActions(InvocationContext invocationContext) + : GitLabActions(invocationContext) { - public PullRequestActions(InvocationContext invocationContext, IFileManagementClient fileManagementClient) - : base(invocationContext) - { - } [Action("List merge requests", Description = "List merge requests")] public async Task ListPullRequests( diff --git a/Apps.GitLab/Actions/RepositoryActions.cs b/Apps.GitLab/Actions/RepositoryActions.cs index fe2c783..82c7201 100644 --- a/Apps.GitLab/Actions/RepositoryActions.cs +++ b/Apps.GitLab/Actions/RepositoryActions.cs @@ -65,7 +65,7 @@ public async Task GetFile( var fileData = await RestClient.ExecuteWithErrorHandling(request); if (fileData == null) - throw new ArgumentException($"File does not exist ({getFileRequest.FilePath})"); + throw new PluginMisconfigurationException($"File does not exist ({getFileRequest.FilePath})"); var filename = Path.GetFileName(getFileRequest.FilePath); if (!MimeTypes.TryGetMimeType(filename, out var mimeType)) diff --git a/Apps.GitLab/Auth/OAuth2/OAuth2AuthorizeService.cs b/Apps.GitLab/Auth/OAuth2/OAuth2AuthorizeService.cs index e5ff0a4..e7e48fe 100644 --- a/Apps.GitLab/Auth/OAuth2/OAuth2AuthorizeService.cs +++ b/Apps.GitLab/Auth/OAuth2/OAuth2AuthorizeService.cs @@ -49,7 +49,7 @@ private static string GetAuthorizationEndpoint(Dictionary values return connectionType switch { ConnectionTypes.OAuth => "https://gitlab.com/oauth/authorize", - ConnectionTypes.OAuthSelfManaged => $"{GetSelfManagedBaseUrl(values)}/oauth/authorize", + ConnectionTypes.OAuthSelfManaged => $"{OAuth2UrlHelper.GetSelfManagedBaseUrl(values)}/oauth/authorize", _ => throw new Exception($"Unsupported connection type for OAuth authorization: {connectionType}") }; } @@ -63,7 +63,4 @@ private static string GetClientId(Dictionary values, string conn _ => throw new Exception($"Unsupported connection type for OAuth authorization: {connectionType}") }; } - - private static string GetSelfManagedBaseUrl(Dictionary values) - => values[CredNames.BaseUrl].TrimEnd('/'); } diff --git a/Apps.GitLab/Auth/OAuth2/OAuth2TokenService.cs b/Apps.GitLab/Auth/OAuth2/OAuth2TokenService.cs index 8cd6ff7..4f82437 100644 --- a/Apps.GitLab/Auth/OAuth2/OAuth2TokenService.cs +++ b/Apps.GitLab/Auth/OAuth2/OAuth2TokenService.cs @@ -131,7 +131,7 @@ private static string GetTokenEndpoint(Dictionary values, string return connectionType switch { ConnectionTypes.OAuth => "https://gitlab.com/oauth/token", - ConnectionTypes.OAuthSelfManaged => $"{GetSelfManagedBaseUrl(values)}/oauth/token", + ConnectionTypes.OAuthSelfManaged => $"{OAuth2UrlHelper.GetSelfManagedBaseUrl(values)}/oauth/token", _ => throw new Exception($"Unsupported connection type for OAuth token exchange: {connectionType}") }; } @@ -155,7 +155,4 @@ private static string GetClientSecret(Dictionary values, string _ => throw new Exception($"Unsupported connection type for OAuth client secret: {connectionType}") }; } - - private static string GetSelfManagedBaseUrl(Dictionary values) - => values[CredNames.BaseUrl].TrimEnd('/'); } diff --git a/Apps.GitLab/Auth/OAuth2/OAuth2UrlHelper.cs b/Apps.GitLab/Auth/OAuth2/OAuth2UrlHelper.cs new file mode 100644 index 0000000..45c6e48 --- /dev/null +++ b/Apps.GitLab/Auth/OAuth2/OAuth2UrlHelper.cs @@ -0,0 +1,9 @@ +using Apps.GitLab.Constants; + +namespace Apps.Gitlab.Auth.OAuth2; + +internal static class OAuth2UrlHelper +{ + public static string GetSelfManagedBaseUrl(Dictionary values) + => values[CredNames.BaseUrl].TrimEnd('/'); +} diff --git a/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs b/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs index 1b15f4c..76f492d 100644 --- a/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/FilePickerDataHandler.cs @@ -13,37 +13,26 @@ namespace Apps.Gitlab.DataSourceHandlers; -public class FilePickerDataHandler : BaseInvocable, IAsyncFileDataSourceItemHandler +public class FilePickerDataHandler( + InvocationContext invocationContext, + [ActionParameter] GetRepositoryRequest repositoryRequest, + [ActionParameter] GetOptionalBranchRequest branchRequest) + : GitLabDataHandler(invocationContext), IAsyncFileDataSourceItemHandler { - private IEnumerable Creds => - InvocationContext.AuthenticationCredentialsProviders; - - private readonly GetRepositoryRequest _repositoryRequest; - private readonly GetOptionalBranchRequest _branchRequest; - - public FilePickerDataHandler( - InvocationContext invocationContext, - [ActionParameter] GetRepositoryRequest repositoryRequest, - [ActionParameter] GetOptionalBranchRequest branchRequest) : base(invocationContext) - { - _repositoryRequest = repositoryRequest; - _branchRequest = branchRequest; - } public async Task> GetFolderContentAsync( FolderContentDataSourceContext context, CancellationToken cancellationToken) { - var projectId = GetProjectId(); - var client = new BlackbirdGitlabClient(Creds); + var projectId = GetProjectId(repositoryRequest.RepositoryId); var folderPath = GitLabPathHelper.NormalizeFolderId(context?.FolderId); - var request = client.CreateRequest($"/projects/{projectId}/repository/tree", Method.Get); + var request = RestClient.CreateRequest($"/projects/{projectId}/repository/tree", Method.Get); request.AddQueryParameter("path", string.IsNullOrEmpty(folderPath) ? "/" : folderPath); - if (!string.IsNullOrWhiteSpace(_branchRequest.Name)) - request.AddQueryParameter("ref", _branchRequest.Name); + if (!string.IsNullOrWhiteSpace(branchRequest.Name)) + request.AddQueryParameter("ref", branchRequest.Name); - var tree = await client.ExecuteWithErrorHandling>(request); + var tree = await RestClient.ExecuteWithErrorHandling>(request); return tree .OrderBy(x => x.Type == "blob") @@ -95,17 +84,4 @@ public Task> GetFolderPathAsync( return Task.FromResult>(path); } - private int GetProjectId() - { - if (string.IsNullOrWhiteSpace(_repositoryRequest.RepositoryId)) - throw new PluginMisconfigurationException("You should select a repository first."); - - return ParsingUtils.ParseIntOrThrow(_repositoryRequest.RepositoryId, "Repository ID"); - } - - private static string GetDisplayName(string path) - { - var normalizedPath = GitLabPathHelper.NormalizePath(path); - return normalizedPath.Split('/').LastOrDefault() ?? normalizedPath; - } } diff --git a/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs b/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs index 87d933b..8ac3e43 100644 --- a/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/FolderPickerDataHandler.cs @@ -12,37 +12,26 @@ namespace Apps.Gitlab.DataSourceHandlers; -public class FolderPickerDataHandler : BaseInvocable, IAsyncFileDataSourceItemHandler +public class FolderPickerDataHandler( + InvocationContext invocationContext, + [ActionParameter] GetRepositoryRequest repositoryRequest, + [ActionParameter] GetOptionalBranchRequest branchRequest) + : GitLabDataHandler(invocationContext), IAsyncFileDataSourceItemHandler { - private IEnumerable Creds => - InvocationContext.AuthenticationCredentialsProviders; - - private readonly GetRepositoryRequest _repositoryRequest; - private readonly GetOptionalBranchRequest _branchRequest; - - public FolderPickerDataHandler( - InvocationContext invocationContext, - [ActionParameter] GetRepositoryRequest repositoryRequest, - [ActionParameter] GetOptionalBranchRequest branchRequest) : base(invocationContext) - { - _repositoryRequest = repositoryRequest; - _branchRequest = branchRequest; - } public async Task> GetFolderContentAsync( FolderContentDataSourceContext context, CancellationToken cancellationToken) { - var projectId = GetProjectId(); - var client = new BlackbirdGitlabClient(Creds); + var projectId = GetProjectId(repositoryRequest.RepositoryId); var folderPath = GitLabPathHelper.NormalizeFolderId(context?.FolderId); - var request = client.CreateRequest($"/projects/{projectId}/repository/tree", Method.Get); + var request = RestClient.CreateRequest($"/projects/{projectId}/repository/tree", Method.Get); request.AddQueryParameter("path", string.IsNullOrEmpty(folderPath) ? "/" : folderPath); - if (!string.IsNullOrWhiteSpace(_branchRequest.Name)) - request.AddQueryParameter("ref", _branchRequest.Name); + if (!string.IsNullOrWhiteSpace(branchRequest.Name)) + request.AddQueryParameter("ref", branchRequest.Name); - var tree = await client.ExecuteWithErrorHandling>(request); + var tree = await RestClient.ExecuteWithErrorHandling>(request); return tree .Where(x => x.Type == "tree") @@ -83,17 +72,4 @@ public Task> GetFolderPathAsync( return Task.FromResult>(path); } - private int GetProjectId() - { - if (string.IsNullOrWhiteSpace(_repositoryRequest.RepositoryId)) - throw new PluginMisconfigurationException("You should select a repository first."); - - return ParsingUtils.ParseIntOrThrow(_repositoryRequest.RepositoryId, "Repository ID"); - } - - private static string GetDisplayName(string path) - { - var normalizedPath = GitLabPathHelper.NormalizePath(path); - return normalizedPath.Split('/').LastOrDefault() ?? normalizedPath; - } } diff --git a/Apps.GitLab/DataSourceHandlers/GitLabDataHandler.cs b/Apps.GitLab/DataSourceHandlers/GitLabDataHandler.cs new file mode 100644 index 0000000..9e69907 --- /dev/null +++ b/Apps.GitLab/DataSourceHandlers/GitLabDataHandler.cs @@ -0,0 +1,30 @@ +using Apps.GitLab.Utils; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Authentication; +using Blackbird.Applications.Sdk.Common.Exceptions; +using Blackbird.Applications.Sdk.Common.Invocation; + +namespace Apps.Gitlab.DataSourceHandlers; + +public abstract class GitLabDataHandler(InvocationContext invocationContext) : BaseInvocable(invocationContext) +{ + protected IEnumerable Creds => + InvocationContext.AuthenticationCredentialsProviders; + + protected BlackbirdGitlabClient RestClient { get; } = + new(invocationContext.AuthenticationCredentialsProviders); + + protected static int GetProjectId(string? repositoryId) + { + if (string.IsNullOrWhiteSpace(repositoryId)) + throw new PluginMisconfigurationException("You should select a repository first."); + + return ParsingUtils.ParseIntOrThrow(repositoryId, "Repository ID"); + } + + protected static string GetDisplayName(string path) + { + var normalizedPath = GitLabPathHelper.NormalizePath(path); + return normalizedPath.Split('/').LastOrDefault() ?? normalizedPath; + } +} diff --git a/Tests.GitLab/BranchActionTests.cs b/Tests.GitLab/BranchActionTests.cs index 76b833f..93c94c4 100644 --- a/Tests.GitLab/BranchActionTests.cs +++ b/Tests.GitLab/BranchActionTests.cs @@ -13,7 +13,7 @@ public class BranchActionTests : TestBaseWithContext [TestMethod, ContextDataSource(ConnectionTypes.PersonalAccessToken)] public async Task GetBranch_WithValidData_ReturnsBranch(InvocationContext context) { - var action = new BranchActions(context, FileManagementClient); + var action = new BranchActions(context); var result = await action.ListRepositoryBranches(new GetRepositoryRequest { RepositoryId = "70992272" }); From 96deedd11c231d38bf7c1696be1762dd29f2ccac Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Mon, 6 Apr 2026 10:49:36 +0300 Subject: [PATCH 12/12] Adjustments to code according to comments --- Apps.GitLab/Actions/RepositoryActions.cs | 8 ++----- .../DataSourceHandlers/BranchDataHandler.cs | 3 ++- .../Responses/RepositoryFileResponse.cs | 7 ++++++ .../Webhooks/Handlers/PushEventHandler.cs | 22 +------------------ .../Webhooks/Payloads/CreateWebhookRequest.cs | 15 +++++++++++++ .../Webhooks/Payloads/WebhookResponse.cs | 12 ++++++++++ 6 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 Apps.GitLab/Models/Respository/Responses/RepositoryFileResponse.cs create mode 100644 Apps.GitLab/Webhooks/Payloads/CreateWebhookRequest.cs create mode 100644 Apps.GitLab/Webhooks/Payloads/WebhookResponse.cs diff --git a/Apps.GitLab/Actions/RepositoryActions.cs b/Apps.GitLab/Actions/RepositoryActions.cs index 82c7201..c34720a 100644 --- a/Apps.GitLab/Actions/RepositoryActions.cs +++ b/Apps.GitLab/Actions/RepositoryActions.cs @@ -7,6 +7,7 @@ using Apps.Gitlab.Models.Respository.Responses; using Apps.GitLab; using Apps.GitLab.Models.Respository.Requests; +using Apps.GitLab.Models.Respository.Responses; using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Actions; using Blackbird.Applications.Sdk.Common.Exceptions; @@ -95,7 +96,7 @@ public async Task GetAllFilesInFolder( var resultFiles = new List(); var content = await RestClient.GetArchive(projectId, branchRequest.Name); if (content.Length == 0) - throw new PluginApplicationException("Repository is empty!"); + throw new PluginMisconfigurationException("Repository is empty!"); List filesFromZip; using (var stream = new MemoryStream(content)) @@ -250,9 +251,4 @@ private Task GetProject(int projectId) var request = RestClient.CreateRequest($"/projects/{projectId}", Method.Get); return RestClient.ExecuteWithErrorHandling(request); } - - private class RepositoryFileResponse - { - public string Content { get; set; } = string.Empty; - } } diff --git a/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs b/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs index 57d72dc..084746f 100644 --- a/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs +++ b/Apps.GitLab/DataSourceHandlers/BranchDataHandler.cs @@ -6,6 +6,7 @@ using Apps.GitLab.Utils; using GitLabApiClient.Models.Branches.Responses; using RestSharp; +using Blackbird.Applications.Sdk.Common.Exceptions; namespace Apps.Gitlab.DataSourceHandlers; @@ -26,7 +27,7 @@ public async Task> GetDataAsync( CancellationToken cancellationToken) { if (RepositoryRequest == null || string.IsNullOrWhiteSpace(RepositoryRequest.RepositoryId)) - throw new ArgumentException("Please, specify repository first"); + throw new PluginMisconfigurationException("Please, specify repository first"); var projectId = ParsingUtils.ParseIntOrThrow(RepositoryRequest.RepositoryId, "Repository ID"); var client = new BlackbirdGitlabClient(Creds); diff --git a/Apps.GitLab/Models/Respository/Responses/RepositoryFileResponse.cs b/Apps.GitLab/Models/Respository/Responses/RepositoryFileResponse.cs new file mode 100644 index 0000000..cad7760 --- /dev/null +++ b/Apps.GitLab/Models/Respository/Responses/RepositoryFileResponse.cs @@ -0,0 +1,7 @@ +namespace Apps.GitLab.Models.Respository.Responses; + +public class RepositoryFileResponse +{ + public string Content { get; set; } = string.Empty; +} + diff --git a/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs b/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs index ba780fc..c0bef57 100644 --- a/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs +++ b/Apps.GitLab/Webhooks/Handlers/PushEventHandler.cs @@ -8,6 +8,7 @@ using Apps.GitLab.Utils; using Newtonsoft.Json; using RestSharp; +using Apps.GitLab.Webhooks.Payloads; namespace Apps.GitLab.Webhooks.Handlers; @@ -53,25 +54,4 @@ public async Task UnsubscribeAsync(IEnumerable