Skip to content

Commit 79e1abe

Browse files
committed
Improve GraphQL query
1 parent b4b99a0 commit 79e1abe

File tree

4 files changed

+95
-107
lines changed

4 files changed

+95
-107
lines changed

src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Text.Json;
23
using AutoMapper;
34
using GitReleaseManager.Core.Extensions;
45

@@ -8,6 +9,7 @@ public class GitHubProfile : Profile
89
{
910
public GitHubProfile()
1011
{
12+
// These mappings convert the result of Octokit queries to model classes
1113
CreateMap<Octokit.Issue, Model.Issue>()
1214
.ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.Number))
1315
.ForMember(dest => dest.InternalNumber, act => act.MapFrom(src => src.Id))
@@ -29,6 +31,30 @@ public GitHubProfile()
2931
.ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.Number))
3032
.ForMember(dest => dest.InternalNumber, act => act.MapFrom(src => src.Number))
3133
.AfterMap((src, dest) => dest.Version = src.Version());
34+
35+
// These mappings convert the result of GraphQL queries to model classes
36+
CreateMap<JsonElement, Model.Issue>()
37+
.ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.GetProperty("number").GetInt32()))
38+
.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).
39+
.ForMember(dest => dest.Title, act => act.MapFrom(src => src.GetProperty("title").GetString()))
40+
.ForMember(dest => dest.HtmlUrl, act => act.MapFrom(src => src.GetProperty("url").GetString()))
41+
.ForMember(dest => dest.IsPullRequest, act => act.MapFrom(src => src.GetProperty("url").GetString().Contains("/pull/", StringComparison.OrdinalIgnoreCase)))
42+
.ForMember(dest => dest.User, act => act.MapFrom(src => src.GetProperty("author")))
43+
.ForMember(dest => dest.Labels, act => act.MapFrom(src => src.GetProperty("labels")))
44+
.ForMember(dest => dest.LinkedIssues, act => act.MapFrom(src => src.GetProperty("linked_issues")))
45+
.ReverseMap();
46+
47+
CreateMap<JsonElement, Model.Label>()
48+
.ForMember(dest => dest.Name, act => act.MapFrom(src => src.GetProperty("name").GetString()))
49+
.ForMember(dest => dest.Color, act => act.MapFrom(src => src.GetProperty("color").GetString()))
50+
.ForMember(dest => dest.Description, act => act.MapFrom(src => src.GetProperty("description").GetString()))
51+
.ReverseMap();
52+
53+
CreateMap<JsonElement, Model.User>()
54+
.ForMember(dest => dest.Login, act => act.MapFrom(src => src.GetProperty("login").GetString()))
55+
.ForMember(dest => dest.HtmlUrl, act => act.MapFrom(src => src.GetProperty("resourcePath").GetString()))
56+
.ForMember(dest => dest.AvatarUrl, act => act.MapFrom(src => src.GetProperty("avatarUrl").GetString()))
57+
.ReverseMap();
3258
}
3359
}
3460
}

src/GitReleaseManager.Core/Provider/GitHubProvider.cs

Lines changed: 56 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -33,70 +33,56 @@ public class GitHubProvider : IVcsProvider
3333

3434
// This query fragment will be executed for issues and pull requests
3535
// because we don't know whether issueNumber refers to an issue or a PR
36-
private const string CONNECT_AND_DISCONNECT_EVENTS_GRAPHQL_QUERY_FRAGMENT = @"
37-
{0}(number: $issueNumber) {{
38-
timelineItems(first: $pageSize, itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT]) {{
39-
nodes {{
40-
__typename,
41-
...on ConnectedEvent {{
42-
createdAt,
43-
id,
44-
source {{
45-
__typename,
46-
... on Issue {{
47-
number
48-
}}
49-
... on PullRequest {{
50-
number
51-
}}
52-
}},
53-
subject {{
54-
__typename
55-
... on Issue {{
56-
number
57-
}}
58-
... on PullRequest {{
59-
number
60-
}}
61-
}}
62-
}}
63-
...on DisconnectedEvent {{
64-
createdAt,
65-
id,
66-
source {{
67-
__typename,
68-
... on Issue {{
69-
number
70-
}}
71-
... on PullRequest {{
72-
number,
73-
author {{
74-
avatarUrl,
75-
resourcePath,
76-
}}
77-
}}
78-
}},
79-
subject {{
80-
__typename
81-
... on Issue {{
82-
number
83-
}}
84-
... on PullRequest {{
85-
number
86-
}}
87-
}}
88-
}}
89-
}}
90-
}}
91-
}}";
92-
93-
private const string CONNECT_AND_DISCONNECT_EVENTS_GRAPHQL_QUERY = @"
94-
query ConnectAndDisconnectEvents($repoName: String!, $repoOwner: String!, $issueNumber: Int!, $pageSize: Int!) {{
95-
repository(name: $repoName, owner: $repoOwner) {{
96-
{0},
97-
{1}
98-
}}
99-
}}";
36+
private const string CLOSING_ISSUES_AND_PULLREQUESTS_GRAPHQL_QUERY = @"
37+
query ClosingIssuesAndPullRequests($repoName: String!, $repoOwner: String!, $issueNumber: Int!, $pageSize: Int!) {
38+
repository(name: $repoName, owner: $repoOwner) {
39+
issue(number: $issueNumber) {
40+
title
41+
id
42+
number
43+
url
44+
labels(first: 100) {
45+
nodes {
46+
name
47+
color
48+
description
49+
}
50+
}
51+
author {
52+
login
53+
avatarUrl
54+
resourcePath
55+
}
56+
closedByPullRequestsReferences(includeClosedPrs: true, first: $pageSize) {
57+
nodes {
58+
number
59+
id
60+
title
61+
author {
62+
login
63+
avatarUrl
64+
resourcePath
65+
}
66+
}
67+
}
68+
}
69+
pullRequest(number: $issueNumber) {
70+
number
71+
title
72+
closingIssuesReferences(first: $pageSize) {
73+
nodes {
74+
number
75+
title
76+
author {
77+
login
78+
avatarUrl
79+
resourcePath
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}";
10086

10187
private readonly IGitHubClient _gitHubClient;
10288
private readonly IMapper _mapper;
@@ -439,13 +425,11 @@ public string GetIssueType(Issue issue)
439425

440426
public async Task<IEnumerable<Issue>> GetLinkedIssuesAsync(string owner, string repository, Issue issue)
441427
{
442-
var graphQLQuery = string.Format(CultureInfo.InvariantCulture, CONNECT_AND_DISCONNECT_EVENTS_GRAPHQL_QUERY,
443-
string.Format(CultureInfo.InvariantCulture, CONNECT_AND_DISCONNECT_EVENTS_GRAPHQL_QUERY_FRAGMENT, "issue"),
444-
string.Format(CultureInfo.InvariantCulture, CONNECT_AND_DISCONNECT_EVENTS_GRAPHQL_QUERY_FRAGMENT, "pullRequest"));
428+
ArgumentNullException.ThrowIfNull(issue, nameof(issue));
445429

446430
var request = new GraphQLHttpRequest
447431
{
448-
Query = graphQLQuery.Replace("\r\n", string.Empty),
432+
Query = CLOSING_ISSUES_AND_PULLREQUESTS_GRAPHQL_QUERY.Replace("\r\n", string.Empty, StringComparison.OrdinalIgnoreCase),
449433
Variables = new
450434
{
451435
pageSize = PAGE_SIZE,
@@ -458,50 +442,19 @@ public async Task<IEnumerable<Issue>> GetLinkedIssuesAsync(string owner, string
458442
var graphQLResponse = await _graphQLClient.SendQueryAsync<dynamic>(request).ConfigureAwait(false);
459443

460444
var rootNode = (JsonElement)graphQLResponse.Data;
461-
var issueNode = rootNode.GetJsonElement("repository.issue");
462-
if (issueNode.ValueKind == JsonValueKind.Null || issueNode.ValueKind == JsonValueKind.Undefined)
463-
{
464-
issueNode = rootNode.GetJsonElement("repository.pullRequest");
465-
}
445+
var issueNode = rootNode.GetFirstJsonElement(new[] { "repository.issue", "repository.pullRequest" });
466446

467447
if (issueNode.ValueKind == JsonValueKind.Null || issueNode.ValueKind == JsonValueKind.Undefined)
468448
{
469449
throw new NotFoundException($"Unable to find issue/pull request {issue.PublicNumber}");
470450
}
471451

472-
var nodes = issueNode.GetJsonElement("timelineItems.nodes");
473-
var sortedNodes = nodes.EnumerateArray().OrderBy(n => n.GetJsonElement("createdAt").GetDateTime());
474-
var connectedEvents = sortedNodes.Where(n => n.GetJsonElement("__typename").GetString() == "ConnectedEvent").ToArray();
475-
var disconnectedEvents = sortedNodes.Where(n => n.GetJsonElement("__typename").GetString() == "DisconnectedEvent").ToArray();
476-
477-
if (!connectedEvents.Any())
478-
{
479-
return Enumerable.Empty<Issue>();
480-
}
452+
var nodes = issueNode.GetFirstJsonElement(new[] { "closedByPullRequestsReferences.nodes", "closingIssuesReferences.nodes" });
481453

482454
var linkedIssues = new List<Issue>();
483-
foreach (var connectEvent in connectedEvents)
455+
foreach (var node in nodes.EnumerateArray())
484456
{
485-
var linkedIssueNumber = connectEvent.GetJsonElement("subject.number").GetInt32();
486-
var correspondingDisconnectEvent = disconnectedEvents
487-
.FirstOrDefault(e =>
488-
e.GetJsonElement("subject.number").GetInt32() == linkedIssueNumber &&
489-
e.GetJsonElement("createdAt").GetDateTime() >= connectEvent.GetJsonElement("createdAt").GetDateTime());
490-
491-
if (correspondingDisconnectEvent.ValueKind == JsonValueKind.Null || correspondingDisconnectEvent.ValueKind == JsonValueKind.Undefined)
492-
{
493-
var linkedIssue = await _gitHubClient.Issue.Get(owner, repository, linkedIssueNumber).ConfigureAwait(false);
494-
linkedIssues.Add(_mapper.Map<Issue>(linkedIssue));
495-
}
496-
else if (correspondingDisconnectEvent.GetJsonElement("createdAt").GetDateTime() >= connectEvent.GetJsonElement("createdAt").GetDateTime())
497-
{
498-
continue;
499-
}
500-
else
501-
{
502-
var linkedIssue = await _gitHubClient.Issue.Get(owner, repository, linkedIssueNumber).ConfigureAwait(false);
503-
linkedIssues.Add(_mapper.Map<Issue>(linkedIssue));
504-
}
457+
linkedIssues.Add(_mapper.Map<Issue>(node));
505458
}
506459

507460
return linkedIssues;

src/GitReleaseManager.Core/Provider/GitLabProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ public Task<IEnumerable<Issue>> GetLinkedIssuesAsync(string owner, string reposi
404404
else
405405
{
406406
var closedBy = _gitLabClient.Issues.ClosedBy(GetGitLabProjectId(owner, repository), issue.PublicNumber);
407+
var relatedTo = _gitLabClient.Issues.RelatedTo(GetGitLabProjectId(owner, repository), issue.PublicNumber);
407408
var issues = _mapper.Map<IEnumerable<Issue>>(closedBy);
408409
return Task.FromResult(issues);
409410
}

src/GitReleaseManager.IntegrationTests/GitHubProviderIntegrationTests.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public class GitHubProviderIntegrationTests
3333
[OneTimeSetUp]
3434
public void OneTimeSetUp()
3535
{
36-
_token = Environment.GetEnvironmentVariable("GITTOOLS_GITHUB_TOKEN");
36+
_token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
3737

3838
if (string.IsNullOrWhiteSpace(_token))
3939
{
@@ -105,18 +105,26 @@ public async Task Should_Get_Commits_Count()
105105
[Test]
106106
public async Task GetLinkedIssues()
107107
{
108-
// Assert that pull request 43 is linked to issues 107 and 108
108+
// Assert that issue 113 in the GitTools/GitReleaseManager repo is linked to pull request 369
109+
var result0 = await _gitHubProvider.GetLinkedIssuesAsync("GitTools", "GitReleaseManager", new Issue() { PublicNumber = 113 }).ConfigureAwait(false);
110+
Assert.That(result0, Is.Not.Null);
111+
Assert.That(result0.Count(), Is.EqualTo(1));
112+
Assert.That(result0.Count(r => r.PublicNumber == 369), Is.EqualTo(1));
113+
114+
// Assert that pull request 43 in the jericho/_testing repo is linked to issues 107 and 108
109115
var result1 = await _gitHubProvider.GetLinkedIssuesAsync("jericho", "_testing", new Issue() { PublicNumber = 43 }).ConfigureAwait(false);
110116
Assert.That(result1, Is.Not.Null);
111117
Assert.That(result1.Count(), Is.EqualTo(2));
112118
Assert.That(result1.Count(r => r.PublicNumber == 107), Is.EqualTo(1));
113119
Assert.That(result1.Count(r => r.PublicNumber == 108), Is.EqualTo(1));
114120

115-
// Assert that issue 108 is linked to pull request 43
121+
// Assert that issue 108 in the jericho/_testing repo is linked to pull request 7, 43 and 109
116122
var result2 = await _gitHubProvider.GetLinkedIssuesAsync("jericho", "_testing", new Issue() { PublicNumber = 108 }).ConfigureAwait(false);
117123
Assert.That(result2, Is.Not.Null);
118-
Assert.That(result2.Count(), Is.EqualTo(1));
124+
Assert.That(result2.Count(), Is.EqualTo(3));
125+
Assert.That(result2.Count(r => r.PublicNumber == 7), Is.EqualTo(1));
119126
Assert.That(result2.Count(r => r.PublicNumber == 43), Is.EqualTo(1));
127+
Assert.That(result2.Count(r => r.PublicNumber == 109), Is.EqualTo(1));
120128
}
121129
}
122130
}

0 commit comments

Comments
 (0)