diff --git a/src/GitReleaseManager.Cli/Program.cs b/src/GitReleaseManager.Cli/Program.cs index dc44e89c..96280e1c 100644 --- a/src/GitReleaseManager.Cli/Program.cs +++ b/src/GitReleaseManager.Cli/Program.cs @@ -13,6 +13,8 @@ using GitReleaseManager.Core.Provider; using GitReleaseManager.Core.ReleaseNotes; using GitReleaseManager.Core.Templates; +using GraphQL.Client.Http; +using GraphQL.Client.Serializer.SystemTextJson; using Microsoft.Extensions.DependencyInjection; using NGitLab; using Octokit; @@ -211,6 +213,12 @@ private static void RegisterVcsProvider(BaseVcsOptions vcsOptions, IServiceColle // default to Github serviceCollection .AddSingleton((_) => new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(vcsOptions.Token) }) + .AddSingleton(_ => + { + var client = new GraphQLHttpClient(new GraphQLHttpClientOptions { EndPoint = new Uri("https://api.github.com/graphql") }, new SystemTextJsonSerializer()); + client.HttpClient.DefaultRequestHeaders.Add("Authorization", $"bearer {vcsOptions.Token}"); + return client; + }) .AddSingleton(); } } diff --git a/src/GitReleaseManager.Core/Configuration/Config.cs b/src/GitReleaseManager.Core/Configuration/Config.cs index 15083cc2..69e237f4 100644 --- a/src/GitReleaseManager.Core/Configuration/Config.cs +++ b/src/GitReleaseManager.Core/Configuration/Config.cs @@ -27,6 +27,7 @@ public Config() ShaSectionHeading = "SHA256 Hashes of the release artifacts", ShaSectionLineFormat = "- `{1}\t{0}`", AllowUpdateToPublishedRelease = false, + IncludeContributors = false, }; Export = new ExportConfig diff --git a/src/GitReleaseManager.Core/Configuration/CreateConfig.cs b/src/GitReleaseManager.Core/Configuration/CreateConfig.cs index 512e4a28..00baed7d 100644 --- a/src/GitReleaseManager.Core/Configuration/CreateConfig.cs +++ b/src/GitReleaseManager.Core/Configuration/CreateConfig.cs @@ -34,5 +34,8 @@ public class CreateConfig [YamlMember(Alias = "allow-update-to-published")] public bool AllowUpdateToPublishedRelease { get; set; } + + [YamlMember(Alias = "include-contributors")] + public bool IncludeContributors { get; set; } } } \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Extensions/JsonExtensions.cs b/src/GitReleaseManager.Core/Extensions/JsonExtensions.cs new file mode 100644 index 00000000..d8920c65 --- /dev/null +++ b/src/GitReleaseManager.Core/Extensions/JsonExtensions.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +namespace GitReleaseManager.Core.Extensions +{ + internal static class JsonExtensions + { + /// + /// Get a JsonElement from a path. Each level in the path is seperated by a dot. + /// + /// The parent Json element. + /// The path of the desired child element. + /// The child element. + public static JsonElement GetJsonElement(this JsonElement jsonElement, string path) + { + if (jsonElement.ValueKind is JsonValueKind.Null || jsonElement.ValueKind is JsonValueKind.Undefined) + { + return default(JsonElement); + } + + string[] segments = path.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var segment in segments) + { + if (int.TryParse(segment, out var index) && jsonElement.ValueKind == JsonValueKind.Array) + { + jsonElement = jsonElement.EnumerateArray().ElementAtOrDefault(index); + if (jsonElement.ValueKind is JsonValueKind.Null || jsonElement.ValueKind is JsonValueKind.Undefined) + { + return default(JsonElement); + } + + continue; + } + + jsonElement = jsonElement.TryGetProperty(segment, out var value) ? value : default; + + if (jsonElement.ValueKind is JsonValueKind.Null || jsonElement.ValueKind is JsonValueKind.Undefined) + { + return default(JsonElement); + } + } + + return jsonElement; + } + + /// + /// Get the first JsonElement matching a path from the provided list of paths. + /// + /// The parent Json element. + /// The path of the desired child element. + /// The child element. + public static JsonElement GetFirstJsonElement(this JsonElement jsonElement, IEnumerable paths) + { + if (jsonElement.ValueKind is JsonValueKind.Null || jsonElement.ValueKind is JsonValueKind.Undefined) + { + return default(JsonElement); + } + + var element = default(JsonElement); + + foreach (var path in paths) + { + element = jsonElement.GetJsonElement(path); + + if (element.ValueKind is JsonValueKind.Null || element.ValueKind is JsonValueKind.Undefined) + { + continue; + } + + break; + } + + return element; + } + + public static string GetJsonElementValue(this JsonElement jsonElement) => jsonElement.ValueKind != JsonValueKind.Null && + jsonElement.ValueKind != JsonValueKind.Undefined + ? jsonElement.ToString() + : default; + } +} diff --git a/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj b/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj index bca953b3..a0401a2e 100644 --- a/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj +++ b/src/GitReleaseManager.Core/GitReleaseManager.Core.csproj @@ -19,6 +19,8 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs b/src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs index dff999dc..11e90526 100644 --- a/src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs +++ b/src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json; using AutoMapper; using GitReleaseManager.Core.Extensions; @@ -8,10 +9,11 @@ public class GitHubProfile : Profile { public GitHubProfile() { + // These mappings convert the result of Octokit queries to model classes CreateMap() .ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.Number)) .ForMember(dest => dest.InternalNumber, act => act.MapFrom(src => src.Id)) - .ForMember(dest => dest.IsPullRequest, act => act.MapFrom(src => src.HtmlUrl.IndexOf("/pull/", StringComparison.OrdinalIgnoreCase) >= 0)) + .ForMember(dest => dest.IsPullRequest, act => act.MapFrom(src => src.HtmlUrl.Contains("/pull/", StringComparison.OrdinalIgnoreCase))) .ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); @@ -23,11 +25,35 @@ public GitHubProfile() CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); CreateMap(); CreateMap() .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()); + + // These mappings convert the result of GraphQL queries to model classes + CreateMap() + .ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.GetProperty("number").GetInt32())) + .ForMember(dest => dest.InternalNumber, act => act.MapFrom(src => -1)) // Not available in graphQL (there's a "id" property but it contains a string which represents the Node ID of the object). + .ForMember(dest => dest.Title, act => act.MapFrom(src => src.GetProperty("title").GetString())) + .ForMember(dest => dest.HtmlUrl, act => act.MapFrom(src => src.GetProperty("url").GetString())) + .ForMember(dest => dest.IsPullRequest, act => act.MapFrom(src => src.GetProperty("url").GetString().Contains("/pull/", StringComparison.OrdinalIgnoreCase))) + .ForMember(dest => dest.User, act => act.MapFrom(src => src.GetProperty("author"))) + .ForMember(dest => dest.Labels, act => act.MapFrom(src => src.GetJsonElement("labels.nodes").EnumerateArray())) + .ReverseMap(); + + CreateMap() + .ForMember(dest => dest.Name, act => act.MapFrom(src => src.GetProperty("name").GetString())) + .ForMember(dest => dest.Color, act => act.MapFrom(src => src.GetProperty("color").GetString())) + .ForMember(dest => dest.Description, act => act.MapFrom(src => src.GetProperty("description").GetString())) + .ReverseMap(); + + CreateMap() + .ForMember(dest => dest.Login, act => act.MapFrom(src => src.GetProperty("login").GetString())) + .ForMember(dest => dest.HtmlUrl, act => act.MapFrom(src => $"https://github.com{src.GetProperty("resourcePath").GetString()}")) // The resourcePath contains a value similar to "/jericho". That's why we must manually prepend "https://github.com + .ForMember(dest => dest.AvatarUrl, act => act.MapFrom(src => src.GetProperty("avatarUrl").GetString())) + .ReverseMap(); } } } \ No newline at end of file diff --git a/src/GitReleaseManager.Core/Model/Issue.cs b/src/GitReleaseManager.Core/Model/Issue.cs index 222d8eb9..5ad73c7c 100644 --- a/src/GitReleaseManager.Core/Model/Issue.cs +++ b/src/GitReleaseManager.Core/Model/Issue.cs @@ -15,5 +15,9 @@ public sealed class Issue public IReadOnlyList