@@ -39,7 +39,8 @@ namespace GitHub.InlineReviews.Services
3939 public class PullRequestSessionService : IPullRequestSessionService
4040 {
4141 static readonly ILogger log = LogManager . ForContext < PullRequestSessionService > ( ) ;
42- static ICompiledQuery < PullRequestDetailModel > readPullRequest ;
42+ static ICompiledQuery < PullRequestDetailModel > readPullRequestWithResolved ;
43+ static ICompiledQuery < PullRequestDetailModel > readPullRequestWithoutResolved ;
4344 static ICompiledQuery < IEnumerable < LastCommitAdapter > > readCommitStatuses ;
4445 static ICompiledQuery < IEnumerable < LastCommitAdapter > > readCommitStatusesEnterprise ;
4546 static ICompiledQuery < ActorModel > readViewer ;
@@ -289,11 +290,26 @@ public async Task<byte[]> ReadFileAsync(string path)
289290 return null ;
290291 }
291292
292- public virtual async Task < PullRequestDetailModel > ReadPullRequestDetail ( HostAddress address , string owner , string name , int number )
293+ public virtual Task < PullRequestDetailModel > ReadPullRequestDetail ( HostAddress address , string owner , string name , int number )
293294 {
294- if ( readPullRequest == null )
295+ // The reviewThreads/isResolved field is only guaranteed to be available on github.com
296+ if ( address . IsGitHubDotCom ( ) )
297+ {
298+ return ReadPullRequestDetailWithResolved ( address , owner , name , number ) ;
299+ }
300+ else
295301 {
296- readPullRequest = new Query ( )
302+ return ReadPullRequestDetailWithoutResolved ( address , owner , name , number ) ;
303+ }
304+ }
305+
306+ async Task < PullRequestDetailModel > ReadPullRequestDetailWithResolved (
307+ HostAddress address , string owner , string name , int number )
308+ {
309+
310+ if ( readPullRequestWithResolved == null )
311+ {
312+ readPullRequestWithResolved = new Query ( )
297313 . Repository ( owner : Var ( nameof ( owner ) ) , name : Var ( nameof ( name ) ) )
298314 . PullRequest ( number : Var ( nameof ( number ) ) )
299315 . Select ( pr => new PullRequestDetailModel
@@ -404,7 +420,7 @@ public virtual async Task<PullRequestDetailModel> ReadPullRequestDetail(HostAddr
404420 } ;
405421
406422 var connection = await graphqlFactory . CreateConnection ( address ) ;
407- var result = await connection . Run ( readPullRequest , vars ) ;
423+ var result = await connection . Run ( readPullRequestWithResolved , vars ) ;
408424
409425 var apiClient = await apiClientFactory . Create ( address ) ;
410426
@@ -436,7 +452,238 @@ public virtual async Task<PullRequestDetailModel> ReadPullRequestDetail(HostAddr
436452 Status = ( PullRequestFileStatus ) Enum . Parse ( typeof ( PullRequestFileStatus ) , file . Status , true ) ,
437453 } ) . ToList ( ) ;
438454
439- BuildPullRequestThreads ( result ) ;
455+ foreach ( var thread in result . Threads )
456+ {
457+ if ( thread . Comments . Count > 0 && thread . Comments [ 0 ] is CommentAdapter adapter )
458+ {
459+ thread . CommitSha = adapter . CommitSha ;
460+ thread . DiffHunk = adapter . DiffHunk ;
461+ thread . Id = adapter . Id ;
462+ thread . IsOutdated = adapter . Position == null ;
463+ thread . OriginalCommitSha = adapter . OriginalCommitId ;
464+ thread . OriginalPosition = adapter . OriginalPosition ;
465+ thread . Path = adapter . Path ;
466+ thread . Position = adapter . Position ;
467+
468+ foreach ( var comment in thread . Comments )
469+ {
470+ comment . Thread = thread ;
471+ }
472+ }
473+ }
474+
475+ foreach ( var review in result . Reviews )
476+ {
477+ review . Comments = result . Threads
478+ . SelectMany ( t => t . Comments )
479+ . Cast < CommentAdapter > ( )
480+ . Where ( c => c . PullRequestReviewId == review . Id )
481+ . ToList ( ) ;
482+ }
483+
484+ return result ;
485+ }
486+
487+ async Task < PullRequestDetailModel > ReadPullRequestDetailWithoutResolved (
488+ HostAddress address , string owner , string name , int number )
489+ {
490+ if ( readPullRequestWithoutResolved == null )
491+ {
492+ readPullRequestWithoutResolved = new Query ( )
493+ . Repository ( owner : Var ( nameof ( owner ) ) , name : Var ( nameof ( name ) ) )
494+ . PullRequest ( number : Var ( nameof ( number ) ) )
495+ . Select ( pr => new PullRequestDetailModel
496+ {
497+ Id = pr . Id . Value ,
498+ Number = pr . Number ,
499+ Author = new ActorModel
500+ {
501+ Login = pr . Author . Login ,
502+ AvatarUrl = pr . Author . AvatarUrl ( null ) ,
503+ } ,
504+ Title = pr . Title ,
505+ Body = pr . Body ,
506+ BaseRefSha = pr . BaseRefOid ,
507+ BaseRefName = pr . BaseRefName ,
508+ BaseRepositoryOwner = pr . Repository . Owner . Login ,
509+ HeadRefName = pr . HeadRefName ,
510+ HeadRefSha = pr . HeadRefOid ,
511+ HeadRepositoryOwner = pr . HeadRepositoryOwner != null ? pr . HeadRepositoryOwner . Login : null ,
512+ State = pr . State . FromGraphQl ( ) ,
513+ UpdatedAt = pr . UpdatedAt ,
514+ CommentCount = pr . Comments ( 0 , null , null , null ) . TotalCount ,
515+ Comments = pr . Comments ( null , null , null , null ) . AllPages ( ) . Select ( comment => new CommentModel
516+ {
517+ Id = comment . Id . Value ,
518+ Author = new ActorModel
519+ {
520+ Login = comment . Author . Login ,
521+ AvatarUrl = comment . Author . AvatarUrl ( null ) ,
522+ } ,
523+ Body = comment . Body ,
524+ CreatedAt = comment . CreatedAt ,
525+ DatabaseId = comment . DatabaseId . Value ,
526+ Url = comment . Url ,
527+ } ) . ToList ( ) ,
528+ Reviews = pr . Reviews ( null , null , null , null , null , null ) . AllPages ( ) . Select ( review => new PullRequestReviewModel
529+ {
530+ Id = review . Id . Value ,
531+ Body = review . Body ,
532+ CommitId = review . Commit . Oid ,
533+ State = review . State . FromGraphQl ( ) ,
534+ SubmittedAt = review . SubmittedAt ,
535+ Author = new ActorModel
536+ {
537+ Login = review . Author . Login ,
538+ AvatarUrl = review . Author . AvatarUrl ( null ) ,
539+ } ,
540+ Comments = review . Comments ( null , null , null , null ) . AllPages ( ) . Select ( comment => new CommentAdapter
541+ {
542+ Id = comment . Id . Value ,
543+ PullRequestId = comment . PullRequest . Number ,
544+ DatabaseId = comment . DatabaseId . Value ,
545+ Author = new ActorModel
546+ {
547+ Login = comment . Author . Login ,
548+ AvatarUrl = comment . Author . AvatarUrl ( null ) ,
549+ } ,
550+ Body = comment . Body ,
551+ Path = comment . Path ,
552+ CommitSha = comment . Commit . Oid ,
553+ DiffHunk = comment . DiffHunk ,
554+ Position = comment . Position ,
555+ OriginalPosition = comment . OriginalPosition ,
556+ OriginalCommitId = comment . OriginalCommit . Oid ,
557+ ReplyTo = comment . ReplyTo != null ? comment . ReplyTo . Id . Value : null ,
558+ CreatedAt = comment . CreatedAt ,
559+ Url = comment . Url ,
560+ } ) . ToList ( ) ,
561+ } ) . ToList ( ) ,
562+ Timeline = pr . Timeline ( null , null , null , null , null ) . AllPages ( ) . Select ( item => item . Switch < object > ( when =>
563+ when . Commit ( commit => new CommitModel
564+ {
565+ AbbreviatedOid = commit . AbbreviatedOid ,
566+ // TODO: commit.Author.User can be null
567+ Author = new ActorModel
568+ {
569+ Login = commit . Author . User . Login ,
570+ AvatarUrl = commit . Author . User . AvatarUrl ( null ) ,
571+ } ,
572+ MessageHeadline = commit . MessageHeadline ,
573+ Oid = commit . Oid ,
574+ } ) . IssueComment ( comment => new CommentModel
575+ {
576+ Author = new ActorModel
577+ {
578+ Login = comment . Author . Login ,
579+ AvatarUrl = comment . Author . AvatarUrl ( null ) ,
580+ } ,
581+ Body = comment . Body ,
582+ CreatedAt = comment . CreatedAt ,
583+ DatabaseId = comment . DatabaseId . Value ,
584+ Id = comment . Id . Value ,
585+ Url = comment . Url ,
586+ } ) ) ) . ToList ( )
587+ } ) . Compile ( ) ;
588+ }
589+
590+ var vars = new Dictionary < string , object >
591+ {
592+ { nameof ( owner ) , owner } ,
593+ { nameof ( name ) , name } ,
594+ { nameof ( number ) , number } ,
595+ } ;
596+
597+ var connection = await graphqlFactory . CreateConnection ( address ) ;
598+ var result = await connection . Run ( readPullRequestWithoutResolved , vars ) ;
599+
600+ var apiClient = await apiClientFactory . Create ( address ) ;
601+
602+ var files = await log . TimeAsync ( nameof ( apiClient . GetPullRequestFiles ) ,
603+ async ( ) => await apiClient . GetPullRequestFiles ( owner , name , number ) . ToList ( ) ) ;
604+
605+ var lastCommitModel = await log . TimeAsync ( nameof ( GetPullRequestLastCommitAdapter ) ,
606+ ( ) => GetPullRequestLastCommitAdapter ( address , owner , name , number ) ) ;
607+
608+ result . Statuses = ( IReadOnlyList < StatusModel > ) lastCommitModel . Statuses ?? Array . Empty < StatusModel > ( ) ;
609+
610+ if ( lastCommitModel . CheckSuites == null )
611+ {
612+ result . CheckSuites = Array . Empty < CheckSuiteModel > ( ) ;
613+ }
614+ else
615+ {
616+ result . CheckSuites = lastCommitModel . CheckSuites ;
617+ foreach ( var checkSuite in result . CheckSuites )
618+ {
619+ checkSuite . HeadSha = lastCommitModel . HeadSha ;
620+ }
621+ }
622+
623+ result . ChangedFiles = files . Select ( file => new PullRequestFileModel
624+ {
625+ FileName = file . FileName ,
626+ Sha = file . Sha ,
627+ Status = ( PullRequestFileStatus ) Enum . Parse ( typeof ( PullRequestFileStatus ) , file . Status , true ) ,
628+ } ) . ToList ( ) ;
629+
630+ // Build pull request threads
631+ var commentsByReplyId = new Dictionary < string , List < CommentAdapter > > ( ) ;
632+
633+ // Get all comments that are not replies.
634+ foreach ( CommentAdapter comment in result . Reviews . SelectMany ( x => x . Comments ) )
635+ {
636+ if ( comment . ReplyTo == null )
637+ {
638+ commentsByReplyId . Add ( comment . Id , new List < CommentAdapter > { comment } ) ;
639+ }
640+ }
641+
642+ // Get the comments that are replies and place them into the relevant list.
643+ foreach ( CommentAdapter comment in result . Reviews . SelectMany ( x => x . Comments ) . OrderBy ( x => x . CreatedAt ) )
644+ {
645+ if ( comment . ReplyTo != null )
646+ {
647+ List < CommentAdapter > thread = null ;
648+
649+ if ( commentsByReplyId . TryGetValue ( comment . ReplyTo , out thread ) )
650+ {
651+ thread . Add ( comment ) ;
652+ }
653+ }
654+ }
655+
656+ // Build a collection of threads for the information collected above.
657+ var threads = new List < PullRequestReviewThreadModel > ( ) ;
658+
659+ foreach ( var threadSource in commentsByReplyId )
660+ {
661+ var adapter = threadSource . Value [ 0 ] ;
662+
663+ var thread = new PullRequestReviewThreadModel
664+ {
665+ Comments = threadSource . Value ,
666+ CommitSha = adapter . CommitSha ,
667+ DiffHunk = adapter . DiffHunk ,
668+ Id = adapter . Id ,
669+ IsOutdated = adapter . Position == null ,
670+ OriginalCommitSha = adapter . OriginalCommitId ,
671+ OriginalPosition = adapter . OriginalPosition ,
672+ Path = adapter . Path ,
673+ Position = adapter . Position ,
674+ } ;
675+
676+ // Set a reference to the thread in the comment.
677+ foreach ( var comment in threadSource . Value )
678+ {
679+ comment . Thread = thread ;
680+ }
681+
682+ threads . Add ( thread ) ;
683+ }
684+
685+ result . Threads = threads ;
686+
440687 return result ;
441688 }
442689
@@ -917,38 +1164,6 @@ async Task<LastCommitAdapter> GetPullRequestLastCommitAdapter(HostAddress addres
9171164 return result . First ( ) ;
9181165 }
9191166
920- static void BuildPullRequestThreads ( PullRequestDetailModel model )
921- {
922- foreach ( var thread in model . Threads )
923- {
924- if ( thread . Comments . Count > 0 && thread . Comments [ 0 ] is CommentAdapter adapter )
925- {
926- thread . CommitSha = adapter . CommitSha ;
927- thread . DiffHunk = adapter . DiffHunk ;
928- thread . Id = adapter . Id ;
929- thread . IsOutdated = adapter . Position == null ;
930- thread . OriginalCommitSha = adapter . OriginalCommitId ;
931- thread . OriginalPosition = adapter . OriginalPosition ;
932- thread . Path = adapter . Path ;
933- thread . Position = adapter . Position ;
934-
935- foreach ( var comment in thread . Comments )
936- {
937- comment . Thread = thread ;
938- }
939- }
940- }
941-
942- foreach ( var review in model . Reviews )
943- {
944- review . Comments = model . Threads
945- . SelectMany ( t => t . Comments )
946- . Cast < CommentAdapter > ( )
947- . Where ( c => c . PullRequestReviewId == review . Id )
948- . ToList ( ) ;
949- }
950- }
951-
9521167 static Octokit . GraphQL . Model . PullRequestReviewEvent ToGraphQl ( Octokit . PullRequestReviewEvent e )
9531168 {
9541169 switch ( e )
0 commit comments