Skip to content

Commit

Permalink
Injectable GraphQlClient
Browse files Browse the repository at this point in the history
  • Loading branch information
Jericho authored and gep13 committed Feb 20, 2025
1 parent 626abee commit 1e88c70
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 47 deletions.
8 changes: 8 additions & 0 deletions src/GitReleaseManager.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -211,6 +213,12 @@ private static void RegisterVcsProvider(BaseVcsOptions vcsOptions, IServiceColle
// default to Github
serviceCollection
.AddSingleton<IGitHubClient>((_) => new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(vcsOptions.Token) })
.AddSingleton<GraphQL.Client.Abstractions.IGraphQLClient>(_ =>
{
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<IVcsProvider, GitHubProvider>();
}
}
Expand Down
5 changes: 2 additions & 3 deletions src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ public GitHubProfile()
.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.GetProperty("labels")))
.ForMember(dest => dest.LinkedIssues, act => act.MapFrom(src => src.GetProperty("linked_issues")))
.ForMember(dest => dest.Labels, act => act.MapFrom(src => src.GetJsonElement("labels.nodes").EnumerateArray()))
.ReverseMap();

CreateMap<JsonElement, Model.Label>()
Expand All @@ -52,7 +51,7 @@ public GitHubProfile()

CreateMap<JsonElement, Model.User>()
.ForMember(dest => dest.Login, act => act.MapFrom(src => src.GetProperty("login").GetString()))
.ForMember(dest => dest.HtmlUrl, act => act.MapFrom(src => src.GetProperty("resourcePath").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();
}
Expand Down
62 changes: 36 additions & 26 deletions src/GitReleaseManager.Core/Provider/GitHubProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using GitReleaseManager.Core.Extensions;
using GraphQL.Client.Abstractions;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.SystemTextJson;
using Octokit;
using ApiException = GitReleaseManager.Core.Exceptions.ApiException;
using ForbiddenException = GitReleaseManager.Core.Exceptions.ForbiddenException;
Expand Down Expand Up @@ -53,11 +52,19 @@ query ClosingIssuesAndPullRequests($repoName: String!, $repoOwner: String!, $iss
avatarUrl
resourcePath
}
closedByPullRequestsReferences(includeClosedPrs: true, first: $pageSize) {
closedByPullRequestsReferences(userLinkedOnly: false, includeClosedPrs: true, first: $pageSize) {
nodes {
number
id
title
id
number
url
labels(first: 100) {
nodes {
name
color
description
}
}
author {
login
avatarUrl
Expand All @@ -69,10 +76,19 @@ query ClosingIssuesAndPullRequests($repoName: String!, $repoOwner: String!, $iss
pullRequest(number: $issueNumber) {
number
title
closingIssuesReferences(first: $pageSize) {
closingIssuesReferences(userLinkedOnly: false, first: $pageSize) {
nodes {
number
title
id
number
url
labels(first: 100) {
nodes {
name
color
description
}
}
author {
login
avatarUrl
Expand All @@ -92,13 +108,11 @@ public GitHubProvider(IGitHubClient gitHubClient, IMapper mapper)
{
_gitHubClient = gitHubClient;
_mapper = mapper;
}

var graphQLClient = new GraphQLHttpClient(new GraphQLHttpClientOptions { EndPoint = new Uri("https://api.github.com/graphql") }, new SystemTextJsonSerializer());
if (!string.IsNullOrEmpty(_gitHubClient?.Connection?.Credentials?.Password))
{
graphQLClient.HttpClient.DefaultRequestHeaders.Add("Authorization", $"bearer {_gitHubClient.Connection.Credentials.Password}");
}

public GitHubProvider(IGitHubClient gitHubClient, IMapper mapper, IGraphQLClient graphQLClient)
: this(gitHubClient, mapper)
{
_graphQLClient = graphQLClient;
}

Expand Down Expand Up @@ -423,8 +437,9 @@ public string GetIssueType(Issue issue)
return issue.IsPullRequest ? "Pull Request" : "Issue";
}

public async Task<IEnumerable<Issue>> GetLinkedIssuesAsync(string owner, string repository, Issue issue)
public async Task<Issue[]> GetLinkedIssuesAsync(string owner, string repository, Issue issue)
{
ArgumentNullException.ThrowIfNull(_graphQLClient, nameof(_graphQLClient));
ArgumentNullException.ThrowIfNull(issue, nameof(issue));

var request = new GraphQLHttpRequest
Expand All @@ -441,21 +456,16 @@ public async Task<IEnumerable<Issue>> GetLinkedIssuesAsync(string owner, string

var graphQLResponse = await _graphQLClient.SendQueryAsync<dynamic>(request).ConfigureAwait(false);

var rootNode = (JsonElement)graphQLResponse.Data;
var issueNode = rootNode.GetFirstJsonElement(new[] { "repository.issue", "repository.pullRequest" });

if (issueNode.ValueKind == JsonValueKind.Null || issueNode.ValueKind == JsonValueKind.Undefined)
var nodes = ((JsonElement)graphQLResponse.Data).GetFirstJsonElement(new[]
{
throw new NotFoundException($"Unable to find issue/pull request {issue.PublicNumber}");
}

var nodes = issueNode.GetFirstJsonElement(new[] { "closedByPullRequestsReferences.nodes", "closingIssuesReferences.nodes" });
"repository.issue.closedByPullRequestsReferences.nodes", // If issue.PublicNumber represents an issue, retrieve the linked PRs
"repository.pullRequest.closingIssuesReferences.nodes", // If issue.PublicNumber represents a PR, retrieve the linked issues
});

var linkedIssues = new List<Issue>();
foreach (var node in nodes.EnumerateArray())
{
linkedIssues.Add(_mapper.Map<Issue>(node));
}
using var enumerator = nodes.EnumerateArray();
var linkedIssues = enumerator
.Select(element => _mapper.Map<Issue>(element))
.ToArray();

return linkedIssues;
}
Expand Down
6 changes: 3 additions & 3 deletions src/GitReleaseManager.Core/Provider/GitLabProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -396,21 +396,21 @@ public string GetIssueType(Issue issue)
return issue.IsPullRequest ? "Merge Request" : "Issue";
}

public Task<IEnumerable<Issue>> GetLinkedIssuesAsync(string owner, string repository, Issue issue)
public Task<Issue[]> GetLinkedIssuesAsync(string owner, string repository, Issue issue)
{
return ExecuteAsync(() =>
{
if (issue.IsPullRequest)
{
var closes = _gitLabClient.MergeRequests.ClosesIssues(issue.PublicNumber);
var issues = _mapper.Map<IEnumerable<Issue>>(closes);
return Task.FromResult(issues);
return Task.FromResult(issues.ToArray());
}
else
{
var relatedTo = _gitLabClient.Issues.RelatedTo(GetGitLabProjectId(owner, repository), issue.PublicNumber);
var issues = _mapper.Map<IEnumerable<Issue>>(relatedTo);
return Task.FromResult(issues);
return Task.FromResult(issues.ToArray());
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/GitReleaseManager.Core/Provider/IVcsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@ public interface IVcsProvider

string GetIssueType(Issue issue);

Task<IEnumerable<Issue>> GetLinkedIssuesAsync(string owner, string repository, Issue issue);
Task<Issue[]> GetLinkedIssuesAsync(string owner, string repository, Issue issue);
}
}
23 changes: 17 additions & 6 deletions src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,22 @@ public async Task<string> BuildReleaseNotesAsync(string user, string repository,

foreach (var issue in distinctValidIssues)
{
var linkedIssues = await _vcsProvider.GetLinkedIssuesAsync(_user, _repository, issue).ConfigureAwait(false);
issue.LinkedIssues = linkedIssues?.ToList().AsReadOnly() ?? Array.AsReadOnly(Array.Empty<Issue>());
// Linked issues are only necessary for figuring out who contributed to a given issue.
// Therefore, we only need to fetch linked issues if IncludeContributors is enabled.
if (_configuration.Create.IncludeContributors)
{
var linkedIssues = await _vcsProvider.GetLinkedIssuesAsync(_user, _repository, issue).ConfigureAwait(false);
issue.LinkedIssues = Array.AsReadOnly(linkedIssues ?? Array.Empty<Issue>());
}
else
{
issue.LinkedIssues = Array.AsReadOnly(Array.Empty<Issue>());
}
}

var contributors = GetContributors(distinctValidIssues);
var contributors = _configuration.Create.IncludeContributors
? GetContributors(distinctValidIssues)
: Array.Empty<User>();

var milestoneQueryString = _vcsProvider.GetMilestoneQueryString();

Expand All @@ -88,7 +99,7 @@ public async Task<string> BuildReleaseNotesAsync(string user, string repository,
},
Contributors = new
{
Count = contributors.Count,
Count = contributors.Length,
Items = contributors,
},
Commits = new
Expand Down Expand Up @@ -128,7 +139,7 @@ private Dictionary<string, List<Issue>> GetIssuesDict(List<Issue> issues)
return issuesByLabel;
}

private static List<User> GetContributors(IEnumerable<Issue> issues)
private static User[] GetContributors(IEnumerable<Issue> issues)
{
var contributors = issues.Select(i => i.User);
var linkedContributors = issues.SelectMany(i => i.LinkedIssues).Select(i => i.User);
Expand All @@ -137,7 +148,7 @@ private static List<User> GetContributors(IEnumerable<Issue> issues)
.Union(linkedContributors)
.Where(u => u != null)
.DistinctBy(u => u.Login)
.ToList();
.ToArray();

return allContributors;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
using AutoMapper;
using GitReleaseManager.Core;
using GitReleaseManager.Core.Provider;
using GraphQL.Client.Abstractions;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.SystemTextJson;
using NUnit.Framework;
using Octokit;
using Shouldly;
Expand All @@ -22,6 +25,7 @@ public class GitHubProviderIntegrationTests

private GitHubProvider _gitHubProvider;
private IGitHubClient _gitHubClient;
private IGraphQLClient _graphQlClient;
private IMapper _mapper;

private string _token;
Expand All @@ -33,16 +37,25 @@ public class GitHubProviderIntegrationTests
[OneTimeSetUp]
public void OneTimeSetUp()
{
_token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
_token = Environment.GetEnvironmentVariable("GITTOOLS_GITHUB_TOKEN");

if (string.IsNullOrWhiteSpace(_token))
{
Assert.Inconclusive("Unable to locate credentials for accessing GitHub API");
}

_graphQlClient = new GraphQLHttpClient(new GraphQLHttpClientOptions { EndPoint = new Uri("https://api.github.com/graphql") }, new SystemTextJsonSerializer());
((GraphQLHttpClient)_graphQlClient).HttpClient.DefaultRequestHeaders.Add("Authorization", $"bearer {_token}");

_mapper = AutoMapperConfiguration.Configure();
_gitHubClient = new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(_token) };
_gitHubProvider = new GitHubProvider(_gitHubClient, _mapper);
_gitHubProvider = new GitHubProvider(_gitHubClient, _mapper, _graphQlClient);
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
((IDisposable)_graphQlClient)?.Dispose();
}

[Test]
Expand Down Expand Up @@ -108,20 +121,21 @@ public async Task GetLinkedIssues()
// Assert that issue 113 in the GitTools/GitReleaseManager repo is linked to pull request 369
var result0 = await _gitHubProvider.GetLinkedIssuesAsync("GitTools", "GitReleaseManager", new Issue() { PublicNumber = 113 }).ConfigureAwait(false);
Assert.That(result0, Is.Not.Null);
Assert.That(result0.Count(), Is.EqualTo(1));
Assert.That(result0.Length, Is.EqualTo(1));
Assert.That(result0.Count(r => r.PublicNumber == 369), Is.EqualTo(1));

// Assert that pull request 43 in the jericho/_testing repo is linked to issues 107 and 108
// Assert that pull request 43 in the jericho/_testing repo is linked to issues 42, 107 and 108
var result1 = await _gitHubProvider.GetLinkedIssuesAsync("jericho", "_testing", new Issue() { PublicNumber = 43 }).ConfigureAwait(false);
Assert.That(result1, Is.Not.Null);
Assert.That(result1.Count(), Is.EqualTo(2));
Assert.That(result1.Length, Is.EqualTo(3));
Assert.That(result1.Count(r => r.PublicNumber == 42), Is.EqualTo(1));
Assert.That(result1.Count(r => r.PublicNumber == 107), Is.EqualTo(1));
Assert.That(result1.Count(r => r.PublicNumber == 108), Is.EqualTo(1));

// Assert that issue 108 in the jericho/_testing repo is linked to pull request 7, 43 and 109
var result2 = await _gitHubProvider.GetLinkedIssuesAsync("jericho", "_testing", new Issue() { PublicNumber = 108 }).ConfigureAwait(false);
Assert.That(result2, Is.Not.Null);
Assert.That(result2.Count(), Is.EqualTo(3));
Assert.That(result2.Length, Is.EqualTo(3));
Assert.That(result2.Count(r => r.PublicNumber == 7), Is.EqualTo(1));
Assert.That(result2.Count(r => r.PublicNumber == 43), Is.EqualTo(1));
Assert.That(result2.Count(r => r.PublicNumber == 109), Is.EqualTo(1));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
using GitReleaseManager.Core.Provider;
using GitReleaseManager.Core.ReleaseNotes;
using GitReleaseManager.Core.Templates;
using GraphQL.Client.Abstractions;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.SystemTextJson;
using NUnit.Framework;
using Octokit;
using Serilog;
Expand All @@ -20,6 +23,7 @@ namespace GitReleaseManager.IntegrationTests
public class ReleaseNotesBuilderIntegrationTests
{
private IGitHubClient _gitHubClient;
private IGraphQLClient _graphQlClient;
#pragma warning disable NUnit1032 // An IDisposable field/property should be Disposed in a TearDown method
private ILogger _logger;
#pragma warning restore NUnit1032 // An IDisposable field/property should be Disposed in a TearDown method
Expand All @@ -42,13 +46,16 @@ public void Configure()
}

_gitHubClient = new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(_token) };
_graphQlClient = new GraphQLHttpClient(new GraphQLHttpClientOptions { EndPoint = new Uri("https://api.github.com/graphql") }, new SystemTextJsonSerializer());
((GraphQLHttpClient)_graphQlClient).HttpClient.DefaultRequestHeaders.Add("Authorization", $"bearer {_token}");
}

[OneTimeTearDown]
public void TearDown()
{
Log.CloseAndFlush();
(_logger as IDisposable)?.Dispose();
((IDisposable)_graphQlClient)?.Dispose();
}

[Test]
Expand All @@ -75,7 +82,7 @@ public async Task SingleMilestone()
? ReleaseTemplates.CONTRIBUTORS_NAME
: ReleaseTemplates.DEFAULT_NAME;

var vcsProvider = new GitHubProvider(_gitHubClient, _mapper);
var vcsProvider = new GitHubProvider(_gitHubClient, _mapper, _graphQlClient);
var releaseNotesBuilder = new ReleaseNotesBuilder(vcsProvider, _logger, fileSystem, configuration, new TemplateFactory(fileSystem, configuration, TemplateKind.Create));
var result = await releaseNotesBuilder.BuildReleaseNotesAsync("GitTools", "GitReleaseManager", "0.12.0", templatePath).ConfigureAwait(false); // 0.12.0 contains a mix of issues and PRs
Debug.WriteLine(result);
Expand All @@ -91,7 +98,7 @@ public async Task SingleMilestone3()
var currentDirectory = Environment.CurrentDirectory;
var configuration = ConfigurationProvider.Provide(currentDirectory, fileSystem);

var vcsProvider = new GitHubProvider(_gitHubClient, _mapper);
var vcsProvider = new GitHubProvider(_gitHubClient, _mapper, _graphQlClient);
var releaseNotesBuilder = new ReleaseNotesBuilder(vcsProvider, _logger, fileSystem, configuration, new TemplateFactory(fileSystem, configuration, TemplateKind.Create));
var result = await releaseNotesBuilder.BuildReleaseNotesAsync("Chocolatey", "ChocolateyGUI", "0.13.0", ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false);
Debug.WriteLine(result);
Expand Down

0 comments on commit 1e88c70

Please sign in to comment.