From 53f2acc3cde90810b00c1b2d7dfc8037475c8d0c Mon Sep 17 00:00:00 2001 From: rrampen Date: Wed, 1 Jan 2020 01:58:12 +0100 Subject: [PATCH] add files and trees client (#77) Co-authored-by: Joseph Petersen --- docker/init.rb | 8 ++ src/GitLabApiClient/FilesClient.cs | 21 +++++ src/GitLabApiClient/GitLabClient.cs | 13 +++ .../Internal/Queries/TreeQueryBuilder.cs | 24 +++++ .../Models/Files/Responses/File.cs | 40 ++++++++ .../Models/Trees/Requests/TreeQueryOptions.cs | 20 ++++ .../Models/Trees/Responses/Tree.cs | 26 ++++++ src/GitLabApiClient/TreesClient.cs | 32 +++++++ test/GitLabApiClient.Test/FilesClientTest.cs | 32 +++++++ test/GitLabApiClient.Test/GitLabClientTest.cs | 2 + test/GitLabApiClient.Test/TreesClientTest.cs | 91 +++++++++++++++++++ 11 files changed, 309 insertions(+) create mode 100644 src/GitLabApiClient/FilesClient.cs create mode 100644 src/GitLabApiClient/Internal/Queries/TreeQueryBuilder.cs create mode 100644 src/GitLabApiClient/Models/Files/Responses/File.cs create mode 100644 src/GitLabApiClient/Models/Trees/Requests/TreeQueryOptions.cs create mode 100644 src/GitLabApiClient/Models/Trees/Responses/Tree.cs create mode 100644 src/GitLabApiClient/TreesClient.cs create mode 100644 test/GitLabApiClient.Test/FilesClientTest.cs create mode 100644 test/GitLabApiClient.Test/TreesClientTest.cs diff --git a/docker/init.rb b/docker/init.rb index ed32b861..b4887e91 100644 --- a/docker/init.rb +++ b/docker/init.rb @@ -1,4 +1,7 @@ #!/usr/bin/env ruby +# Turn off auto devops as it not help for integration testing, causes stuck pipelines. +Gitlab::CurrentSettings.current_application_settings.update!(auto_devops_enabled: false) + u = User.first g = Group.create!(name: 'txxxestgrouxxxp', path: 'txxxestgrouxxxp') @@ -6,6 +9,11 @@ p = Project.create!(namespace: g, creator: u, path: 'txxxestprojecxxxt', name: 'txxxestprojecxxxt') p.repository.create_if_not_exists p.add_maintainer(u) +content = %{# Test project + +Hello world +} +p.repository.create_file(u, 'README.md', content, message: 'Add README.md', branch_name: 'master') c = p.repository.create_dir(u, 'newdir', message: 'Create newdir', branch_name: 'master') t = PersonalAccessToken.new({ user: u, name: 'gitlab-api-client', scopes: ['api']}) diff --git a/src/GitLabApiClient/FilesClient.cs b/src/GitLabApiClient/FilesClient.cs new file mode 100644 index 00000000..30ac045d --- /dev/null +++ b/src/GitLabApiClient/FilesClient.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using GitLabApiClient.Internal.Http; +using GitLabApiClient.Internal.Paths; +using GitLabApiClient.Internal.Utilities; +using GitLabApiClient.Models.Files.Responses; +using GitLabApiClient.Models.Projects.Responses; + +namespace GitLabApiClient +{ + public sealed class FilesClient + { + private readonly GitLabHttpFacade _httpFacade; + + internal FilesClient(GitLabHttpFacade httpFacade) => _httpFacade = httpFacade; + + public async Task GetAsync(ProjectId projectId, string filePath, string reference = "master") + { + return await _httpFacade.Get($"projects/{projectId}/repository/files/{filePath.UrlEncode()}?ref={reference}"); + } + } +} diff --git a/src/GitLabApiClient/GitLabClient.cs b/src/GitLabApiClient/GitLabClient.cs index dc4a2c3d..0259842e 100644 --- a/src/GitLabApiClient/GitLabClient.cs +++ b/src/GitLabApiClient/GitLabClient.cs @@ -50,6 +50,7 @@ public GitLabClient(string hostUrl, string authenticationToken = "") var commitQueryBuilder = new CommitQueryBuilder(); var commitRefsQueryBuilder = new CommitRefsQueryBuilder(); var pipelineQueryBuilder = new PipelineQueryBuilder(); + var treeQueryBuilder = new TreeQueryBuilder(); Issues = new IssuesClient(_httpFacade, issuesQueryBuilder, projectIssuesQueryBuilder, projectIssueNotesQueryBuilder); Uploads = new UploadsClient(_httpFacade); @@ -64,6 +65,8 @@ public GitLabClient(string hostUrl, string authenticationToken = "") Commits = new CommitsClient(_httpFacade, commitQueryBuilder, commitRefsQueryBuilder); Markdown = new MarkdownClient(_httpFacade); Pipelines = new PipelineClient(_httpFacade, pipelineQueryBuilder); + Trees = new TreesClient(_httpFacade, treeQueryBuilder); + Files = new FilesClient(_httpFacade); } /// @@ -121,6 +124,16 @@ public GitLabClient(string hostUrl, string authenticationToken = "") /// public CommitsClient Commits { get; } + /// + /// Access GitLab's trees API. + /// + public TreesClient Trees { get; } + + /// + /// Access GitLab's files API. + /// + public FilesClient Files { get; } + /// /// Access GitLab's Markdown API. /// diff --git a/src/GitLabApiClient/Internal/Queries/TreeQueryBuilder.cs b/src/GitLabApiClient/Internal/Queries/TreeQueryBuilder.cs new file mode 100644 index 00000000..680ac4e5 --- /dev/null +++ b/src/GitLabApiClient/Internal/Queries/TreeQueryBuilder.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; +using GitLabApiClient.Models.Trees.Requests; +using GitLabApiClient.Models.Tags.Requests; + +namespace GitLabApiClient.Internal.Queries +{ + internal class TreeQueryBuilder : QueryBuilder + { + protected override void BuildCore(TreeQueryOptions options) + { + if (!string.IsNullOrEmpty(options.Reference)) + Add("ref", options.Reference); + + if (!string.IsNullOrEmpty(options.Path)) + Add("path", options.Path); + + if (options.Recursive) + Add("recursive", options.Recursive); + + } + } +} diff --git a/src/GitLabApiClient/Models/Files/Responses/File.cs b/src/GitLabApiClient/Models/Files/Responses/File.cs new file mode 100644 index 00000000..0ca24388 --- /dev/null +++ b/src/GitLabApiClient/Models/Files/Responses/File.cs @@ -0,0 +1,40 @@ +using System; +using Newtonsoft.Json; + +namespace GitLabApiClient.Models.Files.Responses +{ + public sealed class File + { + [JsonProperty("file_name")] + public string Filename { get; set; } + + [JsonProperty("file_path")] + public string FullPath { get; set; } + + [JsonProperty("size")] + public int Size { get; set; } + + [JsonProperty("encoding")] + public string Encoding { get; set; } + + [JsonProperty("content_sha256")] + public string ContentSha256 { get; set; } + + [JsonProperty("ref")] + public string Reference { get; set; } + + [JsonProperty("blob_id")] + public string BlobId { get; set; } + + [JsonProperty("commit_id")] + public string CommitId { get; set; } + + [JsonProperty("last_commit_id")] + public string LastCommitId { get; set; } + + [JsonProperty("content")] + public string Content { get; set; } + + public string ContentDecoded => System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(Content)); + } +} diff --git a/src/GitLabApiClient/Models/Trees/Requests/TreeQueryOptions.cs b/src/GitLabApiClient/Models/Trees/Requests/TreeQueryOptions.cs new file mode 100644 index 00000000..afdef3dd --- /dev/null +++ b/src/GitLabApiClient/Models/Trees/Requests/TreeQueryOptions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace GitLabApiClient.Models.Trees.Requests +{ + public sealed class TreeQueryOptions + { + public string Reference { get; set; } + public string Path { get; set; } + public bool Recursive { get; set; } + + internal TreeQueryOptions(string reference = null, string path = null, bool recursive = false) + { + Reference = reference; + Path = path; + Recursive = recursive; + } + } +} diff --git a/src/GitLabApiClient/Models/Trees/Responses/Tree.cs b/src/GitLabApiClient/Models/Trees/Responses/Tree.cs new file mode 100644 index 00000000..3986d864 --- /dev/null +++ b/src/GitLabApiClient/Models/Trees/Responses/Tree.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; +using GitLabApiClient.Models.Releases.Responses; +using Newtonsoft.Json; + +namespace GitLabApiClient.Models.Trees.Responses +{ + public sealed class Tree + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("path")] + public string Path { get; set; } + + [JsonProperty("mode")] + public string Mode { get; set; } + } +} diff --git a/src/GitLabApiClient/TreesClient.cs b/src/GitLabApiClient/TreesClient.cs new file mode 100644 index 00000000..6f9bd90e --- /dev/null +++ b/src/GitLabApiClient/TreesClient.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using GitLabApiClient.Internal.Http; +using GitLabApiClient.Internal.Paths; +using GitLabApiClient.Internal.Queries; +using GitLabApiClient.Models.Trees.Requests; +using GitLabApiClient.Models.Trees.Responses; + +namespace GitLabApiClient +{ + public sealed class TreesClient + { + private readonly GitLabHttpFacade _httpFacade; + private readonly TreeQueryBuilder _treeQueryBuilder; + + internal TreesClient(GitLabHttpFacade httpFacade, TreeQueryBuilder treeQueryBuilder) + { + _httpFacade = httpFacade; + _treeQueryBuilder = treeQueryBuilder; + } + + public async Task> GetAsync(ProjectId projectId, Action options = null) + { + var queryOptions = new TreeQueryOptions(); + options?.Invoke(queryOptions); + + string url = _treeQueryBuilder.Build($"projects/{projectId}/repository/tree", queryOptions); + return await _httpFacade.GetPagedList(url); + } + } +} diff --git a/test/GitLabApiClient.Test/FilesClientTest.cs b/test/GitLabApiClient.Test/FilesClientTest.cs new file mode 100644 index 00000000..3230d39e --- /dev/null +++ b/test/GitLabApiClient.Test/FilesClientTest.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; +using static GitLabApiClient.Test.Utilities.GitLabApiHelper; + +namespace GitLabApiClient.Test +{ + [Trait("Category", "LinuxIntegration")] + [Collection("GitLabContainerFixture")] + public class FilesClientTest + { + private readonly FilesClient _sut = new FilesClient(GetFacade()); + + [Fact] + public async Task GetFile() + { + var file = await _sut.GetAsync(TestProjectId, "README.md"); + file.Content.Should().NotBeNull(); + file.Encoding.Should().Be("base64"); + file.Reference.Should().Be("master"); + file.Filename.Should().Be("README.md"); + file.FullPath.Should().Be("README.md"); + file.Content.Should().Be("IyBUZXN0IHByb2plY3QKCkhlbGxvIHdvcmxkCg=="); + file.Size.Should().Be(28); + file.ContentSha256.Should().Be("b6cb63af62daa14162368903ca4e42350cb1d855446febbdb22fb5c24f9aeedb"); + file.BlobId.Should().HaveLength(40); + file.CommitId.Should().HaveLength(40); + file.LastCommitId.Should().HaveLength(40); + file.ContentDecoded.Should().Be("# Test project\n\nHello world\n"); + } + } +} diff --git a/test/GitLabApiClient.Test/GitLabClientTest.cs b/test/GitLabApiClient.Test/GitLabClientTest.cs index 37a4dbeb..0138c399 100644 --- a/test/GitLabApiClient.Test/GitLabClientTest.cs +++ b/test/GitLabApiClient.Test/GitLabClientTest.cs @@ -37,6 +37,7 @@ public void CheckClients() var sut = new GitLabClient("https://gitlab.com/api/v4/"); sut.Branches.Should().BeAssignableTo(typeof(BranchClient)); sut.Commits.Should().BeAssignableTo(typeof(CommitsClient)); + sut.Files.Should().BeAssignableTo(typeof(FilesClient)); sut.Groups.Should().BeAssignableTo(typeof(GroupsClient)); sut.Issues.Should().BeAssignableTo(typeof(IssuesClient)); sut.Markdown.Should().BeAssignableTo(typeof(MarkdownClient)); @@ -44,6 +45,7 @@ public void CheckClients() sut.Projects.Should().BeAssignableTo(typeof(ProjectsClient)); sut.Releases.Should().BeAssignableTo(typeof(ReleaseClient)); sut.Tags.Should().BeAssignableTo(typeof(TagClient)); + sut.Trees.Should().BeAssignableTo(typeof(TreesClient)); sut.Uploads.Should().BeAssignableTo(typeof(UploadsClient)); sut.Users.Should().BeAssignableTo(typeof(UsersClient)); sut.Webhooks.Should().BeAssignableTo(typeof(WebhookClient)); diff --git a/test/GitLabApiClient.Test/TreesClientTest.cs b/test/GitLabApiClient.Test/TreesClientTest.cs new file mode 100644 index 00000000..e915d957 --- /dev/null +++ b/test/GitLabApiClient.Test/TreesClientTest.cs @@ -0,0 +1,91 @@ +using System.Threading.Tasks; +using FluentAssertions; +using GitLabApiClient.Internal.Queries; +using Xunit; +using static GitLabApiClient.Test.Utilities.GitLabApiHelper; + +namespace GitLabApiClient.Test +{ + [Trait("Category", "LinuxIntegration")] + [Collection("GitLabContainerFixture")] + public class TreesClientTest + { + private readonly TreesClient _sut = new TreesClient(GetFacade(), new TreeQueryBuilder()); + + [Fact] + public async Task GetTrees() + { + var trees = await _sut.GetAsync(TestProjectId); + trees.Should().SatisfyRespectively( + first => + { + first.Id.Should().HaveLength(40); + first.Mode.Should().Be("040000"); + first.Type.Should().Be("tree"); + first.Name.Should().Be("newdir"); + first.Path.Should().Be("newdir"); + }, + second => + { + second.Id.Should().HaveLength(40); + second.Mode.Should().Be("100644"); + second.Type.Should().Be("blob"); + second.Name.Should().Be("README.md"); + second.Path.Should().Be("README.md"); + }); + + var otherTrees = await _sut.GetAsync(TestProjectId, options => options.Reference = "master"); + otherTrees.Should().BeEquivalentTo(trees); + } + + [Fact] + public async Task GetTreesInFolder() + { + var trees = await _sut.GetAsync(TestProjectId, options => options.Path = "newdir/"); + trees.Should().SatisfyRespectively( + first => + { + first.Id.Should().HaveLength(40); + first.Mode.Should().Be("100644"); + first.Type.Should().Be("blob"); + first.Name.Should().Be(".gitkeep"); + first.Path.Should().Be("newdir/.gitkeep"); + }); + } + + [Fact] + public async Task GetTreesRecursively() + { + var trees = await _sut.GetAsync(TestProjectId, options => + { + options.Recursive = true; + options.Reference = "master"; + }); + trees.Should().SatisfyRespectively( + first => + { + first.Id.Should().HaveLength(40); + first.Mode.Should().Be("040000"); + first.Type.Should().Be("tree"); + first.Name.Should().Be("newdir"); + first.Path.Should().Be("newdir"); + }, + second => + { + second.Id.Should().HaveLength(40); + second.Mode.Should().Be("100644"); + second.Type.Should().Be("blob"); + second.Name.Should().Be("README.md"); + second.Path.Should().Be("README.md"); + }, + third => + { + third.Id.Should().HaveLength(40); + third.Mode.Should().Be("100644"); + third.Type.Should().Be("blob"); + third.Name.Should().Be(".gitkeep"); + third.Path.Should().Be("newdir/.gitkeep"); + }); + } + } +}