Skip to content

Commit

Permalink
(#146) Provide initial support for GitLab
Browse files Browse the repository at this point in the history
Need to put some words here...
  • Loading branch information
gep13 committed Aug 30, 2023
1 parent 2007685 commit 6a7a78a
Show file tree
Hide file tree
Showing 24 changed files with 545 additions and 53 deletions.
27 changes: 23 additions & 4 deletions src/GitReleaseManager.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
using GitReleaseManager.Core.Commands;
using GitReleaseManager.Core.Configuration;
using GitReleaseManager.Core.Helpers;
using GitReleaseManager.Core.Model;
using GitReleaseManager.Core.Options;
using GitReleaseManager.Core.Provider;
using GitReleaseManager.Core.ReleaseNotes;
using GitReleaseManager.Core.Templates;
using Microsoft.Extensions.DependencyInjection;
using NGitLab;
using Octokit;
using Serilog;

Expand Down Expand Up @@ -96,7 +98,6 @@ private static void RegisterServices(BaseSubOptions options)
.AddSingleton<IFileSystem>(fileSystem)
.AddSingleton<IReleaseNotesExporter, ReleaseNotesExporter>()
.AddSingleton<IReleaseNotesBuilder, ReleaseNotesBuilder>()
.AddSingleton<IVcsProvider, GitHubProvider>()
.AddSingleton<IVcsService, VcsService>();

if (options is BaseVcsOptions vcsOptions)
Expand All @@ -106,9 +107,7 @@ private static void RegisterServices(BaseSubOptions options)
throw new Exception("The token option is not defined");
}

var gitHubClient = new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(vcsOptions.Token) };
serviceCollection = serviceCollection
.AddSingleton<IGitHubClient>(gitHubClient);
RegisterVcsProvider(vcsOptions, serviceCollection);
}

serviceCollection = serviceCollection
Expand Down Expand Up @@ -197,5 +196,25 @@ private static Task<int> ExecuteCommand<TOptions>(TOptions options)

private static void LogOptions(BaseSubOptions options)
=> Log.Debug("{@Options}", options);

private static void RegisterVcsProvider(BaseVcsOptions vcsOptions, IServiceCollection serviceCollection)
{
Log.Information("Using {Provider} as VCS Provider", vcsOptions.Provider);
if (vcsOptions.Provider == VcsProvider.GitLab)
{
var gitlabClient = new GitLabClient("https://gitlab.com", vcsOptions.Token);
serviceCollection
.AddSingleton<IVcsProvider, GitLabProvider>()
.AddSingleton<IGitLabClient>(gitlabClient);
}
else
{
// default to Github
var gitHubClient = new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(vcsOptions.Token) };
serviceCollection
.AddSingleton<IGitHubClient>(gitHubClient)
.AddSingleton<IVcsProvider, GitHubProvider>();
}
}
}
}
31 changes: 19 additions & 12 deletions src/GitReleaseManager.Core.Tests/Provider/GitHubProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class GitHubProviderTests
private const bool SKIP_PRERELEASES = false;

private readonly Release _release = new Release();
private readonly Milestone _milestone = new Milestone { PublicNumber = MILESTONE_NUMBER };
private readonly ReleaseAssetUpload _releaseAssetUpload = new ReleaseAssetUpload();
private readonly Octokit.NewLabel _newLabel = new Octokit.NewLabel(LABEL_NAME, "ffffff");
private readonly Octokit.NewRelease _newRelease = new Octokit.NewRelease(TAG_NAME);
Expand Down Expand Up @@ -270,7 +271,7 @@ public async Task Should_Get_Issues_For_Milestone(ItemStateFilter itemStateFilte
_mapper.Map<IEnumerable<Issue>>(Arg.Any<object>())
.Returns(issues);

var result = await _gitHubProvider.GetIssuesAsync(OWNER, REPOSITORY, MILESTONE_NUMBER, itemStateFilter).ConfigureAwait(false);
var result = await _gitHubProvider.GetIssuesAsync(OWNER, REPOSITORY, _milestone, itemStateFilter).ConfigureAwait(false);
result.ShouldBeSameAs(issues);

await _gitHubClient.Issue.Received(1).GetAllForRepository(
Expand All @@ -288,7 +289,7 @@ public async Task Should_Throw_An_Exception_On_Getting_Issues_For_Non_Existent_M
_gitHubClient.Issue.GetAllForRepository(OWNER, REPOSITORY, Arg.Any<RepositoryIssueRequest>(), Arg.Any<ApiOptions>())
.Returns(Task.FromException<IReadOnlyList<Octokit.Issue>>(_exception));

var ex = await Should.ThrowAsync<ApiException>(() => _gitHubProvider.GetIssuesAsync(OWNER, REPOSITORY, 1)).ConfigureAwait(false);
var ex = await Should.ThrowAsync<ApiException>(() => _gitHubProvider.GetIssuesAsync(OWNER, REPOSITORY, _milestone)).ConfigureAwait(false);
ex.Message.ShouldBe(_exception.Message);
ex.InnerException.ShouldBe(_exception);
}
Expand Down Expand Up @@ -581,38 +582,44 @@ public async Task Should_Throw_An_Exception_On_Creating_Release()
[Test]
public async Task Should_Delete_Release()
{
var id = 1;
var releaseId = 1;
var release = new Release();
release.Id = releaseId;

_gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, id)
_gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, releaseId)
.Returns(Task.CompletedTask);

await _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, id).ConfigureAwait(false);
await _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false);

await _gitHubClient.Repository.Release.Received(1).Delete(OWNER, REPOSITORY, id).ConfigureAwait(false);
await _gitHubClient.Repository.Release.Received(1).Delete(OWNER, REPOSITORY, releaseId).ConfigureAwait(false);
}

[Test]
public async Task Should_Throw_An_Exception_On_Deleting_Release_For_Non_Existent_Id()
{
var id = 1;
var releaseId = 1;
var release = new Release();
release.Id = releaseId;

_gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, id)
_gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, releaseId)
.Returns(Task.FromException(_notFoundException));

var ex = await Should.ThrowAsync<NotFoundException>(() => _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, id)).ConfigureAwait(false);
var ex = await Should.ThrowAsync<NotFoundException>(() => _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release)).ConfigureAwait(false);
ex.Message.ShouldBe(_notFoundException.Message);
ex.InnerException.ShouldBeSameAs(_notFoundException);
}

[Test]
public async Task Should_Throw_An_Exception_On_Deleting_Release()
{
var id = 1;
var releaseId = 1;
var release = new Release();
release.Id = releaseId;

_gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, id)
_gitHubClient.Repository.Release.Delete(OWNER, REPOSITORY, releaseId)
.Returns(Task.FromException(_exception));

var ex = await Should.ThrowAsync<ApiException>(() => _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, id)).ConfigureAwait(false);
var ex = await Should.ThrowAsync<ApiException>(() => _gitHubProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release)).ConfigureAwait(false);
ex.Message.ShouldBe(_exception.Message);
ex.InnerException.ShouldBeSameAs(_exception);
}
Expand Down
25 changes: 13 additions & 12 deletions src/GitReleaseManager.Core.Tests/VcsServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public class VcsServiceTests
{
private const string OWNER = "owner";
private const string REPOSITORY = "repository";
private const int MILESTONE_NUMBER = 1;
private const int PUBLIC_MILESTONE_NUMBER = 1;
private const int INTERNAL_MILESTONE_NUMBER = 123;
private const string MILESTONE_TITLE = "0.1.0";
private const string TAG_NAME = "0.1.0";
private const string RELEASE_NOTES = "Release Notes";
Expand Down Expand Up @@ -235,18 +236,18 @@ public async Task Should_Log_An_Warning_When_Labels_Not_Configured()
[Test]
public async Task Should_Close_Milestone()
{
var milestone = new Milestone { Number = MILESTONE_NUMBER };
var milestone = new Milestone { PublicNumber = PUBLIC_MILESTONE_NUMBER, InternalNumber = INTERNAL_MILESTONE_NUMBER };

_vcsProvider.GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Open)
.Returns(Task.FromResult(milestone));

_vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Closed)
_vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.InternalNumber, ItemState.Closed)
.Returns(Task.CompletedTask);

await _vcsService.CloseMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false);

await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Open).ConfigureAwait(false);
await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Closed).ConfigureAwait(false);
await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.InternalNumber, ItemState.Closed).ConfigureAwait(false);
}

[Test]
Expand All @@ -258,25 +259,25 @@ public async Task Should_Log_An_Warning_On_Closing_When_Milestone_Cannot_Be_Foun
await _vcsService.CloseMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false);

await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Open).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, MILESTONE_NUMBER, ItemState.Closed).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, PUBLIC_MILESTONE_NUMBER, ItemState.Closed).ConfigureAwait(false);
_logger.Received(1).Warning(UNABLE_TO_FOUND_MILESTONE_MESSAGE, "open", MILESTONE_TITLE, OWNER, REPOSITORY);
}

[Test]
public async Task Should_Open_Milestone()
{
var milestone = new Milestone { Number = MILESTONE_NUMBER };
var milestone = new Milestone { PublicNumber = PUBLIC_MILESTONE_NUMBER, InternalNumber = INTERNAL_MILESTONE_NUMBER };

_vcsProvider.GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Closed)
.Returns(Task.FromResult(milestone));

_vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Open)
_vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.InternalNumber, ItemState.Open)
.Returns(Task.CompletedTask);

await _vcsService.OpenMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false);

await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Closed).ConfigureAwait(false);
await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Open).ConfigureAwait(false);
await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.InternalNumber, ItemState.Open).ConfigureAwait(false);
_logger.Received(2).Verbose(Arg.Any<string>(), MILESTONE_TITLE, OWNER, REPOSITORY);
}

Expand All @@ -289,7 +290,7 @@ public async Task Should_Log_An_Warning_On_Opening_When_Milestone_Cannot_Be_Foun
await _vcsService.OpenMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false);

await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Closed).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, MILESTONE_NUMBER, ItemState.Open).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, PUBLIC_MILESTONE_NUMBER, ItemState.Open).ConfigureAwait(false);
_logger.Received(1).Warning(UNABLE_TO_FOUND_MILESTONE_MESSAGE, "closed", MILESTONE_TITLE, OWNER, REPOSITORY);
}

Expand Down Expand Up @@ -556,13 +557,13 @@ public async Task Should_Delete_Draft_Release()
_vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME)
.Returns(Task.FromResult(release));

_vcsProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release.Id)
_vcsProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release)
.Returns(Task.CompletedTask);

await _vcsService.DiscardReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);

await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);
await _vcsProvider.Received(1).DeleteReleaseAsync(OWNER, REPOSITORY, release.Id).ConfigureAwait(false);
await _vcsProvider.Received(1).DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false);
}

[Test]
Expand All @@ -576,7 +577,7 @@ public async Task Should_Not_Delete_Published_Release()
await _vcsService.DiscardReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);

await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().DeleteReleaseAsync(OWNER, REPOSITORY, release.Id).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false);
_logger.Received(1).Warning(Arg.Any<string>(), TAG_NAME);
}

Expand Down
8 changes: 8 additions & 0 deletions src/GitReleaseManager.Core/Commands/AddAssetsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public AddAssetsCommand(IVcsService vcsService, ILogger logger)

public async Task<int> ExecuteAsync(AddAssetSubOptions options)
{
var vcsOptions = options as BaseVcsOptions;

if (vcsOptions?.Provider == Model.VcsProvider.GitLab)
{
_logger.Error("The addasset command is currently not supported when targetting GitLab.");
return 1;
}

_logger.Information("Uploading assets");
await _vcsService.AddAssetsAsync(options.RepositoryOwner, options.RepositoryName, options.TagName, options.AssetPaths).ConfigureAwait(false);

Expand Down
10 changes: 9 additions & 1 deletion src/GitReleaseManager.Core/Commands/ExportCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ public ExportCommand(IVcsService vcsService, ILogger logger)

public async Task<int> ExecuteAsync(ExportSubOptions options)
{
_logger.Information("Exporting release {TagName}", options.TagName);
if (string.IsNullOrEmpty(options.TagName))
{
_logger.Information("Exporting all releases");
}
else
{
_logger.Information("Exporting release {TagName}", options.TagName);
}

var releasesContent = await _vcsService.ExportReleasesAsync(options.RepositoryOwner, options.RepositoryName, options.TagName, options.SkipPrereleases).ConfigureAwait(false);

using (var sw = new StreamWriter(File.Open(options.FileOutputPath, FileMode.OpenOrCreate)))
Expand Down
8 changes: 8 additions & 0 deletions src/GitReleaseManager.Core/Commands/LabelCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public LabelCommand(IVcsService vcsService, ILogger logger)

public async Task<int> ExecuteAsync(LabelSubOptions options)
{
var vcsOptions = options as BaseVcsOptions;

if (vcsOptions?.Provider == Model.VcsProvider.GitLab)
{
_logger.Error("The label command is currently not supported when targetting GitLab.");
return 1;
}

_logger.Information("Creating standard labels");
await _vcsService.CreateLabelsAsync(options.RepositoryOwner, options.RepositoryName).ConfigureAwait(false);

Expand Down
26 changes: 24 additions & 2 deletions src/GitReleaseManager.Core/Extensions/MilestoneExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using Octokit;
using Serilog;

namespace GitReleaseManager.Core.Extensions
Expand All @@ -8,7 +7,30 @@ public static class MilestoneExtensions
{
public static readonly ILogger _logger = Log.ForContext(typeof(MilestoneExtensions));

public static Version Version(this Milestone ver)
public static Version Version(this Octokit.Milestone ver)
{
if (ver is null)
{
throw new ArgumentNullException(nameof(ver));
}

var nameWithoutPrerelease = ver.Title.Split('-')[0];
if (nameWithoutPrerelease.StartsWith("v", StringComparison.OrdinalIgnoreCase))
{
_logger.Debug("Removing version prefix from {Name}", ver.Title);
nameWithoutPrerelease = nameWithoutPrerelease.Remove(0, 1);
}

if (!System.Version.TryParse(nameWithoutPrerelease, out Version parsedVersion))
{
_logger.Warning("No valid version was found on {Title}", ver.Title);
return new Version(0, 0);
}

return parsedVersion;
}

public static Version Version(this NGitLab.Models.Milestone ver)
{
if (ver is null)
{
Expand Down
1 change: 1 addition & 0 deletions src/GitReleaseManager.Core/GitReleaseManager.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NGitLab" Version="6.39.0" />
<PackageReference Include="Octokit" Version="7.1.0" />
<PackageReference Include="Scriban" Version="5.7.0" />
<PackageReference Include="seriloganalyzer" Version="0.15.0" />
Expand Down
2 changes: 2 additions & 0 deletions src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public GitHubProfile()
CreateMap<Model.Label, Octokit.NewLabel>().ReverseMap();
CreateMap<Model.Milestone, Octokit.Milestone>();
CreateMap<Octokit.Milestone, Model.Milestone>()
.ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.Number))
.ForMember(dest => dest.InternalNumber, act => act.MapFrom(src => src.Number))
.AfterMap((src, dest) => dest.Version = src.Version());
}
}
Expand Down
35 changes: 35 additions & 0 deletions src/GitReleaseManager.Core/MappingProfiles/GitLabProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace GitReleaseManager.Core.MappingProfiles
{
using System;
using AutoMapper;
using GitReleaseManager.Core.Extensions;

public class GitLabProfile : Profile
{
public GitLabProfile()
{
CreateMap<NGitLab.Models.Milestone, Model.Milestone>()
.ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.Iid))
.ForMember(dest => dest.InternalNumber, act => act.MapFrom(src => src.Id))
.AfterMap((src, dest) => dest.Version = src.Version());
CreateMap<NGitLab.Models.ReleaseInfo, Model.Release>()
.ForMember(dest => dest.Draft, act => act.MapFrom(src => src.ReleasedAt > DateTime.UtcNow))
.ForMember(dest => dest.Body, act => act.MapFrom(src => src.Description))
.ForMember(dest => dest.Assets, act => act.MapFrom(src => src.Assets.Links))
.ReverseMap();
CreateMap<NGitLab.Models.ReleaseLink, Model.ReleaseAsset>().ReverseMap();
CreateMap<NGitLab.Models.Issue, Model.Issue>()
.ForMember(dest => dest.Number, act => act.MapFrom(src => src.IssueId))
.ForMember(dest => dest.HtmlUrl, act => act.MapFrom(src => src.WebUrl))
.ReverseMap();
CreateMap<string, Model.Label>().ForMember(dest => dest.Name, act => act.MapFrom(src => src));
CreateMap<Model.Release, NGitLab.Models.ReleaseCreate>()
.ForMember(dest => dest.Description, act => act.MapFrom(src => src.Body))
.ForMember(dest => dest.Ref, act => act.MapFrom(src => src.TargetCommitish))
.ForMember(dest => dest.Milestones, act => act.MapFrom(src => new string[] { src.TagName }))
.ForMember(dest => dest.ReleasedAt, act => act.MapFrom(src => src.Draft ? DateTime.UtcNow.AddYears(1) : DateTime.UtcNow))
.ForMember(dest => dest.Assets, act => act.Ignore())
.ReverseMap();
}
}
}
Loading

0 comments on commit 6a7a78a

Please sign in to comment.