22using System . Collections . Generic ;
33using System . Globalization ;
44using System . Linq ;
5+ using System . Text . Json ;
56using System . Threading . Tasks ;
67using AutoMapper ;
8+ using GitReleaseManager . Core . Extensions ;
9+ using GraphQL . Client . Abstractions ;
10+ using GraphQL . Client . Http ;
11+ using GraphQL . Client . Serializer . SystemTextJson ;
712using Octokit ;
813using ApiException = GitReleaseManager . Core . Exceptions . ApiException ;
914using ForbiddenException = GitReleaseManager . Core . Exceptions . ForbiddenException ;
@@ -26,13 +31,62 @@ public class GitHubProvider : IVcsProvider
2631 private const int PAGE_SIZE = 100 ;
2732 private const string NOT_FOUND_MESSGAE = "NotFound" ;
2833
34+ // This query fragment will be executed for issues and pull requests
35+ // 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+ }}
66+ }}
67+ }}
68+ }}" ;
69+
70+ private const string CONNECT_AND_DISCONNECT_EVENTS_GRAPHQL_QUERY = @"
71+ query ConnectAndDisconnectEvents($repoName: String!, $repoOwner: String!, $issueNumber: Int!, $pageSize: Int!) {{
72+ repository(name: $repoName, owner: $repoOwner) {{
73+ {0},
74+ {1}
75+ }}
76+ }}" ;
77+
2978 private readonly IGitHubClient _gitHubClient ;
3079 private readonly IMapper _mapper ;
80+ private readonly IGraphQLClient _graphQLClient ;
3181
3282 public GitHubProvider ( IGitHubClient gitHubClient , IMapper mapper )
3383 {
3484 _gitHubClient = gitHubClient ;
3585 _mapper = mapper ;
86+
87+ var graphQLClient = new GraphQLHttpClient ( new GraphQLHttpClientOptions { EndPoint = new Uri ( "https://api.github.com/graphql" ) } , new SystemTextJsonSerializer ( ) ) ;
88+ graphQLClient . HttpClient . DefaultRequestHeaders . Add ( "Authorization" , $ "bearer { _gitHubClient . Connection . Credentials . Password } ") ;
89+ _graphQLClient = graphQLClient ;
3690 }
3791
3892 public Task DeleteAssetAsync ( string owner , string repository , ReleaseAsset asset )
@@ -356,6 +410,72 @@ public string GetIssueType(Issue issue)
356410 return issue . IsPullRequest ? "Pull Request" : "Issue" ;
357411 }
358412
413+ public async Task < Issue > GetLinkedIssueAsync ( string owner , string repository , int issueNumber )
414+ {
415+ var graphQLQuery = string . Format ( CultureInfo . InvariantCulture , CONNECT_AND_DISCONNECT_EVENTS_GRAPHQL_QUERY ,
416+ string . Format ( CultureInfo . InvariantCulture , CONNECT_AND_DISCONNECT_EVENTS_GRAPHQL_QUERY_FRAGMENT , "issue" ) ,
417+ string . Format ( CultureInfo . InvariantCulture , CONNECT_AND_DISCONNECT_EVENTS_GRAPHQL_QUERY_FRAGMENT , "pullRequest" ) ) ;
418+
419+ var request = new GraphQLHttpRequest
420+ {
421+ Query = graphQLQuery . Replace ( "\r \n " , string . Empty ) ,
422+ Variables = new
423+ {
424+ pageSize = PAGE_SIZE ,
425+ repoName = repository ,
426+ repoOwner = owner ,
427+ issueNumber = issueNumber ,
428+ } ,
429+ } ;
430+
431+ var graphQLResponse = await _graphQLClient . SendQueryAsync < dynamic > ( request ) . ConfigureAwait ( false ) ;
432+
433+ var rootNode = ( JsonElement ) graphQLResponse . Data ;
434+ var issueNode = rootNode . GetJsonElement ( "repository.issue" ) ;
435+ if ( issueNode . ValueKind == JsonValueKind . Null || issueNode . ValueKind == JsonValueKind . Undefined )
436+ {
437+ issueNode = rootNode . GetJsonElement ( "repository.pullRequest" ) ;
438+ }
439+
440+ if ( issueNode . ValueKind == JsonValueKind . Null || issueNode . ValueKind == JsonValueKind . Undefined )
441+ {
442+ throw new NotFoundException ( $ "Unable to find issue/pull request { issueNumber } ") ;
443+ }
444+
445+ var nodes = issueNode . GetJsonElement ( "timelineItems.nodes" ) ;
446+ var sortedNodes = nodes . EnumerateArray ( ) . OrderByDescending ( n => n . GetJsonElement ( "createdAt" ) . GetDateTime ( ) ) ;
447+ var mostRecentConnectedEvent = sortedNodes . FirstOrDefault ( n => n . GetJsonElement ( "__typename" ) . GetString ( ) == "ConnectedEvent" ) ;
448+ var mostRecentDisconnectedEvent = sortedNodes . FirstOrDefault ( n => n . GetJsonElement ( "__typename" ) . GetString ( ) == "DisconnectedEvent" ) ;
449+
450+ // Make sure we found an event that indicates that an issue/PR was linked to this issue/PR
451+ if ( mostRecentConnectedEvent . ValueKind == JsonValueKind . Null || mostRecentConnectedEvent . ValueKind == JsonValueKind . Undefined )
452+ {
453+ return null ;
454+ }
455+
456+ // We found an event indicating that an issue was linked. Make sure it wasn't un-linked
457+ else if ( mostRecentDisconnectedEvent . ValueKind == JsonValueKind . Null || mostRecentDisconnectedEvent . ValueKind == JsonValueKind . Undefined )
458+ {
459+ var linkedIssueNumber = mostRecentConnectedEvent . GetJsonElement ( "subject.number" ) . GetInt32 ( ) ;
460+ var issue = await _gitHubClient . Issue . Get ( owner , repository , linkedIssueNumber ) . ConfigureAwait ( false ) ;
461+ return _mapper . Map < Issue > ( issue ) ;
462+ }
463+
464+ // We found a linked issue and a disconnection event. Check which one is the most recent
465+ else if ( mostRecentDisconnectedEvent . GetJsonElement ( "createdAt" ) . GetDateTime ( ) >= mostRecentConnectedEvent . GetJsonElement ( "createdAt" ) . GetDateTime ( ) )
466+ {
467+ return null ;
468+ }
469+
470+ // We found an event indicating that an issue was linked and we determined that it is more recent than any of the "un-link" events
471+ else
472+ {
473+ var linkedIssueNumber = mostRecentConnectedEvent . GetJsonElement ( "subject.number" ) . GetInt32 ( ) ;
474+ var issue = await _gitHubClient . Issue . Get ( owner , repository , linkedIssueNumber ) . ConfigureAwait ( false ) ;
475+ return _mapper . Map < Issue > ( issue ) ;
476+ }
477+ }
478+
359479 private async Task ExecuteAsync ( Func < Task > action )
360480 {
361481 try
0 commit comments