diff --git a/lib/Octokit.GraphQL.0.1.1-beta.nupkg b/lib/Octokit.GraphQL.0.1.1-beta.nupkg new file mode 100644 index 0000000000..ac26783672 Binary files /dev/null and b/lib/Octokit.GraphQL.0.1.1-beta.nupkg differ diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 7dd7ceaad4..28f1263421 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -50,11 +50,11 @@ ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - - ..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.dll + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll - - ..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll ..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll diff --git a/src/GitHub.Api/GraphQLKeychainCredentialStore.cs b/src/GitHub.Api/GraphQLKeychainCredentialStore.cs index 0098d15983..4ad122fb88 100644 --- a/src/GitHub.Api/GraphQLKeychainCredentialStore.cs +++ b/src/GitHub.Api/GraphQLKeychainCredentialStore.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using GitHub.Extensions; using GitHub.Primitives; @@ -23,7 +24,7 @@ public GraphQLKeychainCredentialStore(IKeychain keychain, HostAddress address) this.address = address; } - public async Task GetCredentials() + public async Task GetCredentials(CancellationToken cancellationToken = default) { var userPass = await keychain.Load(address).ConfigureAwait(false); return userPass?.Item2; diff --git a/src/GitHub.Api/packages.config b/src/GitHub.Api/packages.config index d28e60261c..c6373f1d86 100644 --- a/src/GitHub.Api/packages.config +++ b/src/GitHub.Api/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/GitHub.App/GitHub.App.csproj b/src/GitHub.App/GitHub.App.csproj index 9e4bd472fd..36a602790d 100644 --- a/src/GitHub.App/GitHub.App.csproj +++ b/src/GitHub.App/GitHub.App.csproj @@ -144,11 +144,11 @@ ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - - ..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.dll + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll - - ..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll @@ -229,6 +229,7 @@ + @@ -259,6 +260,7 @@ + @@ -416,8 +418,8 @@ - Resources.resx - + Resources.resx + PublicResXFileCodeGenerator Resources.Designer.cs diff --git a/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs index 7b7cc105f6..95a36e35c5 100644 --- a/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs @@ -16,10 +16,6 @@ public sealed class PullRequestCheckViewModelDesigner : ViewModelBase, IPullRequ public Uri DetailsUrl { get; set; } = new Uri("http://github.com"); - public string AvatarUrl { get; set; } = "https://avatars1.githubusercontent.com/u/417571?s=88&v=4"; - - public BitmapImage Avatar { get; set; } = null; - public ReactiveCommand OpenDetailsUrl { get; set; } = null; } } \ No newline at end of file diff --git a/src/GitHub.App/Services/FromGraphQlExtensions.cs b/src/GitHub.App/Services/FromGraphQlExtensions.cs new file mode 100644 index 0000000000..d56a63ae93 --- /dev/null +++ b/src/GitHub.App/Services/FromGraphQlExtensions.cs @@ -0,0 +1,105 @@ +using System; +using GitHub.Models; +using Octokit.GraphQL.Model; +using CheckConclusionState = GitHub.Models.CheckConclusionState; +using CheckStatusState = GitHub.Models.CheckStatusState; +using PullRequestReviewState = GitHub.Models.PullRequestReviewState; +using StatusState = GitHub.Models.StatusState; + +namespace GitHub.Services +{ + public static class FromGraphQlExtensions + { + public static CheckConclusionState? FromGraphQl(this Octokit.GraphQL.Model.CheckConclusionState? value) + { + switch (value) + { + case null: + return null; + case Octokit.GraphQL.Model.CheckConclusionState.ActionRequired: + return CheckConclusionState.ActionRequired; + case Octokit.GraphQL.Model.CheckConclusionState.TimedOut: + return CheckConclusionState.TimedOut; + case Octokit.GraphQL.Model.CheckConclusionState.Cancelled: + return CheckConclusionState.Cancelled; + case Octokit.GraphQL.Model.CheckConclusionState.Failure: + return CheckConclusionState.Failure; + case Octokit.GraphQL.Model.CheckConclusionState.Success: + return CheckConclusionState.Success; + case Octokit.GraphQL.Model.CheckConclusionState.Neutral: + return CheckConclusionState.Neutral; + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + + public static PullRequestStateEnum FromGraphQl(this PullRequestState value) + { + switch (value) + { + case PullRequestState.Open: + return PullRequestStateEnum.Open; + case PullRequestState.Closed: + return PullRequestStateEnum.Closed; + case PullRequestState.Merged: + return PullRequestStateEnum.Merged; + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + + public static StatusState FromGraphQl(this Octokit.GraphQL.Model.StatusState value) + { + switch (value) + { + case Octokit.GraphQL.Model.StatusState.Expected: + return StatusState.Expected; + case Octokit.GraphQL.Model.StatusState.Error: + return StatusState.Error; + case Octokit.GraphQL.Model.StatusState.Failure: + return StatusState.Failure; + case Octokit.GraphQL.Model.StatusState.Pending: + return StatusState.Pending; + case Octokit.GraphQL.Model.StatusState.Success: + return StatusState.Success; + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + + public static CheckStatusState FromGraphQl(this Octokit.GraphQL.Model.CheckStatusState value) + { + switch (value) + { + case Octokit.GraphQL.Model.CheckStatusState.Queued: + return CheckStatusState.Queued; + case Octokit.GraphQL.Model.CheckStatusState.InProgress: + return CheckStatusState.InProgress; + case Octokit.GraphQL.Model.CheckStatusState.Completed: + return CheckStatusState.Completed; + case Octokit.GraphQL.Model.CheckStatusState.Requested: + return CheckStatusState.Requested; + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + + public static GitHub.Models.PullRequestReviewState FromGraphQl(this Octokit.GraphQL.Model.PullRequestReviewState value) + { + switch (value) { + case Octokit.GraphQL.Model.PullRequestReviewState.Pending: + return PullRequestReviewState.Pending; + case Octokit.GraphQL.Model.PullRequestReviewState.Commented: + return PullRequestReviewState.Commented; + case Octokit.GraphQL.Model.PullRequestReviewState.Approved: + return PullRequestReviewState.Approved; + case Octokit.GraphQL.Model.PullRequestReviewState.ChangesRequested: + return PullRequestReviewState.ChangesRequested; + case Octokit.GraphQL.Model.PullRequestReviewState.Dismissed: + return PullRequestReviewState.Dismissed; + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } +} \ No newline at end of file diff --git a/src/GitHub.App/Services/PullRequestService.cs b/src/GitHub.App/Services/PullRequestService.cs index 47a7f947fe..ffca06e083 100644 --- a/src/GitHub.App/Services/PullRequestService.cs +++ b/src/GitHub.App/Services/PullRequestService.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using System.Windows.Forms; using GitHub.Api; +using GitHub.App.Services; using GitHub.Extensions; using GitHub.Logging; using GitHub.Models; @@ -23,6 +24,8 @@ using Rothko; using static System.FormattableString; using static Octokit.GraphQL.Variable; +using CheckConclusionState = GitHub.Models.CheckConclusionState; +using CheckStatusState = GitHub.Models.CheckStatusState; using StatusState = GitHub.Models.StatusState; namespace GitHub.Services @@ -38,6 +41,7 @@ public class PullRequestService : IPullRequestService static readonly Regex BranchCapture = new Regex(@"branch\.(?.+)\.ghfvs-pr", RegexOptions.ECMAScript); static ICompiledQuery> readAssignableUsers; static ICompiledQuery> readPullRequests; + static ICompiledQuery> readPullRequestsEnterprise; static readonly string[] TemplatePaths = new[] { @@ -78,51 +82,120 @@ public async Task> ReadPullRequests( string after, PullRequestStateEnum[] states) { - if (readPullRequests == null) + + ICompiledQuery> query; + + if (address.IsGitHubDotCom()) { - readPullRequests = new Query() - .Repository(Var(nameof(owner)), Var(nameof(name))) - .PullRequests( - first: 100, - after: Var(nameof(after)), - orderBy: new IssueOrder { Direction = OrderDirection.Desc, Field = IssueOrderField.CreatedAt }, - states: Var(nameof(states))) - .Select(page => new Page - { - EndCursor = page.PageInfo.EndCursor, - HasNextPage = page.PageInfo.HasNextPage, - TotalCount = page.TotalCount, - Items = page.Nodes.Select(pr => new ListItemAdapter - { - Id = pr.Id.Value, - LastCommit = pr.Commits(null, null, 1, null).Nodes.Select(commit => - new LastCommitSummaryModel - { - Statuses = commit.Commit.Status - .Select(context => - context.Contexts.Select(statusContext => new StatusSummaryModel - { - State = (StatusState)statusContext.State, - }).ToList() - ).SingleOrDefault() - }).ToList().FirstOrDefault(), - Author = new ActorModel - { - Login = pr.Author.Login, - AvatarUrl = pr.Author.AvatarUrl(null), - }, - CommentCount = pr.Comments(0, null, null, null).TotalCount, - Number = pr.Number, - Reviews = pr.Reviews(null, null, null, null, null, null).AllPages().Select(review => new ReviewAdapter - { - Body = review.Body, - CommentCount = review.Comments(null, null, null, null).TotalCount, - }).ToList(), - State = (PullRequestStateEnum)pr.State, - Title = pr.Title, - UpdatedAt = pr.UpdatedAt, - }).ToList(), - }).Compile(); + if (readPullRequests == null) + { + readPullRequests = new Query() + .Repository(Var(nameof(owner)), Var(nameof(name))) + .PullRequests( + first: 100, + after: Var(nameof(after)), + orderBy: new IssueOrder { Direction = OrderDirection.Desc, Field = IssueOrderField.CreatedAt }, + states: Var(nameof(states))) + .Select(page => new Page + { + EndCursor = page.PageInfo.EndCursor, + HasNextPage = page.PageInfo.HasNextPage, + TotalCount = page.TotalCount, + Items = page.Nodes.Select(pr => new ListItemAdapter + { + Id = pr.Id.Value, + LastCommit = pr.Commits(null, null, 1, null).Nodes.Select(commit => + new LastCommitSummaryAdapter + { + CheckSuites = commit.Commit.CheckSuites(null, null, null, null, null).AllPages(10) + .Select(suite => new CheckSuiteSummaryModel + { + CheckRuns = suite.CheckRuns(null, null, null, null, null).AllPages(10) + .Select(run => new CheckRunSummaryModel + { + Conclusion = run.Conclusion.FromGraphQl(), + Status = run.Status.FromGraphQl() + }).ToList() + }).ToList(), + Statuses = commit.Commit.Status + .Select(context => + context.Contexts.Select(statusContext => new StatusSummaryModel + { + State = statusContext.State.FromGraphQl(), + }).ToList() + ).SingleOrDefault() + }).ToList().FirstOrDefault(), + Author = new ActorModel + { + Login = pr.Author.Login, + AvatarUrl = pr.Author.AvatarUrl(null), + }, + CommentCount = pr.Comments(0, null, null, null).TotalCount, + Number = pr.Number, + Reviews = pr.Reviews(null, null, null, null, null, null).AllPages().Select(review => new ReviewAdapter + { + Body = review.Body, + CommentCount = review.Comments(null, null, null, null).TotalCount, + }).ToList(), + State = pr.State.FromGraphQl(), + Title = pr.Title, + UpdatedAt = pr.UpdatedAt, + }).ToList(), + }).Compile(); + } + + query = readPullRequests; + } + else + { + if (readPullRequestsEnterprise == null) + { + readPullRequestsEnterprise = new Query() + .Repository(Var(nameof(owner)), Var(nameof(name))) + .PullRequests( + first: 100, + after: Var(nameof(after)), + orderBy: new IssueOrder { Direction = OrderDirection.Desc, Field = IssueOrderField.CreatedAt }, + states: Var(nameof(states))) + .Select(page => new Page + { + EndCursor = page.PageInfo.EndCursor, + HasNextPage = page.PageInfo.HasNextPage, + TotalCount = page.TotalCount, + Items = page.Nodes.Select(pr => new ListItemAdapter + { + Id = pr.Id.Value, + LastCommit = pr.Commits(null, null, 1, null).Nodes.Select(commit => + new LastCommitSummaryAdapter + { + Statuses = commit.Commit.Status + .Select(context => + context.Contexts.Select(statusContext => new StatusSummaryModel + { + State = statusContext.State.FromGraphQl(), + }).ToList() + ).SingleOrDefault() + }).ToList().FirstOrDefault(), + Author = new ActorModel + { + Login = pr.Author.Login, + AvatarUrl = pr.Author.AvatarUrl(null), + }, + CommentCount = pr.Comments(0, null, null, null).TotalCount, + Number = pr.Number, + Reviews = pr.Reviews(null, null, null, null, null, null).AllPages().Select(review => new ReviewAdapter + { + Body = review.Body, + CommentCount = review.Comments(null, null, null, null).TotalCount, + }).ToList(), + State = pr.State.FromGraphQl(), + Title = pr.Title, + UpdatedAt = pr.UpdatedAt, + }).ToList(), + }).Compile(); + } + + query = readPullRequestsEnterprise; } var graphql = await graphqlFactory.CreateConnection(address); @@ -134,38 +207,65 @@ public async Task> ReadPullRequests( { nameof(states), states.Select(x => (PullRequestState)x).ToList() }, }; - var result = await graphql.Run(readPullRequests, vars); + var result = await graphql.Run(query, vars); foreach (var item in result.Items.Cast()) { item.CommentCount += item.Reviews.Sum(x => x.Count); item.Reviews = null; - var hasStatuses = item.LastCommit.Statuses != null - && item.LastCommit.Statuses.Any(); + var checkRuns = item.LastCommit?.CheckSuites?.SelectMany(model => model.CheckRuns).ToArray(); + + var hasCheckRuns = checkRuns?.Any() ?? false; + var hasStatuses = item.LastCommit?.Statuses?.Any() ?? false; - if (!hasStatuses) + if (!hasCheckRuns && !hasStatuses) { item.Checks = PullRequestChecksState.None; } else { - var statusHasFailure = item.LastCommit - .Statuses - .Any(status => status.State == StatusState.Failure); + var checksHasFailure = false; + var checksHasCompleteSuccess = true; + + if (hasCheckRuns) + { + checksHasFailure = checkRuns + .Any(model => model.Conclusion.HasValue + && (model.Conclusion.Value == CheckConclusionState.Failure + || model.Conclusion.Value == CheckConclusionState.ActionRequired)); + if (!checksHasFailure) + { + checksHasCompleteSuccess = checkRuns + .All(model => model.Conclusion.HasValue + && (model.Conclusion.Value == CheckConclusionState.Success + || model.Conclusion.Value == CheckConclusionState.Neutral)); + } + } + + var statusHasFailure = false; var statusHasCompleteSuccess = true; - if (!statusHasFailure) + + if (!checksHasFailure && hasStatuses) { - statusHasCompleteSuccess = - item.LastCommit.Statuses.All(status => status.State == StatusState.Success); + statusHasFailure = item.LastCommit + .Statuses + .Any(status => status.State == StatusState.Failure + || status.State == StatusState.Error); + + if (!statusHasFailure) + { + statusHasCompleteSuccess = + item.LastCommit.Statuses.All(status => status.State == StatusState.Success); + } } - if (statusHasFailure) + if (checksHasFailure || statusHasFailure) { item.Checks = PullRequestChecksState.Failure; } - else if (statusHasCompleteSuccess) + else if (statusHasCompleteSuccess && checksHasCompleteSuccess) { item.Checks = PullRequestChecksState.Success; } @@ -900,7 +1000,7 @@ class ListItemAdapter : PullRequestListItemModel { public IList Reviews { get; set; } - public LastCommitSummaryModel LastCommit { get; set; } + public LastCommitSummaryAdapter LastCommit { get; set; } } class ReviewAdapter @@ -910,14 +1010,27 @@ class ReviewAdapter public int Count => CommentCount + (!string.IsNullOrWhiteSpace(Body) ? 1 : 0); } - class StatusSummaryModel + class LastCommitSummaryAdapter { - public StatusState State { get; set; } + public List CheckSuites { get; set; } + + public List Statuses { get; set; } } - class LastCommitSummaryModel + class CheckSuiteSummaryModel { - public List Statuses { get; set; } + public List CheckRuns { get; set; } + } + + class CheckRunSummaryModel + { + public CheckConclusionState? Conclusion { get; set; } + public CheckStatusState Status { get; set; } + } + + class StatusSummaryModel + { + public StatusState State { get; set; } } } } diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckType.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckType.cs new file mode 100644 index 0000000000..f5cc9dbc20 --- /dev/null +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckType.cs @@ -0,0 +1,8 @@ +namespace GitHub.ViewModels.GitHubPane +{ + public enum PullRequestCheckType + { + StatusApi, + ChecksApi + } +} \ No newline at end of file diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs index e705a58608..7572aac568 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; +using System.Linq.Expressions; using System.Reactive; using System.Reactive.Linq; using System.Windows.Media.Imaging; @@ -22,7 +23,7 @@ public class PullRequestCheckViewModel: ViewModelBase, IPullRequestCheckViewMode public static IEnumerable Build(IViewViewModelFactory viewViewModelFactory, PullRequestDetailModel pullRequest) { - return pullRequest.Statuses?.Select(model => + var statuses = pullRequest.Statuses?.Select(model => { PullRequestCheckStatus checkStatus; switch (model.State) @@ -43,18 +44,62 @@ public static IEnumerable Build(IViewViewModelFactor } var pullRequestCheckViewModel = (PullRequestCheckViewModel) viewViewModelFactory.CreateViewModel(); + pullRequestCheckViewModel.CheckType = PullRequestCheckType.StatusApi; pullRequestCheckViewModel.Title = model.Context; pullRequestCheckViewModel.Description = model.Description; pullRequestCheckViewModel.Status = checkStatus; - pullRequestCheckViewModel.DetailsUrl = new Uri(model.TargetUrl); - pullRequestCheckViewModel.AvatarUrl = model.AvatarUrl ?? DefaultAvatar; - pullRequestCheckViewModel.Avatar = model.AvatarUrl != null - ? new BitmapImage(new Uri(model.AvatarUrl)) - : AvatarProvider.CreateBitmapImage(DefaultAvatar); + pullRequestCheckViewModel.DetailsUrl = !string.IsNullOrEmpty(model.TargetUrl) ? new Uri(model.TargetUrl) : null; return pullRequestCheckViewModel; - }) ?? new PullRequestCheckViewModel[0]; + + var checks = pullRequest.CheckSuites?.SelectMany(model => model.CheckRuns) + .Select(model => + { + PullRequestCheckStatus checkStatus; + switch (model.Status) + { + case CheckStatusState.Requested: + case CheckStatusState.Queued: + case CheckStatusState.InProgress: + checkStatus = PullRequestCheckStatus.Pending; + break; + + case CheckStatusState.Completed: + switch (model.Conclusion) + { + case CheckConclusionState.Success: + checkStatus = PullRequestCheckStatus.Success; + break; + + case CheckConclusionState.ActionRequired: + case CheckConclusionState.TimedOut: + case CheckConclusionState.Cancelled: + case CheckConclusionState.Failure: + case CheckConclusionState.Neutral: + checkStatus = PullRequestCheckStatus.Failure; + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + break; + default: + throw new ArgumentOutOfRangeException(); + } + + var pullRequestCheckViewModel = (PullRequestCheckViewModel)viewViewModelFactory.CreateViewModel(); + pullRequestCheckViewModel.CheckType = PullRequestCheckType.ChecksApi; + pullRequestCheckViewModel.Title = model.Name; + pullRequestCheckViewModel.Description = model.Summary; + pullRequestCheckViewModel.Status = checkStatus; + pullRequestCheckViewModel.DetailsUrl = new Uri(model.DetailsUrl); + + return pullRequestCheckViewModel; + }) ?? new PullRequestCheckViewModel[0]; + + return statuses.Concat(checks).OrderBy(model => model.Title); } [ImportingConstructor] @@ -66,21 +111,29 @@ public PullRequestCheckViewModel(IUsageTracker usageTracker) private void DoOpenDetailsUrl(object obj) { - usageTracker.IncrementCounter(x => x.NumberOfPRCheckStatusesOpenInGitHub).Forget(); + Expression> expression; + if (CheckType == PullRequestCheckType.StatusApi) + { + expression = x => x.NumberOfPRStatusesOpenInGitHub; + } + else + { + expression = x => x.NumberOfPRChecksOpenInGitHub; + } + + usageTracker.IncrementCounter(expression).Forget(); } public string Title { get; private set; } public string Description { get; private set; } + public PullRequestCheckType CheckType { get; private set; } + public PullRequestCheckStatus Status{ get; private set; } public Uri DetailsUrl { get; private set; } - public string AvatarUrl { get; private set; } - - public BitmapImage Avatar { get; private set; } - public ReactiveCommand OpenDetailsUrl { get; } } } diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs index 449e8393c8..be08da1b1b 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs @@ -126,10 +126,16 @@ public PullRequestDetailViewModel( SyncSubmodules.Subscribe(_ => Refresh().ToObservable()); SubscribeOperationError(SyncSubmodules); - OpenOnGitHub = ReactiveCommand.Create(); + OpenOnGitHub = ReactiveCommand.Create().OnExecuteCompleted(DoOpenDetailsUrl); + ShowReview = ReactiveCommand.Create().OnExecuteCompleted(DoShowReview); } + private void DoOpenDetailsUrl(object obj) + { + usageTracker.IncrementCounter(measuresModel => measuresModel.NumberOfPRDetailsOpenInGitHub).Forget(); + } + /// /// Gets the underlying pull request model. /// diff --git a/src/GitHub.App/packages.config b/src/GitHub.App/packages.config index bc724e90b5..f531b14594 100644 --- a/src/GitHub.App/packages.config +++ b/src/GitHub.App/packages.config @@ -21,7 +21,7 @@ - + diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs index 3793468823..32ea3ebc20 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs @@ -30,16 +30,6 @@ public interface IPullRequestCheckViewModel: IViewModel /// Uri DetailsUrl { get; } - /// - /// The AvatarUrl of the Status/Check application - /// - string AvatarUrl { get; } - - /// - /// The BitmapImage of the AvatarUrl - /// - BitmapImage Avatar { get; } - /// /// A command that opens the DetailsUrl in a browser /// diff --git a/src/GitHub.Exports/GitHub.Exports.csproj b/src/GitHub.Exports/GitHub.Exports.csproj index 6e03a1fbca..a148a9adf0 100644 --- a/src/GitHub.Exports/GitHub.Exports.csproj +++ b/src/GitHub.Exports/GitHub.Exports.csproj @@ -171,6 +171,12 @@ + + + + + + diff --git a/src/GitHub.Exports/Models/AnnotationModel.cs b/src/GitHub.Exports/Models/AnnotationModel.cs new file mode 100644 index 0000000000..c986fc8aa3 --- /dev/null +++ b/src/GitHub.Exports/Models/AnnotationModel.cs @@ -0,0 +1,48 @@ +namespace GitHub.Models +{ + /// + /// Model for a single check annotation. + /// + public class CheckRunAnnotationModel + { + /// + /// The path to the file that this annotation was made on. + /// + public string BlobUrl { get; set; } + + /// + /// The starting line number (1 indexed). + /// + public int StartLine { get; set; } + + /// + /// The ending line number (1 indexed). + /// + public int EndLine { get; set; } + + /// + /// The path that this annotation was made on. + /// + public string Filename { get; set; } + + /// + /// The annotation's message. + /// + public string Message { get; set; } + + /// + /// The annotation's title. + /// + public string Title { get; set; } + + /// + /// The annotation's severity level. + /// + public CheckAnnotationLevel? AnnotationLevel { get; set; } + + /// + /// Additional information about the annotation. + /// + public string RawDetails { get; set; } + } +} \ No newline at end of file diff --git a/src/GitHub.Exports/Models/CheckAnnotationLevel.cs b/src/GitHub.Exports/Models/CheckAnnotationLevel.cs new file mode 100644 index 0000000000..b0efb7e1ec --- /dev/null +++ b/src/GitHub.Exports/Models/CheckAnnotationLevel.cs @@ -0,0 +1,9 @@ +namespace GitHub.Models +{ + public enum CheckAnnotationLevel + { + Failure, + Notice, + Warning, + } +} \ No newline at end of file diff --git a/src/GitHub.Exports/Models/CheckConclusionState.cs b/src/GitHub.Exports/Models/CheckConclusionState.cs new file mode 100644 index 0000000000..3659c3dadb --- /dev/null +++ b/src/GitHub.Exports/Models/CheckConclusionState.cs @@ -0,0 +1,12 @@ +namespace GitHub.Models +{ + public enum CheckConclusionState + { + ActionRequired, + TimedOut, + Cancelled, + Failure, + Success, + Neutral, + } +} \ No newline at end of file diff --git a/src/GitHub.Exports/Models/CheckRunModel.cs b/src/GitHub.Exports/Models/CheckRunModel.cs new file mode 100644 index 0000000000..a25335c8bb --- /dev/null +++ b/src/GitHub.Exports/Models/CheckRunModel.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace GitHub.Models +{ + /// + /// Model for a single check run. + /// + public class CheckRunModel + { + /// The conclusion of the check run. + public CheckConclusionState? Conclusion { get; set; } + + /// + /// The current status of a Check Run. + /// + public CheckStatusState Status { get; set; } + + /// + /// Identifies the date and time when the check run was completed. + /// + public DateTimeOffset? CompletedAt { get; set; } + + /// The name of the check for this check run. + public string Name { get; set; } + + /// + /// The URL from which to find full details of the check run on the integrator's site. + /// + public string DetailsUrl { get; set; } + + /// + /// The summary of a Check Run. + /// + public string Summary { get; set; } + } +} \ No newline at end of file diff --git a/src/GitHub.Exports/Models/CheckStatusState.cs b/src/GitHub.Exports/Models/CheckStatusState.cs new file mode 100644 index 0000000000..6c7e0a2f14 --- /dev/null +++ b/src/GitHub.Exports/Models/CheckStatusState.cs @@ -0,0 +1,10 @@ +namespace GitHub.Models +{ + public enum CheckStatusState + { + Queued, + InProgress, + Completed, + Requested, + } +} \ No newline at end of file diff --git a/src/GitHub.Exports/Models/CheckSuiteModel.cs b/src/GitHub.Exports/Models/CheckSuiteModel.cs new file mode 100644 index 0000000000..c6e82cc1be --- /dev/null +++ b/src/GitHub.Exports/Models/CheckSuiteModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace GitHub.Models +{ + /// + /// Model for a single check suite. + /// + public class CheckSuiteModel + { + /// + /// The check runs associated with a check suite. + /// + public List CheckRuns { get; set; } + } +} \ No newline at end of file diff --git a/src/GitHub.Exports/Models/PullRequestDetailModel.cs b/src/GitHub.Exports/Models/PullRequestDetailModel.cs index 3cd2de8a58..975c6511d2 100644 --- a/src/GitHub.Exports/Models/PullRequestDetailModel.cs +++ b/src/GitHub.Exports/Models/PullRequestDetailModel.cs @@ -93,8 +93,13 @@ public class PullRequestDetailModel public IReadOnlyList Threads { get; set; } /// - /// Gets or sets a collection of pull request Checks & Statuses + /// Gets or sets a collection of pull request Checks Suites /// - public List Statuses { get; set; } + public IReadOnlyList CheckSuites { get; set; } + + /// + /// Gets or sets a collection of pull request Statuses + /// + public IReadOnlyList Statuses { get; set; } } } diff --git a/src/GitHub.Exports/Models/StatusModel.cs b/src/GitHub.Exports/Models/StatusModel.cs index 9d4998a494..3b0832caa0 100644 --- a/src/GitHub.Exports/Models/StatusModel.cs +++ b/src/GitHub.Exports/Models/StatusModel.cs @@ -1,7 +1,7 @@ namespace GitHub.Models { /// - /// Holds details about a pull request Status + /// Model for a single pull request Status. /// public class StatusModel { @@ -24,10 +24,5 @@ public class StatusModel /// The descritption for the Status /// public string Description { get; set; } - - /// - /// The Url for the avatar for the Status - /// - public string AvatarUrl { get; set; } } } \ No newline at end of file diff --git a/src/GitHub.Exports/Models/UsageModel.cs b/src/GitHub.Exports/Models/UsageModel.cs index 4a668c13ec..74c5e6c057 100644 --- a/src/GitHub.Exports/Models/UsageModel.cs +++ b/src/GitHub.Exports/Models/UsageModel.cs @@ -59,7 +59,8 @@ public class MeasuresModel public int NumberOfWelcomeTrainingClicks { get; set; } public int NumberOfGitHubPaneHelpClicks { get; set; } public int NumberOfPRDetailsOpenInGitHub { get; set; } - public int NumberOfPRCheckStatusesOpenInGitHub { get; set; } + public int NumberOfPRStatusesOpenInGitHub { get; set; } + public int NumberOfPRChecksOpenInGitHub { get; set; } public int NumberOfPRDetailsViewChanges { get; set; } public int NumberOfPRDetailsViewFile { get; set; } public int NumberOfPRDetailsCompareWithSolution { get; set; } diff --git a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj index 8e46e3a6b0..09afe0daad 100644 --- a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj +++ b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj @@ -373,11 +373,11 @@ ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - - ..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.dll + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll - - ..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs index ccc89a6dfd..c4ed289942 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading.Tasks; using GitHub.Api; +using GitHub.App.Services; using GitHub.Factories; using GitHub.InlineReviews.Models; using GitHub.Models; @@ -17,6 +18,7 @@ using LibGit2Sharp; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Projection; +using Octokit; using Octokit.GraphQL; using Octokit.GraphQL.Core; using Octokit.GraphQL.Model; @@ -24,6 +26,13 @@ using Serilog; using PullRequestReviewEvent = Octokit.PullRequestReviewEvent; using static Octokit.GraphQL.Variable; +using CheckAnnotationLevel = GitHub.Models.CheckAnnotationLevel; +using CheckConclusionState = GitHub.Models.CheckConclusionState; +using CheckStatusState = GitHub.Models.CheckStatusState; +using DraftPullRequestReviewComment = Octokit.GraphQL.Model.DraftPullRequestReviewComment; +using FileMode = System.IO.FileMode; +using NotFoundException = LibGit2Sharp.NotFoundException; +using PullRequestReviewState = Octokit.GraphQL.Model.PullRequestReviewState; using StatusState = GitHub.Models.StatusState; // GraphQL DatabaseId field are marked as deprecated, but we need them for interop with REST. @@ -40,6 +49,7 @@ public class PullRequestSessionService : IPullRequestSessionService static readonly ILogger log = LogManager.ForContext(); static ICompiledQuery readPullRequest; static ICompiledQuery> readCommitStatuses; + static ICompiledQuery> readCommitStatusesEnterprise; static ICompiledQuery readViewer; readonly IGitService gitService; @@ -292,14 +302,14 @@ public virtual async Task ReadPullRequestDetail(HostAddr HeadRefName = pr.HeadRefName, HeadRefSha = pr.HeadRefOid, HeadRepositoryOwner = pr.HeadRepositoryOwner != null ? pr.HeadRepositoryOwner.Login : null, - State = (PullRequestStateEnum)pr.State, + State = pr.State.FromGraphQl(), UpdatedAt = pr.UpdatedAt, Reviews = pr.Reviews(null, null, null, null, null, null).AllPages().Select(review => new PullRequestReviewModel { Id = review.Id.Value, Body = review.Body, CommitId = review.Commit.Oid, - State = (GitHub.Models.PullRequestReviewState)review.State, + State = review.State.FromGraphQl(), SubmittedAt = review.SubmittedAt, Author = new ActorModel { @@ -346,6 +356,7 @@ public virtual async Task ReadPullRequestDetail(HostAddr var lastCommitModel = await GetPullRequestLastCommitAdapter(address, owner, name, number); result.Statuses = lastCommitModel.Statuses; + result.CheckSuites = lastCommitModel.CheckSuites; result.ChangedFiles = files.Select(file => new PullRequestFileModel { @@ -743,26 +754,69 @@ Task GetRepository(ILocalRepositoryModel repository) async Task GetPullRequestLastCommitAdapter(HostAddress address, string owner, string name, int number) { - if (readCommitStatuses == null) + ICompiledQuery> query; + if (address.IsGitHubDotCom()) { - readCommitStatuses = new Query() - .Repository(Var(nameof(owner)), Var(nameof(name))) - .PullRequest(Var(nameof(number))).Commits(last: 1).Nodes.Select( - commit => new LastCommitAdapter - { - Statuses = commit.Commit.Status - .Select(context => - context.Contexts.Select(statusContext => new StatusModel - { - State = (StatusState)statusContext.State, - Context = statusContext.Context, - TargetUrl = statusContext.TargetUrl, - Description = statusContext.Description, - AvatarUrl = statusContext.Creator.AvatarUrl(null) - }).ToList() - ).SingleOrDefault() - } - ).Compile(); + if (readCommitStatuses == null) + { + readCommitStatuses = new Query() + .Repository(Var(nameof(owner)), Var(nameof(name))) + .PullRequest(Var(nameof(number))).Commits(last: 1).Nodes.Select( + commit => new LastCommitAdapter + { + CheckSuites = commit.Commit.CheckSuites(null, null, null, null, null).AllPages(10) + .Select(suite => new CheckSuiteModel + { + CheckRuns = suite.CheckRuns(null, null, null, null, null).AllPages(10) + .Select(run => new CheckRunModel + { + Conclusion = run.Conclusion.FromGraphQl(), + Status = run.Status.FromGraphQl(), + Name = run.Name, + DetailsUrl = run.Permalink, + Summary = run.Summary, + }).ToList() + }).ToList(), + Statuses = commit.Commit.Status + .Select(context => + context.Contexts.Select(statusContext => new StatusModel + { + State = statusContext.State.FromGraphQl(), + Context = statusContext.Context, + TargetUrl = statusContext.TargetUrl, + Description = statusContext.Description, + }).ToList() + ).SingleOrDefault() + } + ).Compile(); + } + + query = readCommitStatuses; + } + else + { + if (readCommitStatusesEnterprise == null) + { + readCommitStatusesEnterprise = new Query() + .Repository(Var(nameof(owner)), Var(nameof(name))) + .PullRequest(Var(nameof(number))).Commits(last: 1).Nodes.Select( + commit => new LastCommitAdapter + { + Statuses = commit.Commit.Status + .Select(context => + context.Contexts.Select(statusContext => new StatusModel + { + State = statusContext.State.FromGraphQl(), + Context = statusContext.Context, + TargetUrl = statusContext.TargetUrl, + Description = statusContext.Description, + }).ToList() + ).SingleOrDefault() + } + ).Compile(); + } + + query = readCommitStatusesEnterprise; } var vars = new Dictionary @@ -773,7 +827,7 @@ async Task GetPullRequestLastCommitAdapter(HostAddress addres }; var connection = await graphqlFactory.CreateConnection(address); - var result = await connection.Run(readCommitStatuses, vars); + var result = await connection.Run(query, vars); return result.First(); } @@ -836,11 +890,6 @@ static void BuildPullRequestThreads(PullRequestDetailModel model) model.Threads = threads; } - static GitHub.Models.PullRequestReviewState FromGraphQL(Octokit.GraphQL.Model.PullRequestReviewState s) - { - return (GitHub.Models.PullRequestReviewState)s; - } - static Octokit.GraphQL.Model.PullRequestReviewEvent ToGraphQl(Octokit.PullRequestReviewEvent e) { switch (e) @@ -869,6 +918,8 @@ class CommentAdapter : PullRequestReviewCommentModel class LastCommitAdapter { + public List CheckSuites { get; set; } + public List Statuses { get; set; } } } diff --git a/src/GitHub.InlineReviews/packages.config b/src/GitHub.InlineReviews/packages.config index 946cd3d240..44d71eb082 100644 --- a/src/GitHub.InlineReviews/packages.config +++ b/src/GitHub.InlineReviews/packages.config @@ -34,7 +34,7 @@ - + diff --git a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj index 2a2eff01e8..9be12f78af 100644 --- a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj +++ b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj @@ -258,11 +258,11 @@ ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - - ..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.dll + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll - - ..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll ..\..\packages\Rothko.0.0.3-ghfvs\lib\net45\rothko.dll diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestCheckView.xaml b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestCheckView.xaml index c4dfc84771..d04daeeda1 100644 --- a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestCheckView.xaml +++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestCheckView.xaml @@ -25,7 +25,7 @@ - + @@ -41,12 +41,13 @@ - diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestReviewSummaryView.xaml b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestReviewSummaryView.xaml index ac5477ecc3..7f8389fb7f 100644 --- a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestReviewSummaryView.xaml +++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestReviewSummaryView.xaml @@ -16,7 +16,7 @@ Name="root"> - + diff --git a/src/GitHub.VisualStudio/packages.config b/src/GitHub.VisualStudio/packages.config index b5614cd619..c9fd83b206 100644 --- a/src/GitHub.VisualStudio/packages.config +++ b/src/GitHub.VisualStudio/packages.config @@ -37,7 +37,7 @@ - + diff --git a/test/GitHub.App.UnitTests/GitHub.App.UnitTests.csproj b/test/GitHub.App.UnitTests/GitHub.App.UnitTests.csproj index 99044da6d8..72d56259cc 100644 --- a/test/GitHub.App.UnitTests/GitHub.App.UnitTests.csproj +++ b/test/GitHub.App.UnitTests/GitHub.App.UnitTests.csproj @@ -91,11 +91,11 @@ ..\..\packages\NSubstitute.2.0.3\lib\net45\NSubstitute.dll True - - ..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.dll + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll - - ..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll diff --git a/test/GitHub.App.UnitTests/packages.config b/test/GitHub.App.UnitTests/packages.config index 43cd82a08e..84d11fa23a 100644 --- a/test/GitHub.App.UnitTests/packages.config +++ b/test/GitHub.App.UnitTests/packages.config @@ -30,7 +30,7 @@ - +