Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 8e4a5a9

Browse files
committed
Add downlevel support for GitHub Enterprise
Only use reviewThreads/isResolved on github.com where they are guaranteed to be available.
1 parent 085bfa9 commit 8e4a5a9

File tree

1 file changed

+253
-38
lines changed

1 file changed

+253
-38
lines changed

src/GitHub.InlineReviews/Services/PullRequestSessionService.cs

Lines changed: 253 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)