diff --git a/lib/Octokit.GraphQL.0.1.1-beta.nupkg b/lib/Octokit.GraphQL.0.1.1-beta.nupkg deleted file mode 100644 index ac26783672..0000000000 Binary files a/lib/Octokit.GraphQL.0.1.1-beta.nupkg and /dev/null differ diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 6ff35a4ff0..dabd0989c6 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -30,6 +30,6 @@ - + diff --git a/src/GitHub.App/GitHub.App.csproj b/src/GitHub.App/GitHub.App.csproj index a6f5be28c8..5a147b26c3 100644 --- a/src/GitHub.App/GitHub.App.csproj +++ b/src/GitHub.App/GitHub.App.csproj @@ -49,10 +49,10 @@ - + - \ No newline at end of file + diff --git a/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs b/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs index cd282d81c7..aaf13bafef 100644 --- a/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/CommentThreadViewModelDesigner.cs @@ -1,5 +1,7 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using GitHub.Models; using GitHub.ViewModels; using ReactiveUI; @@ -8,6 +10,16 @@ namespace GitHub.SampleData [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")] public class CommentThreadViewModelDesigner : ViewModelBase, ICommentThreadViewModel { + public CommentThreadViewModelDesigner() + { + Comments = new ReactiveList(){new CommentViewModelDesigner() + { + Author = new ActorViewModel{ Login = "shana"}, + Body = "You can use a `CompositeDisposable` type here, it's designed to handle disposables in an optimal way (you can just call `Dispose()` on it and it will handle disposing everything it holds)." + }}; + + } + public IReadOnlyReactiveList Comments { get; } = new ReactiveList(); diff --git a/src/GitHub.App/SampleData/InlineAnnotationViewModelDesigner.cs b/src/GitHub.App/SampleData/InlineAnnotationViewModelDesigner.cs new file mode 100644 index 0000000000..4aea77efb7 --- /dev/null +++ b/src/GitHub.App/SampleData/InlineAnnotationViewModelDesigner.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using GitHub.Models; +using GitHub.ViewModels; + +namespace GitHub.SampleData +{ + public class InlineAnnotationViewModelDesigner : IInlineAnnotationViewModel + { + public InlineAnnotationViewModelDesigner() + { + var checkRunAnnotationModel = new CheckRunAnnotationModel + { + AnnotationLevel = CheckAnnotationLevel.Failure, + Path = "SomeFile.cs", + EndLine = 12, + StartLine = 12, + Message = "Some Error Message", + Title = "CS12345" + }; + + var checkRunModel = + new CheckRunModel + { + Annotations = new List {checkRunAnnotationModel}, + Name = "Fake Check Run" + }; + + var checkSuiteModel = new CheckSuiteModel() + { + ApplicationName = "Fake Check Suite", + HeadSha = "ed6198c37b13638e902716252b0a17d54bd59e4a", + CheckRuns = new List { checkRunModel} + }; + + Model= new InlineAnnotationModel(checkSuiteModel, checkRunModel, checkRunAnnotationModel); + } + + public InlineAnnotationModel Model { get; } + } +} \ No newline at end of file diff --git a/src/GitHub.App/SampleData/PullRequestAnnotationItemViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestAnnotationItemViewModelDesigner.cs new file mode 100644 index 0000000000..03f4e7d056 --- /dev/null +++ b/src/GitHub.App/SampleData/PullRequestAnnotationItemViewModelDesigner.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reactive; +using GitHub.Models; +using GitHub.ViewModels.GitHubPane; +using ReactiveUI; + +namespace GitHub.SampleData +{ + [ExcludeFromCodeCoverage] + public sealed class PullRequestAnnotationItemViewModelDesigner : IPullRequestAnnotationItemViewModel + { + public CheckRunAnnotationModel Annotation { get; set; } + public bool IsExpanded { get; set; } + public string LineDescription => $"{Annotation.StartLine}:{Annotation.EndLine}"; + public bool IsFileInPullRequest { get; set; } + public ReactiveCommand OpenAnnotation { get; } + } +} \ No newline at end of file diff --git a/src/GitHub.App/SampleData/PullRequestAnnotationsViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestAnnotationsViewModelDesigner.cs new file mode 100644 index 0000000000..8e728ee8f3 --- /dev/null +++ b/src/GitHub.App/SampleData/PullRequestAnnotationsViewModelDesigner.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reactive; +using System.Threading.Tasks; +using GitHub.Models; +using GitHub.ViewModels.GitHubPane; +using ReactiveUI; + +namespace GitHub.SampleData +{ + [ExcludeFromCodeCoverage] + public sealed class PullRequestAnnotationsViewModelDesigner : PanePageViewModelBase, IPullRequestAnnotationsViewModel + { + public LocalRepositoryModel LocalRepository { get; set; } + public string RemoteRepositoryOwner { get; set; } + public int PullRequestNumber { get; set; } = 123; + public string CheckRunId { get; set; } + public ReactiveCommand NavigateToPullRequest { get; } + public string PullRequestTitle { get; } = "Fixing stuff in this PR"; + public string CheckSuiteName { get; } = "Awesome Check Suite"; + public string CheckRunSummary { get; } = "Awesome Check Run Summary"; + public string CheckRunText { get; } = "Awesome Check Run Text"; + + public IReadOnlyDictionary AnnotationsDictionary { get; } + = new Dictionary + { + { + "asdf/asdf.cs", + new IPullRequestAnnotationItemViewModel[] + { + new PullRequestAnnotationItemViewModelDesigner + { + Annotation = new CheckRunAnnotationModel + { + AnnotationLevel = CheckAnnotationLevel.Warning, + StartLine = 3, + EndLine = 4, + Path = "asdf/asdf.cs", + Message = "; is expected", + Title = "CS 12345" + }, + IsExpanded = true, + IsFileInPullRequest = true + }, + new PullRequestAnnotationItemViewModelDesigner + { + Annotation = new CheckRunAnnotationModel + { + AnnotationLevel = CheckAnnotationLevel.Failure, + StartLine = 3, + EndLine = 4, + Path = "asdf/asdf.cs", + Message = "; is expected", + Title = "CS 12345" + }, + IsExpanded = true, + IsFileInPullRequest = true + }, + } + }, + { + "blah.cs", + new IPullRequestAnnotationItemViewModel[] + { + new PullRequestAnnotationItemViewModelDesigner + { + Annotation = new CheckRunAnnotationModel + { + AnnotationLevel = CheckAnnotationLevel.Notice, + StartLine = 3, + EndLine = 4, + Path = "blah.cs", + Message = "; is expected", + Title = "CS 12345" + }, + IsExpanded = true, + } + } + }, + }; + + public string CheckRunName { get; } = "Psuedo Check Run"; + + public Task InitializeAsync(LocalRepositoryModel localRepository, IConnection connection, string owner, + string repo, + int pullRequestNumber, string checkRunId) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs index 3011e581c1..ec75bf27be 100644 --- a/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestCheckViewModelDesigner.cs @@ -1,6 +1,6 @@ using System; using System.Reactive; -using System.Windows.Media.Imaging; +using GitHub.Models; using GitHub.ViewModels; using GitHub.ViewModels.GitHubPane; using ReactiveUI; @@ -18,5 +18,11 @@ public sealed class PullRequestCheckViewModelDesigner : ViewModelBase, IPullRequ public Uri DetailsUrl { get; set; } = new Uri("http://github.com"); public ReactiveCommand OpenDetailsUrl { get; set; } = null; + + public PullRequestCheckType CheckType { get; set; } = PullRequestCheckType.ChecksApi; + + public string CheckRunId { get; set; } + + public bool HasAnnotations { get; } = true; } } \ No newline at end of file diff --git a/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs index 0bb87d5d37..9f1ad0262a 100644 --- a/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs @@ -9,6 +9,7 @@ using System.Reactive; using System.Threading.Tasks; using GitHub.SampleData; +using ReactiveUI.Legacy; namespace GitHub.SampleData { @@ -122,8 +123,10 @@ public PullRequestDetailViewModelDesigner() public ReactiveCommand Checkout { get; } public ReactiveCommand Pull { get; } public ReactiveCommand Push { get; } + public ReactiveCommand SyncSubmodules { get; } public ReactiveCommand OpenOnGitHub { get; } public ReactiveCommand ShowReview { get; } + public ReactiveCommand ShowAnnotations { get; } public IReadOnlyList Checks { get; } diff --git a/src/GitHub.App/SampleData/PullRequestFilesViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestFilesViewModelDesigner.cs index b60b43bc03..5dda31c86d 100644 --- a/src/GitHub.App/SampleData/PullRequestFilesViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestFilesViewModelDesigner.cs @@ -36,6 +36,9 @@ public PullRequestFilesViewModelDesigner() public ReactiveCommand DiffFileWithWorkingDirectory { get; } public ReactiveCommand OpenFileInWorkingDirectory { get; } public ReactiveCommand OpenFirstComment { get; } + public ReactiveCommand OpenFirstAnnotationNotice { get; } + public ReactiveCommand OpenFirstAnnotationWarning { get; } + public ReactiveCommand OpenFirstAnnotationFailure { get; } public Task InitializeAsync( IPullRequestSession session, diff --git a/src/GitHub.App/Services/FromGraphQlExtensions.cs b/src/GitHub.App/Services/FromGraphQlExtensions.cs index d56a63ae93..d9b79bcece 100644 --- a/src/GitHub.App/Services/FromGraphQlExtensions.cs +++ b/src/GitHub.App/Services/FromGraphQlExtensions.cs @@ -1,6 +1,7 @@ using System; using GitHub.Models; using Octokit.GraphQL.Model; +using CheckAnnotationLevel = GitHub.Models.CheckAnnotationLevel; using CheckConclusionState = GitHub.Models.CheckConclusionState; using CheckStatusState = GitHub.Models.CheckStatusState; using PullRequestReviewState = GitHub.Models.PullRequestReviewState; @@ -84,7 +85,7 @@ public static CheckStatusState FromGraphQl(this Octokit.GraphQL.Model.CheckStatu } } - public static GitHub.Models.PullRequestReviewState FromGraphQl(this Octokit.GraphQL.Model.PullRequestReviewState value) + public static PullRequestReviewState FromGraphQl(this Octokit.GraphQL.Model.PullRequestReviewState value) { switch (value) { case Octokit.GraphQL.Model.PullRequestReviewState.Pending: @@ -101,5 +102,20 @@ public static GitHub.Models.PullRequestReviewState FromGraphQl(this Octokit.Grap throw new ArgumentOutOfRangeException(nameof(value), value, null); } } + + public static CheckAnnotationLevel FromGraphQl(this Octokit.GraphQL.Model.CheckAnnotationLevel value) + { + switch (value) + { + case Octokit.GraphQL.Model.CheckAnnotationLevel.Failure: + return CheckAnnotationLevel.Failure; + case Octokit.GraphQL.Model.CheckAnnotationLevel.Notice: + return CheckAnnotationLevel.Notice; + case Octokit.GraphQL.Model.CheckAnnotationLevel.Warning: + return CheckAnnotationLevel.Warning; + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } } } \ No newline at end of file diff --git a/src/GitHub.App/Services/PullRequestEditorService.cs b/src/GitHub.App/Services/PullRequestEditorService.cs index aaf44c50ad..db4889e936 100644 --- a/src/GitHub.App/Services/PullRequestEditorService.cs +++ b/src/GitHub.App/Services/PullRequestEditorService.cs @@ -285,7 +285,7 @@ await pullRequestService.ExtractToTempFile( } /// - public async Task OpenDiff( + public Task OpenDiff( IPullRequestSession session, string relativePath, IInlineCommentThreadModel thread) @@ -294,11 +294,17 @@ public async Task OpenDiff( Guard.ArgumentNotEmptyString(relativePath, nameof(relativePath)); Guard.ArgumentNotNull(thread, nameof(thread)); - var diffViewer = await OpenDiff(session, relativePath, thread.CommitSha, scrollToFirstDraftOrDiff: false); + return OpenDiff(session, relativePath, thread.CommitSha, thread.LineNumber - 1); + } + + /// + public async Task OpenDiff(IPullRequestSession session, string relativePath, string headSha, int fromLine) + { + var diffViewer = await OpenDiff(session, relativePath, headSha, scrollToFirstDraftOrDiff: false); - var param = (object)new InlineCommentNavigationParams + var param = (object) new InlineCommentNavigationParams { - FromLine = thread.LineNumber - 1, + FromLine = fromLine, }; // HACK: We need to wait here for the inline comment tags to initialize so we can find the next inline comment. diff --git a/src/GitHub.App/Services/PullRequestService.cs b/src/GitHub.App/Services/PullRequestService.cs index 9e141857fa..55a85b7d98 100644 --- a/src/GitHub.App/Services/PullRequestService.cs +++ b/src/GitHub.App/Services/PullRequestService.cs @@ -115,7 +115,7 @@ public async Task> ReadPullRequests( { Conclusion = run.Conclusion.FromGraphQl(), Status = run.Status.FromGraphQl() - }).ToList() + }).ToList(), }).ToList(), Statuses = commit.Commit.Status .Select(context => diff --git a/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs index 05f70c788c..5e13fe5a4a 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs @@ -35,6 +35,7 @@ public sealed class GitHubPaneViewModel : ViewModelBase, IGitHubPaneViewModel, I static readonly Regex pullUri = CreateRoute("/:owner/:repo/pull/:number"); static readonly Regex pullNewReviewUri = CreateRoute("/:owner/:repo/pull/:number/review/new"); static readonly Regex pullUserReviewsUri = CreateRoute("/:owner/:repo/pull/:number/reviews/:login"); + static readonly Regex pullCheckRunsUri = CreateRoute("/:owner/:repo/pull/:number/checkruns/:id"); readonly IViewViewModelFactory viewModelFactory; readonly ISimpleApiClientFactory apiClientFactory; @@ -266,6 +267,15 @@ public async Task NavigateTo(Uri uri) var login = match.Groups["login"].Value; await ShowPullRequestReviews(owner, repo, number, login); } + else if ((match = pullCheckRunsUri.Match(uri.AbsolutePath))?.Success == true) + { + var owner = match.Groups["owner"].Value; + var repo = match.Groups["repo"].Value; + var number = int.Parse(match.Groups["number"].Value); + var id = match.Groups["id"].Value; + + await ShowPullRequestCheckRun(owner, repo, number, id); + } else { throw new NotSupportedException("Unrecognised GitHub pane URL: " + uri.AbsolutePath); @@ -319,6 +329,20 @@ public Task ShowPullRequestReviews(string owner, string repo, int number, string x.User.Login == login); } + /// + public Task ShowPullRequestCheckRun(string owner, string repo, int number, string checkRunId) + { + Guard.ArgumentNotNull(owner, nameof(owner)); + Guard.ArgumentNotNull(repo, nameof(repo)); + + return NavigateTo( + x => x.InitializeAsync(LocalRepository, Connection, owner, repo, number, checkRunId), + x => x.RemoteRepositoryOwner == owner && + x.LocalRepository.Name == repo && + x.PullRequestNumber == number && + x.CheckRunId == checkRunId); + } + /// public Task ShowPullRequestReviewAuthoring(string owner, string repo, int number) { @@ -489,8 +513,8 @@ static async Task IsValidRepository(ISimpleApiClient client) static Regex CreateRoute(string route) { - // Build RegEx from route (:foo to named group (?[\w_.-]+)). - var routeFormat = "^" + new Regex("(:([a-z]+))\\b").Replace(route, @"(?<$2>[\w_.-]+)") + "$"; + // Build RegEx from route (:foo to named group (?[\w_.\-=]+)). + var routeFormat = "^" + new Regex("(:([a-z]+))\\b").Replace(route, @"(?<$2>[\w_.\-=]+)") + "$"; return new Regex(routeFormat, RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase); } } diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationItemViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationItemViewModel.cs new file mode 100644 index 0000000000..d07c806f89 --- /dev/null +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationItemViewModel.cs @@ -0,0 +1,56 @@ +using System.Reactive; +using System.Reactive.Linq; +using GitHub.Models; +using GitHub.Services; +using ReactiveUI; + +namespace GitHub.ViewModels.GitHubPane +{ + /// + public class PullRequestAnnotationItemViewModel : ViewModelBase, IPullRequestAnnotationItemViewModel + { + bool isExpanded; + + /// + /// Initializes the . + /// + /// The check run annotation model. + /// A flag that denotes if the annotation is part of the pull request's changes. + /// The check suite model. + /// The pull request session. + /// The pull request editor service. + public PullRequestAnnotationItemViewModel( + CheckRunAnnotationModel annotation, + bool isFileInPullRequest, + CheckSuiteModel checkSuite, + IPullRequestSession session, + IPullRequestEditorService editorService) + { + Annotation = annotation; + IsFileInPullRequest = isFileInPullRequest; + + OpenAnnotation = ReactiveCommand.CreateFromTask( + async _ => await editorService.OpenDiff(session, annotation.Path, checkSuite.HeadSha, annotation.EndLine - 1), + Observable.Return(IsFileInPullRequest)); + } + + /// + public bool IsFileInPullRequest { get; } + + /// + public CheckRunAnnotationModel Annotation { get; } + + /// + public string LineDescription => $"{Annotation.StartLine}:{Annotation.EndLine}"; + + /// + public ReactiveCommand OpenAnnotation { get; } + + /// + public bool IsExpanded + { + get { return isExpanded; } + set { this.RaiseAndSetIfChanged(ref isExpanded, value); } + } + } +} \ No newline at end of file diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationsViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationsViewModel.cs new file mode 100644 index 0000000000..8b74b94ca5 --- /dev/null +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestAnnotationsViewModel.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Reactive; +using System.Threading.Tasks; +using GitHub.Models; +using GitHub.Services; +using ReactiveUI; + +namespace GitHub.ViewModels.GitHubPane +{ + /// + [Export(typeof(IPullRequestAnnotationsViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class PullRequestAnnotationsViewModel : PanePageViewModelBase, IPullRequestAnnotationsViewModel + { + readonly IPullRequestSessionManager sessionManager; + readonly IPullRequestEditorService pullRequestEditorService; + + IPullRequestSession session; + string title; + string checkSuiteName; + string checkRunName; + IReadOnlyDictionary annotationsDictionary; + string checkRunSummary; + string checkRunText; + + /// + /// Initializes a new instance of the class. + /// + /// The pull request session manager. + /// The pull request editor service. + [ImportingConstructor] + public PullRequestAnnotationsViewModel(IPullRequestSessionManager sessionManager, IPullRequestEditorService pullRequestEditorService) + { + this.sessionManager = sessionManager; + this.pullRequestEditorService = pullRequestEditorService; + NavigateToPullRequest = ReactiveCommand.Create(() => { + NavigateTo(FormattableString.Invariant( + $"{LocalRepository.Owner}/{LocalRepository.Name}/pull/{PullRequestNumber}")); + }); + } + + /// + public async Task InitializeAsync(LocalRepositoryModel localRepository, IConnection connection, string owner, + string repo, int pullRequestNumber, string checkRunId) + { + if (repo != localRepository.Name) + { + throw new NotSupportedException(); + } + + IsLoading = true; + + try + { + LocalRepository = localRepository; + RemoteRepositoryOwner = owner; + PullRequestNumber = pullRequestNumber; + CheckRunId = checkRunId; + session = await sessionManager.GetSession(owner, repo, pullRequestNumber); + Load(session.PullRequest); + } + finally + { + IsLoading = false; + } + } + + /// + public LocalRepositoryModel LocalRepository { get; private set; } + + /// + public string RemoteRepositoryOwner { get; private set; } + + /// + public int PullRequestNumber { get; private set; } + + /// + public string CheckRunId { get; private set; } + + /// + public ReactiveCommand NavigateToPullRequest { get; private set; } + + /// + public string PullRequestTitle + { + get { return title; } + private set { this.RaiseAndSetIfChanged(ref title, value); } + } + + /// + public string CheckSuiteName + { + get { return checkSuiteName; } + private set { this.RaiseAndSetIfChanged(ref checkSuiteName, value); } + } + + /// + public string CheckRunName + { + get { return checkRunName; } + private set { this.RaiseAndSetIfChanged(ref checkRunName, value); } + } + + /// + public string CheckRunSummary + { + get { return checkRunSummary; } + private set { this.RaiseAndSetIfChanged(ref checkRunSummary, value); } + } + + /// + public string CheckRunText + { + get { return checkRunText; } + private set { this.RaiseAndSetIfChanged(ref checkRunText, value); } + } + + /// + public IReadOnlyDictionary AnnotationsDictionary + { + get { return annotationsDictionary; } + private set { this.RaiseAndSetIfChanged(ref annotationsDictionary, value); } + } + + void Load(PullRequestDetailModel pullRequest) + { + IsBusy = true; + + try + { + PullRequestTitle = pullRequest.Title; + + var checkSuiteRun = pullRequest + .CheckSuites.SelectMany(checkSuite => checkSuite.CheckRuns + .Select(checkRun => new{checkSuite, checkRun})) + .First(arg => arg.checkRun.Id == CheckRunId); + + CheckSuiteName = checkSuiteRun.checkSuite.ApplicationName; + CheckRunName = checkSuiteRun.checkRun.Name; + CheckRunSummary = checkSuiteRun.checkRun.Summary; + CheckRunText = checkSuiteRun.checkRun.Text; + + var changedFiles = new HashSet(session.PullRequest.ChangedFiles.Select(model => model.FileName)); + + var annotationsLookup = checkSuiteRun.checkRun.Annotations + .ToLookup(annotation => annotation.Path); + + AnnotationsDictionary = annotationsLookup + .Select(models => models.Key) + .OrderBy(s => s) + .ToDictionary( + path => path, + path => annotationsLookup[path] + .Select(annotation => new PullRequestAnnotationItemViewModel(annotation, changedFiles.Contains(path), checkSuiteRun.checkSuite, session, pullRequestEditorService)) + .Cast() + .ToArray() + ); + } + finally + { + IsBusy = false; + } + } + } +} \ 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 f95bd95fd1..0f0ab9224c 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckViewModel.cs @@ -4,29 +4,35 @@ using System.Linq; using System.Linq.Expressions; using System.Reactive; -using System.Reactive.Linq; -using System.Windows.Media.Imaging; using GitHub.Extensions; using GitHub.Factories; using GitHub.Models; +using GitHub.Primitives; using GitHub.Services; using ReactiveUI; namespace GitHub.ViewModels.GitHubPane { + /// [Export(typeof(IPullRequestCheckViewModel))] [PartCreationPolicy(CreationPolicy.NonShared)] public class PullRequestCheckViewModel: ViewModelBase, IPullRequestCheckViewModel { - private readonly IUsageTracker usageTracker; const string DefaultAvatar = "pack://application:,,,/GitHub.App;component/Images/default_user_avatar.png"; + private readonly IUsageTracker usageTracker; + + /// + /// Factory method to create a . + /// + /// A viewviewmodel factory. + /// The pull request. public static IEnumerable Build(IViewViewModelFactory viewViewModelFactory, PullRequestDetailModel pullRequest) { - var statuses = pullRequest.Statuses?.Select(model => + var statuses = pullRequest.Statuses?.Select(statusModel => { PullRequestCheckStatus checkStatus; - switch (model.State) + switch (statusModel.State) { case StatusState.Expected: case StatusState.Error: @@ -45,19 +51,22 @@ public static IEnumerable Build(IViewViewModelFactor var pullRequestCheckViewModel = (PullRequestCheckViewModel) viewViewModelFactory.CreateViewModel(); pullRequestCheckViewModel.CheckType = PullRequestCheckType.StatusApi; - pullRequestCheckViewModel.Title = model.Context; - pullRequestCheckViewModel.Description = model.Description; + pullRequestCheckViewModel.Title = statusModel.Context; + pullRequestCheckViewModel.Description = statusModel.Description; pullRequestCheckViewModel.Status = checkStatus; - pullRequestCheckViewModel.DetailsUrl = !string.IsNullOrEmpty(model.TargetUrl) ? new Uri(model.TargetUrl) : null; + pullRequestCheckViewModel.DetailsUrl = !string.IsNullOrEmpty(statusModel.TargetUrl) ? new Uri(statusModel.TargetUrl) : null; return pullRequestCheckViewModel; }) ?? Array.Empty(); - var checks = pullRequest.CheckSuites?.SelectMany(model => model.CheckRuns) - .Select(model => + var checks = + pullRequest.CheckSuites? + .SelectMany(checkSuite => checkSuite.CheckRuns + .Select(checkRun => new { checkSuiteModel = checkSuite, checkRun})) + .Select(arg => { PullRequestCheckStatus checkStatus; - switch (model.Status) + switch (arg.checkRun.Status) { case CheckStatusState.Requested: case CheckStatusState.Queued: @@ -66,7 +75,7 @@ public static IEnumerable Build(IViewViewModelFactor break; case CheckStatusState.Completed: - switch (model.Conclusion) + switch (arg.checkRun.Conclusion) { case CheckConclusionState.Success: checkStatus = PullRequestCheckStatus.Success; @@ -91,17 +100,22 @@ public static IEnumerable Build(IViewViewModelFactor var pullRequestCheckViewModel = (PullRequestCheckViewModel)viewViewModelFactory.CreateViewModel(); pullRequestCheckViewModel.CheckType = PullRequestCheckType.ChecksApi; - pullRequestCheckViewModel.Title = model.Name; - pullRequestCheckViewModel.Description = model.Summary; + pullRequestCheckViewModel.CheckRunId = arg.checkRun.Id; + pullRequestCheckViewModel.HasAnnotations = arg.checkRun.Annotations?.Any() ?? false; + pullRequestCheckViewModel.Title = arg.checkRun.Name; + pullRequestCheckViewModel.Description = arg.checkRun.Summary; pullRequestCheckViewModel.Status = checkStatus; - pullRequestCheckViewModel.DetailsUrl = new Uri(model.DetailsUrl); - + pullRequestCheckViewModel.DetailsUrl = new Uri(arg.checkRun.DetailsUrl); return pullRequestCheckViewModel; }) ?? Array.Empty(); return statuses.Concat(checks).OrderBy(model => model.Title); } + /// + /// Initializes a new instance of . + /// + /// The usage tracker. [ImportingConstructor] public PullRequestCheckViewModel(IUsageTracker usageTracker) { @@ -124,16 +138,28 @@ private void DoOpenDetailsUrl() usageTracker.IncrementCounter(expression).Forget(); } + /// public string Title { get; private set; } + /// public string Description { get; private set; } + /// public PullRequestCheckType CheckType { get; private set; } + /// + public string CheckRunId { get; private set; } + + /// + public bool HasAnnotations { get; private set; } + + /// public PullRequestCheckStatus Status{ get; private set; } + /// public Uri DetailsUrl { 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 f968b391c3..ef5d29d7ff 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs @@ -18,14 +18,14 @@ using GitHub.Services; using LibGit2Sharp; using ReactiveUI; +using ReactiveUI.Legacy; using Serilog; using static System.FormattableString; +using ReactiveCommand = ReactiveUI.ReactiveCommand; namespace GitHub.ViewModels.GitHubPane { - /// - /// A view model which displays the details of a pull request. - /// + /// [Export(typeof(IPullRequestDetailViewModel))] [PartCreationPolicy(CreationPolicy.NonShared)] public sealed class PullRequestDetailViewModel : PanePageViewModelBase, IPullRequestDetailViewModel @@ -131,7 +131,10 @@ public PullRequestDetailViewModel( SubscribeOperationError(SyncSubmodules); OpenOnGitHub = ReactiveCommand.Create(DoOpenDetailsUrl); + ShowReview = ReactiveCommand.Create(DoShowReview); + + ShowAnnotations = ReactiveCommand.Create(DoShowAnnotations); } private void DoOpenDetailsUrl() @@ -139,9 +142,7 @@ private void DoOpenDetailsUrl() usageTracker.IncrementCounter(measuresModel => measuresModel.NumberOfPRDetailsOpenInGitHub).Forget(); } - /// - /// Gets the underlying pull request model. - /// + /// public PullRequestDetailModel Model { get { return model; } @@ -159,124 +160,89 @@ private set } } - /// - /// Gets the local repository. - /// + /// public LocalRepositoryModel LocalRepository { get; private set; } - /// - /// Gets the owner of the remote repository that contains the pull request. - /// - /// - /// The remote repository may be different from the local repository if the local - /// repository is a fork and the user is viewing pull requests from the parent repository. - /// + /// public string RemoteRepositoryOwner { get; private set; } - /// - /// Gets the Pull Request number. - /// + /// public int Number { get; private set; } - /// - /// Gets the Pull Request author. - /// + /// public IActorViewModel Author { get { return author; } private set { this.RaiseAndSetIfChanged(ref author, value); } } - /// - /// Gets the session for the pull request. - /// + /// public IPullRequestSession Session { get; private set; } - /// - /// Gets a string describing how to display the pull request's source branch. - /// + /// public string SourceBranchDisplayName { get { return sourceBranchDisplayName; } private set { this.RaiseAndSetIfChanged(ref sourceBranchDisplayName, value); } } - /// - /// Gets a string describing how to display the pull request's target branch. - /// + /// public string TargetBranchDisplayName { get { return targetBranchDisplayName; } private set { this.RaiseAndSetIfChanged(ref targetBranchDisplayName, value); } } - /// - /// Gets a value indicating whether the pull request branch is checked out. - /// + /// public bool IsCheckedOut { get { return isCheckedOut; } private set { this.RaiseAndSetIfChanged(ref isCheckedOut, value); } } - /// - /// Gets a value indicating whether the pull request comes from a fork. - /// + /// public bool IsFromFork { get { return isFromFork; } private set { this.RaiseAndSetIfChanged(ref isFromFork, value); } } - /// - /// Gets the pull request body. - /// + /// public string Body { get { return body; } private set { this.RaiseAndSetIfChanged(ref body, value); } } - /// - /// Gets the state associated with the command. - /// + /// public IPullRequestCheckoutState CheckoutState { get { return checkoutState; } private set { this.RaiseAndSetIfChanged(ref checkoutState, value); } } - /// - /// Gets the state associated with the and commands. - /// + /// public IPullRequestUpdateState UpdateState { get { return updateState; } private set { this.RaiseAndSetIfChanged(ref updateState, value); } } - /// - /// Gets the error message to be displayed in the action area as a result of an error in a - /// git operation. - /// + /// public string OperationError { get { return operationError; } private set { this.RaiseAndSetIfChanged(ref operationError, value); } } - /// - /// Gets the latest pull request review for each user. - /// + /// public IReadOnlyList Reviews { get { return reviews; } private set { this.RaiseAndSetIfChanged(ref reviews, value); } } - /// - /// Gets the pull request's changed files. - /// + /// public IPullRequestFilesViewModel Files { get; } /// @@ -288,50 +254,35 @@ public Uri WebUrl private set { this.RaiseAndSetIfChanged(ref webUrl, value); } } - /// - /// Gets a command that checks out the pull request locally. - /// + /// public ReactiveCommand Checkout { get; } - /// - /// Gets a command that pulls changes to the current branch. - /// + /// public ReactiveCommand Pull { get; } - /// - /// Gets a command that pushes changes from the current branch. - /// + /// public ReactiveCommand Push { get; } - /// - /// Sync submodules for PR branch. - /// + /// public ReactiveCommand SyncSubmodules { get; } - /// - /// Gets a command that opens the pull request on GitHub. - /// + /// public ReactiveCommand OpenOnGitHub { get; } - /// - /// Gets a command that navigates to a pull request review. - /// + /// public ReactiveCommand ShowReview { get; } + /// + public ReactiveCommand ShowAnnotations { get; } + + /// public IReadOnlyList Checks { get { return checks; } private set { this.RaiseAndSetIfChanged(ref checks, value); } } - /// - /// Initializes the view model. - /// - /// The local repository. - /// The connection to the repository host. - /// The pull request's repository owner. - /// The pull request's repository name. - /// The pull request number. + /// public async Task InitializeAsync( LocalRepositoryModel localRepository, IConnection connection, @@ -521,11 +472,7 @@ public override async Task Refresh() } } - /// - /// Gets the full path to a file in the working directory. - /// - /// The file. - /// The full path to the file in the working directory. + /// public string GetLocalFilePath(IPullRequestFileNode file) { return Path.Combine(LocalRepository.LocalPath, file.RelativePath); @@ -651,10 +598,8 @@ async Task DoSyncSubmodules() } } - void DoShowReview(IPullRequestReviewSummaryViewModel item) + void DoShowReview(IPullRequestReviewSummaryViewModel review) { - var review = item; - if (review.State == PullRequestReviewState.Pending) { NavigateTo(Invariant($"{RemoteRepositoryOwner}/{LocalRepository.Name}/pull/{Number}/review/new")); @@ -665,6 +610,11 @@ void DoShowReview(IPullRequestReviewSummaryViewModel item) } } + void DoShowAnnotations(IPullRequestCheckViewModel checkView) + { + NavigateTo(Invariant($"{RemoteRepositoryOwner}/{LocalRepository.Name}/pull/{Number}/checkruns/{checkView.CheckRunId}")); + } + class CheckoutCommandState : IPullRequestCheckoutState { public CheckoutCommandState(string caption, string disabledMessage) diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestFileNode.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestFileNode.cs index f9e1e4e164..374bd131ef 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestFileNode.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestFileNode.cs @@ -13,6 +13,9 @@ namespace GitHub.ViewModels.GitHubPane public class PullRequestFileNode : ReactiveObject, IPullRequestFileNode { int commentCount; + int annotationNoticeCount; + int annotationWarningCount; + int _annotationFailureCount; /// /// Initializes a new instance of the class. @@ -99,5 +102,32 @@ public int CommentCount get { return commentCount; } set { this.RaiseAndSetIfChanged(ref commentCount, value); } } + + /// + /// Gets or sets the number of annotation notices on the file. + /// + public int AnnotationNoticeCount + { + get { return annotationNoticeCount; } + set { this.RaiseAndSetIfChanged(ref annotationNoticeCount, value); } + } + + /// + /// Gets or sets the number of annotation errors on the file. + /// + public int AnnotationWarningCount + { + get { return annotationWarningCount; } + set { this.RaiseAndSetIfChanged(ref annotationWarningCount, value); } + } + + /// + /// Gets or sets the number of annotation failures on the file. + /// + public int AnnotationFailureCount + { + get { return _annotationFailureCount; } + set { this.RaiseAndSetIfChanged(ref _annotationFailureCount, value); } + } } } diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs index f1c51bdf51..6f7ef97e54 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs @@ -65,6 +65,26 @@ public PullRequestFilesViewModel( await editorService.OpenDiff(pullRequestSession, file.RelativePath, thread); } }); + + OpenFirstAnnotationNotice = ReactiveCommand.CreateFromTask( + async file => await OpenFirstAnnotation(editorService, file, CheckAnnotationLevel.Notice)); + + OpenFirstAnnotationWarning = ReactiveCommand.CreateFromTask( + async file => await OpenFirstAnnotation(editorService, file, CheckAnnotationLevel.Warning)); + + OpenFirstAnnotationFailure = ReactiveCommand.CreateFromTask( + async file => await OpenFirstAnnotation(editorService, file, CheckAnnotationLevel.Failure)); + } + + private async Task OpenFirstAnnotation(IPullRequestEditorService editorService, IPullRequestFileNode file, + CheckAnnotationLevel checkAnnotationLevel) + { + var annotationModel = await GetFirstAnnotation(file, checkAnnotationLevel); + + if (annotationModel != null) + { + await editorService.OpenDiff(pullRequestSession, file.RelativePath, annotationModel.HeadSha, annotationModel.EndLine); + } } /// @@ -122,6 +142,18 @@ public async Task InitializeAsync( { subscriptions.Add(file.WhenAnyValue(x => x.InlineCommentThreads) .Subscribe(x => node.CommentCount = CountComments(x, filter))); + + subscriptions.Add(file.WhenAnyValue(x => x.InlineAnnotations) + .Subscribe(x => + { + var noticeCount = x.Count(model => model.AnnotationLevel == CheckAnnotationLevel.Notice); + var warningCount = x.Count(model => model.AnnotationLevel == CheckAnnotationLevel.Warning); + var failureCount = x.Count(model => model.AnnotationLevel == CheckAnnotationLevel.Failure); + + node.AnnotationNoticeCount = noticeCount; + node.AnnotationWarningCount = warningCount; + node.AnnotationFailureCount = failureCount; + })); } var dir = GetDirectory(Path.GetDirectoryName(node.RelativePath), dirs); @@ -148,6 +180,15 @@ public async Task InitializeAsync( /// public ReactiveCommand OpenFirstComment { get; } + /// + public ReactiveCommand OpenFirstAnnotationNotice { get; } + + /// + public ReactiveCommand OpenFirstAnnotationWarning { get; } + + /// + public ReactiveCommand OpenFirstAnnotationFailure { get; } + static int CountComments( IEnumerable thread, Func commentFilter) @@ -200,6 +241,15 @@ async Task GetFirstCommentThread(IPullRequestFileNode return threads.FirstOrDefault(); } + async Task GetFirstAnnotation(IPullRequestFileNode file, + CheckAnnotationLevel annotationLevel) + { + var sessionFile = await pullRequestSession.GetFile(file.RelativePath); + var annotations = sessionFile.InlineAnnotations; + + return annotations.FirstOrDefault(model => model.AnnotationLevel == annotationLevel); + } + /// /// Implements the command. /// diff --git a/src/GitHub.App/ViewModels/InlineAnnotationViewModel.cs b/src/GitHub.App/ViewModels/InlineAnnotationViewModel.cs new file mode 100644 index 0000000000..c395a9abef --- /dev/null +++ b/src/GitHub.App/ViewModels/InlineAnnotationViewModel.cs @@ -0,0 +1,21 @@ +using GitHub.Models; +using GitHub.ViewModels; + +namespace GitHub.ViewModels +{ + /// + public class InlineAnnotationViewModel: IInlineAnnotationViewModel + { + /// + public InlineAnnotationModel Model { get; } + + /// + /// Initializes a . + /// + /// The inline annotation model. + public InlineAnnotationViewModel(InlineAnnotationModel model) + { + Model = model; + } + } +} \ No newline at end of file diff --git a/src/GitHub.App/ViewModels/PullRequestReviewCommentThreadViewModel.cs b/src/GitHub.App/ViewModels/PullRequestReviewCommentThreadViewModel.cs index 43b0d299ed..c821754f8b 100644 --- a/src/GitHub.App/ViewModels/PullRequestReviewCommentThreadViewModel.cs +++ b/src/GitHub.App/ViewModels/PullRequestReviewCommentThreadViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.Composition; using System.Globalization; using System.Linq; @@ -75,8 +76,7 @@ public bool IsNewThread public bool NeedsPush => needsPush.Value; /// - public async Task InitializeAsync( - IPullRequestSession session, + public async Task InitializeAsync(IPullRequestSession session, IPullRequestSessionFile file, IInlineCommentThreadModel thread, bool addPlaceholder) diff --git a/src/GitHub.Exports.Reactive/Models/IPullRequestSessionFile.cs b/src/GitHub.Exports.Reactive/Models/IPullRequestSessionFile.cs index e13eb31a6c..0cd2a87cf0 100644 --- a/src/GitHub.Exports.Reactive/Models/IPullRequestSessionFile.cs +++ b/src/GitHub.Exports.Reactive/Models/IPullRequestSessionFile.cs @@ -54,6 +54,11 @@ public interface IPullRequestSessionFile : INotifyPropertyChanged /// IReadOnlyList InlineCommentThreads { get; } + /// + /// Gets the inline annotations for the file. + /// + IReadOnlyList InlineAnnotations { get; } + /// /// Gets an observable that is raised with a collection of 0-based line numbers when the /// review comments on the file are changed. diff --git a/src/GitHub.Exports.Reactive/Models/InlineAnnotationModel.cs b/src/GitHub.Exports.Reactive/Models/InlineAnnotationModel.cs new file mode 100644 index 0000000000..c102614014 --- /dev/null +++ b/src/GitHub.Exports.Reactive/Models/InlineAnnotationModel.cs @@ -0,0 +1,82 @@ +using GitHub.Extensions; + +namespace GitHub.Models +{ + /// + /// Represents an inline annotation on an . + /// + public class InlineAnnotationModel + { + readonly CheckSuiteModel checkSuite; + readonly CheckRunModel checkRun; + readonly CheckRunAnnotationModel annotation; + + /// + /// Initializes the . + /// + /// The check suite model. + /// The check run model. + /// The annotation model. + public InlineAnnotationModel(CheckSuiteModel checkSuite, CheckRunModel checkRun, + CheckRunAnnotationModel annotation) + { + Guard.ArgumentNotNull(checkRun, nameof(checkRun)); + Guard.ArgumentNotNull(annotation, nameof(annotation)); + Guard.ArgumentNotNull(annotation.AnnotationLevel, nameof(annotation.AnnotationLevel)); + + this.checkSuite = checkSuite; + this.checkRun = checkRun; + this.annotation = annotation; + } + + /// + /// Gets the annotation path. + /// + public string Path => annotation.Path; + + /// + /// Gets the start line of the annotation. + /// + public int StartLine => annotation.StartLine; + + /// + /// Gets the end line of the annotation. + /// + public int EndLine => annotation.EndLine; + + /// + /// Gets the annotation level. + /// + public CheckAnnotationLevel AnnotationLevel => annotation.AnnotationLevel; + + /// + /// Gets the name of the check suite. + /// + public string CheckSuiteName => checkSuite.ApplicationName; + + /// + /// Gets the name of the check run. + /// + public string CheckRunName => checkRun.Name; + + /// + /// Gets the annotation title. + /// + public string Title => annotation.Title; + + /// + /// Gets the annotation message. + /// + public string Message => annotation.Message; + + /// + /// Gets the sha the check run was created on. + /// + public string HeadSha => checkSuite.HeadSha; + + /// + /// Gets the a descriptor for the line(s) reported. + /// + public string LineDescription => $"{StartLine}:{EndLine}"; + } +} \ No newline at end of file diff --git a/src/GitHub.Exports.Reactive/Services/IPullRequestEditorService.cs b/src/GitHub.Exports.Reactive/Services/IPullRequestEditorService.cs index ec5e274422..954f0a6e34 100644 --- a/src/GitHub.Exports.Reactive/Services/IPullRequestEditorService.cs +++ b/src/GitHub.Exports.Reactive/Services/IPullRequestEditorService.cs @@ -37,8 +37,8 @@ public interface IPullRequestEditorService Task OpenDiff(IPullRequestSession session, string relativePath, string headSha = null, bool scrollToFirstDiff = true); /// - /// Opens an diff viewer for a file in a pull request with the specified inline comment - /// thread open. + /// Opens an diff viewer for a file in a pull request with the specified inline review + /// comment thread open. /// /// The pull request session. /// The path to the file, relative to the repository. @@ -46,6 +46,19 @@ public interface IPullRequestEditorService /// The opened diff viewer. Task OpenDiff(IPullRequestSession session, string relativePath, IInlineCommentThreadModel thread); + /// + /// Opens an diff viewer for a file in a pull request with the specified inline review line open. + /// + /// The pull request session. + /// The path to the file, relative to the repository. + /// + /// The commit SHA of the right hand side of the diff. Pass null to compare with the + /// working directory, or "HEAD" to compare with the HEAD commit of the pull request. + /// + /// The line number to open + /// The opened diff viewer. + Task OpenDiff(IPullRequestSession session, string relativePath, string headSha, int fromLine); + /// /// Find the active text view. /// diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestAnnotationItemViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestAnnotationItemViewModel.cs new file mode 100644 index 0000000000..98e60121c4 --- /dev/null +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestAnnotationItemViewModel.cs @@ -0,0 +1,37 @@ +using System.Reactive; +using GitHub.Models; +using ReactiveUI; + +namespace GitHub.ViewModels.GitHubPane +{ + /// + /// The viewmodel for a single annotation item in a list + /// + public interface IPullRequestAnnotationItemViewModel + { + /// + /// Gets the annotation model. + /// + CheckRunAnnotationModel Annotation { get; } + + /// + /// Gets a formatted descriptor of the line(s) the annotation is about. + /// + string LineDescription { get; } + + /// + /// Gets or sets a flag to control the expanded state. + /// + bool IsExpanded { get; set; } + + /// + /// Gets a flag which indicates this annotation item is from a file changed in this pull request. + /// + bool IsFileInPullRequest { get; } + + /// + /// Gets a command which opens the annotation in the diff view. + /// + ReactiveCommand OpenAnnotation { get; } + } +} \ No newline at end of file diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestAnnotationsViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestAnnotationsViewModel.cs new file mode 100644 index 0000000000..9f4ca12e45 --- /dev/null +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestAnnotationsViewModel.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Reactive; +using System.Threading.Tasks; +using GitHub.Models; +using ReactiveUI; +using ReactiveUI.Legacy; + +namespace GitHub.ViewModels.GitHubPane +{ + /// + /// A viewmodel which displays a list of annotations for a pull request's check run. + /// + public interface IPullRequestAnnotationsViewModel : IPanePageViewModel + { + /// + /// Gets the local repository. + /// + LocalRepositoryModel LocalRepository { get; } + + /// + /// Gets the owner of the remote repository that contains the pull request. + /// + /// + /// The remote repository may be different from the local repository if the local + /// repository is a fork and the user is viewing pull requests from the parent repository. + /// + string RemoteRepositoryOwner { get; } + + /// + /// Gets the number of the pull request. + /// + int PullRequestNumber { get; } + + /// + /// Gets the title of the pull request. + /// + string PullRequestTitle { get; } + + /// + /// Gets the id of the check run. + /// + string CheckRunId { get; } + + /// + /// Gets the name of the check run. + /// + string CheckRunName { get; } + + /// + /// Gets a command which navigates to the parent pull request. + /// + ReactiveCommand NavigateToPullRequest { get; } + + /// + /// Name of the Check Suite. + /// + string CheckSuiteName { get; } + + /// + /// Summary of the Check Run + /// + string CheckRunSummary { get; } + + /// + /// Text of the Check Run + /// + string CheckRunText { get; } + + /// + /// Gets a dictionary of annotations by file path. + /// + IReadOnlyDictionary AnnotationsDictionary { get; } + + /// + /// Initializes the view model. + /// + /// The local repository. + /// The connection to the repository host. + /// The pull request's repository owner. + /// The pull request's repository name. + /// The pull request's number. + /// The pull request's check run id. + Task InitializeAsync( + LocalRepositoryModel localRepository, + IConnection connection, + string owner, + string repo, + int pullRequestNumber, + string checkRunId); + } +} \ No newline at end of file diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs index 1a9d658a39..d24a9c7212 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestCheckViewModel.cs @@ -1,6 +1,5 @@ using System; using System.Reactive; -using System.Windows.Media.Imaging; using GitHub.Models; using ReactiveUI; @@ -12,30 +11,44 @@ namespace GitHub.ViewModels.GitHubPane public interface IPullRequestCheckViewModel: IViewModel { /// - /// The title of the Status/Check + /// The title of the Status/Check. /// string Title { get; } /// - /// The description of the Status/Check + /// The description of the Status/Check. /// string Description { get; } /// - /// The status of the Status/Check + /// The status of the Status/Check. /// PullRequestCheckStatus Status { get; } /// - /// The url where more information about the Status/Check can be found + /// The url where more information about the Status/Check can be found. /// Uri DetailsUrl { get; } /// - /// A command that opens the DetailsUrl in a browser + /// A command that opens the DetailsUrl in a browser. /// - ReactiveCommand OpenDetailsUrl { get; } + + /// + /// Gets the type of check run, Status/Check. + /// + PullRequestCheckType CheckType { get; } + + /// + /// Gets the id of the check run. + /// + string CheckRunId { get; } + + /// + /// Gets a flag to show this check run has annotations. + /// + bool HasAnnotations { get; } } public enum PullRequestCheckStatus diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs index c10c6b2376..52384df726 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs @@ -5,6 +5,7 @@ using GitHub.Models; using GitHub.Services; using ReactiveUI; +using ReactiveUI.Legacy; namespace GitHub.ViewModels.GitHubPane { @@ -62,7 +63,7 @@ public interface IPullRequestUpdateState } /// - /// Represents a view model for displaying details of a pull request. + /// A view model which displays the details of a pull request. /// public interface IPullRequestDetailViewModel : IPanePageViewModel, IOpenInBrowser { @@ -165,6 +166,11 @@ public interface IPullRequestDetailViewModel : IPanePageViewModel, IOpenInBrowse /// ReactiveCommand Push { get; } + /// + /// Sync submodules for PR branch. + /// + ReactiveCommand SyncSubmodules { get; } + /// /// Gets a command that opens the pull request on GitHub. /// @@ -176,7 +182,12 @@ public interface IPullRequestDetailViewModel : IPanePageViewModel, IOpenInBrowse ReactiveCommand ShowReview { get; } /// - /// Gets the latest pull request Checks & Statuses + /// Gets a command that navigates to a pull request's check run annotation list. + /// + ReactiveCommand ShowAnnotations { get; } + + /// + /// Gets the latest pull request checks & statuses. /// IReadOnlyList Checks { get; } diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFileNode.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFileNode.cs index 2eaac8c82b..c9d1b55b8e 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFileNode.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFileNode.cs @@ -33,5 +33,20 @@ public interface IPullRequestFileNode : IPullRequestChangeNode /// Gets the number of review comments on the file. /// int CommentCount { get; } + + /// + /// Gets or sets the number of annotation notices on the file. + /// + int AnnotationNoticeCount { get; } + + /// + /// Gets or sets the number of annotation errors on the file. + /// + int AnnotationWarningCount { get; } + + /// + /// Gets or sets the number of annotation failures on the file. + /// + int AnnotationFailureCount { get; } } } \ No newline at end of file diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFilesViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFilesViewModel.cs index 7f2eda69c8..ebef31bf39 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFilesViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestFilesViewModel.cs @@ -51,6 +51,24 @@ public interface IPullRequestFilesViewModel : IViewModel, IDisposable /// ReactiveCommand OpenFirstComment { get; } + /// + /// Gets a command that opens the first annotation notice for a in + /// the diff viewer. + /// + ReactiveCommand OpenFirstAnnotationNotice { get; } + + /// + /// Gets a command that opens the first annotation warning for a in + /// the diff viewer. + /// + ReactiveCommand OpenFirstAnnotationWarning { get; } + + /// + /// Gets a command that opens the first annotation failure for a in + /// the diff viewer. + /// + ReactiveCommand OpenFirstAnnotationFailure { get; } + /// /// Initializes the view model. /// diff --git a/src/GitHub.Exports.Reactive/ViewModels/IInlineAnnotationViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/IInlineAnnotationViewModel.cs new file mode 100644 index 0000000000..1ed533c306 --- /dev/null +++ b/src/GitHub.Exports.Reactive/ViewModels/IInlineAnnotationViewModel.cs @@ -0,0 +1,15 @@ +using GitHub.Models; + +namespace GitHub.ViewModels +{ + /// + /// A view model that represents a single inline annotation. + /// + public interface IInlineAnnotationViewModel + { + /// + /// Gets the inline annotation model. + /// + InlineAnnotationModel Model { get; } + } +} \ No newline at end of file diff --git a/src/GitHub.Exports.Reactive/ViewModels/IPullRequestReviewCommentThreadViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/IPullRequestReviewCommentThreadViewModel.cs index 0a74e78b56..0afeb7ba12 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/IPullRequestReviewCommentThreadViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/IPullRequestReviewCommentThreadViewModel.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using GitHub.Models; using GitHub.Services; @@ -48,10 +49,9 @@ public interface IPullRequestReviewCommentThreadViewModel : ICommentThreadViewMo /// The file that the comment is on. /// The thread. /// - /// Whether to add a placeholder comment at the end of the thread. + /// Whether to add a placeholder comment at the end of the thread. /// - Task InitializeAsync( - IPullRequestSession session, + Task InitializeAsync(IPullRequestSession session, IPullRequestSessionFile file, IInlineCommentThreadModel thread, bool addPlaceholder); @@ -64,8 +64,7 @@ Task InitializeAsync( /// The 0-based line number of the thread. /// The side of the diff. /// Whether to start the placeholder in edit state. - Task InitializeNewAsync( - IPullRequestSession session, + Task InitializeNewAsync(IPullRequestSession session, IPullRequestSessionFile file, int lineNumber, DiffSide side, diff --git a/src/GitHub.Exports/Models/AnnotationModel.cs b/src/GitHub.Exports/Models/AnnotationModel.cs index c986fc8aa3..3f8dc694b6 100644 --- a/src/GitHub.Exports/Models/AnnotationModel.cs +++ b/src/GitHub.Exports/Models/AnnotationModel.cs @@ -5,11 +5,6 @@ /// 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). /// @@ -23,7 +18,7 @@ public class CheckRunAnnotationModel /// /// The path that this annotation was made on. /// - public string Filename { get; set; } + public string Path { get; set; } /// /// The annotation's message. @@ -38,11 +33,6 @@ public class CheckRunAnnotationModel /// /// The annotation's severity level. /// - public CheckAnnotationLevel? AnnotationLevel { get; set; } - - /// - /// Additional information about the annotation. - /// - public string RawDetails { get; set; } + public CheckAnnotationLevel AnnotationLevel { get; set; } } } \ No newline at end of file diff --git a/src/GitHub.Exports/Models/CheckRunModel.cs b/src/GitHub.Exports/Models/CheckRunModel.cs index a25335c8bb..de6ec46d9d 100644 --- a/src/GitHub.Exports/Models/CheckRunModel.cs +++ b/src/GitHub.Exports/Models/CheckRunModel.cs @@ -8,7 +8,14 @@ namespace GitHub.Models /// public class CheckRunModel { - /// The conclusion of the check run. + /// + /// The id of a Check Run. + /// + public string Id { get; set; } + + /// + /// The conclusion of the check run. + /// public CheckConclusionState? Conclusion { get; set; } /// @@ -21,7 +28,14 @@ public class CheckRunModel /// public DateTimeOffset? CompletedAt { get; set; } - /// The name of the check for this check run. + /// + /// The check run's annotations. + /// + public List Annotations { get; set; } + + /// + /// The name of the check for this check run. + /// public string Name { get; set; } /// @@ -33,5 +47,10 @@ public class CheckRunModel /// The summary of a Check Run. /// public string Summary { get; set; } + + /// + /// The detail of a Check Run. + /// + public string Text { get; set; } } } \ No newline at end of file diff --git a/src/GitHub.Exports/Models/CheckSuiteModel.cs b/src/GitHub.Exports/Models/CheckSuiteModel.cs index c6e82cc1be..43ad354910 100644 --- a/src/GitHub.Exports/Models/CheckSuiteModel.cs +++ b/src/GitHub.Exports/Models/CheckSuiteModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace GitHub.Models @@ -8,9 +8,16 @@ namespace GitHub.Models /// public class CheckSuiteModel { + /// + /// The head sha of a Check Suite. + /// + public string HeadSha { get; set; } + /// /// The check runs associated with a check suite. /// public List CheckRuns { get; set; } + + public string ApplicationName { get; set; } } } \ No newline at end of file diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckType.cs b/src/GitHub.Exports/Models/PullRequestCheckType.cs similarity index 67% rename from src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckType.cs rename to src/GitHub.Exports/Models/PullRequestCheckType.cs index f5cc9dbc20..d088320b53 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestCheckType.cs +++ b/src/GitHub.Exports/Models/PullRequestCheckType.cs @@ -1,4 +1,4 @@ -namespace GitHub.ViewModels.GitHubPane +namespace GitHub.Models { public enum PullRequestCheckType { diff --git a/src/GitHub.Exports/Settings/generated/IPackageSettings.cs b/src/GitHub.Exports/Settings/generated/IPackageSettings.cs index fa8d50b008..aec212bed9 100644 --- a/src/GitHub.Exports/Settings/generated/IPackageSettings.cs +++ b/src/GitHub.Exports/Settings/generated/IPackageSettings.cs @@ -1,4 +1,4 @@ -// This is an automatically generated file, based on settings.json and PackageSettingsGen.tt +// This is an automatically generated file, based on settings.json and PackageSettingsGen.tt /* settings.json content: { "settings": [ @@ -51,4 +51,4 @@ public interface IPackageSettings : INotifyPropertyChanged bool HideTeamExplorerWelcomeMessage { get; set; } bool EnableTraceLogging { get; set; } } -} +} \ No newline at end of file diff --git a/src/GitHub.Exports/ViewModels/GitHubPane/IGitHubPaneViewModel.cs b/src/GitHub.Exports/ViewModels/GitHubPane/IGitHubPaneViewModel.cs index ecab58b39d..69eb5c1c2f 100644 --- a/src/GitHub.Exports/ViewModels/GitHubPane/IGitHubPaneViewModel.cs +++ b/src/GitHub.Exports/ViewModels/GitHubPane/IGitHubPaneViewModel.cs @@ -91,6 +91,15 @@ public interface IGitHubPaneViewModel : IViewModel /// The pull rqeuest number. Task ShowPullRequest(string owner, string repo, int number); + /// + /// Shows the details for a pull request's check run in the GitHub pane. + /// + /// The repository owner. + /// The repository name. + /// The pull rqeuest number. + /// The check run id. + Task ShowPullRequestCheckRun(string owner, string repo, int number, string checkRunId); + /// /// Shows the pull requests reviews authored by a user. /// diff --git a/src/GitHub.InlineReviews/Commands/NextInlineCommentCommand.cs b/src/GitHub.InlineReviews/Commands/NextInlineCommentCommand.cs index 42ea92d69a..e6b4c35f50 100644 --- a/src/GitHub.InlineReviews/Commands/NextInlineCommentCommand.cs +++ b/src/GitHub.InlineReviews/Commands/NextInlineCommentCommand.cs @@ -53,7 +53,7 @@ public override Task Execute(InlineCommentNavigationParams parameter) if (tags.Count > 0) { var cursorPoint = GetCursorPoint(textViews[0], parameter); - var next = tags.FirstOrDefault(x => x.Point > cursorPoint) ?? tags.First(); + var next = tags.FirstOrDefault(x => x.Point >= cursorPoint) ?? tags.First(); ShowPeekComments(parameter, next.TextView, next.Tag, textViews); } diff --git a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj index ef3c381721..6938de120a 100644 --- a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj +++ b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj @@ -354,11 +354,11 @@ ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - - ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll + + ..\..\packages\Octokit.GraphQL.0.1.2-beta\lib\netstandard1.1\Octokit.GraphQL.dll - - ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + ..\..\packages\Octokit.GraphQL.0.1.2-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll @@ -441,9 +441,7 @@ PreserveNewest - - - + diff --git a/src/GitHub.InlineReviews/Models/PullRequestSessionFile.cs b/src/GitHub.InlineReviews/Models/PullRequestSessionFile.cs index a38b0d22c7..8cfa924372 100644 --- a/src/GitHub.InlineReviews/Models/PullRequestSessionFile.cs +++ b/src/GitHub.InlineReviews/Models/PullRequestSessionFile.cs @@ -25,6 +25,7 @@ public class PullRequestSessionFile : ReactiveObject, IPullRequestSessionFile IReadOnlyList diff; string commitSha; IReadOnlyList inlineCommentThreads; + IReadOnlyList inlineAnnotations; /// /// Initializes a new instance of the class. @@ -99,6 +100,29 @@ public IReadOnlyList InlineCommentThreads /// public IObservable>> LinesChanged => linesChanged; + /// + public IReadOnlyList InlineAnnotations + { + get + { + return inlineAnnotations; + } + set + { + var lines = (inlineAnnotations ?? Enumerable.Empty())? + .Concat(value ?? Enumerable.Empty()) + .Select(x => Tuple.Create(x.StartLine, DiffSide.Right)) + .Where(x => x.Item1 >= 0) + .Distinct() + .ToList(); + + this.RaisePropertyChanging(); + inlineAnnotations = value; + this.RaisePropertyChanged(); + NotifyLinesChanged(lines); + } + } + /// /// Raises the signal. /// diff --git a/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs index 7a3d9c845d..88d11c3866 100644 --- a/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs +++ b/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs @@ -44,6 +44,20 @@ Task> Diff( string relativePath, byte[] contents); + + + /// + /// Builds a set of annotation models for a file based on a pull request model + /// + /// The pull request session. + /// The relative path to the file. + /// + /// A collection of objects. + /// + IReadOnlyList BuildAnnotations( + PullRequestDetailModel pullRequest, + string relativePath); + /// /// Builds a set of comment thread models for a file based on a pull request model and a diff. /// diff --git a/src/GitHub.InlineReviews/Services/PullRequestSession.cs b/src/GitHub.InlineReviews/Services/PullRequestSession.cs index 88f2995824..29f232edc2 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestSession.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestSession.cs @@ -310,6 +310,7 @@ async Task UpdateFile(PullRequestSessionFile file) file.CommitSha = file.IsTrackingHead ? PullRequest.HeadRefSha : file.CommitSha; file.Diff = await service.Diff(LocalRepository, mergeBaseSha, file.CommitSha, file.RelativePath); file.InlineCommentThreads = service.BuildCommentThreads(PullRequest, file.RelativePath, file.Diff, file.CommitSha); + file.InlineAnnotations = service.BuildAnnotations(PullRequest, file.RelativePath); } void UpdatePendingReview() diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs index 8f8aa08f41..5043b96d8c 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs @@ -8,7 +8,6 @@ using System.Text; using System.Threading.Tasks; using GitHub.Api; -using GitHub.App.Services; using GitHub.Factories; using GitHub.InlineReviews.Models; using GitHub.Models; @@ -18,22 +17,15 @@ using LibGit2Sharp; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Projection; -using Octokit; using Octokit.GraphQL; -using Octokit.GraphQL.Core; using Octokit.GraphQL.Model; using ReactiveUI; 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. #pragma warning disable CS0618 @@ -97,6 +89,23 @@ public virtual async Task> Diff(LocalRepositoryModel re } } + /// + public IReadOnlyList BuildAnnotations( + PullRequestDetailModel pullRequest, + string relativePath) + { + relativePath = relativePath.Replace("\\", "/"); + + return pullRequest.CheckSuites + ?.SelectMany(checkSuite => checkSuite.CheckRuns.Select(checkRun => new { checkSuite, checkRun})) + .SelectMany(arg => + arg.checkRun.Annotations + .Where(annotation => annotation.Path == relativePath) + .Select(annotation => new InlineAnnotationModel(arg.checkSuite, arg.checkRun, annotation))) + .OrderBy(tuple => tuple.StartLine) + .ToArray(); + } + /// public IReadOnlyList BuildCommentThreads( PullRequestDetailModel pullRequest, @@ -357,6 +366,10 @@ public virtual async Task ReadPullRequestDetail(HostAddr result.Statuses = lastCommitModel.Statuses; result.CheckSuites = lastCommitModel.CheckSuites; + foreach (var checkSuite in result.CheckSuites) + { + checkSuite.HeadSha = lastCommitModel.HeadSha; + } result.ChangedFiles = files.Select(file => new PullRequestFileModel { @@ -764,18 +777,32 @@ async Task GetPullRequestLastCommitAdapter(HostAddress addres .PullRequest(Var(nameof(number))).Commits(last: 1).Nodes.Select( commit => new LastCommitAdapter { + HeadSha = commit.Commit.Oid, 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 { + Id = run.Id.Value, Conclusion = run.Conclusion.FromGraphQl(), Status = run.Status.FromGraphQl(), Name = run.Name, DetailsUrl = run.Permalink, Summary = run.Summary, - }).ToList() + Text = run.Text, + Annotations = run.Annotations(null, null, null, null).AllPages() + .Select(annotation => new CheckRunAnnotationModel + { + Title = annotation.Title, + Message = annotation.Message, + Path = annotation.Path, + AnnotationLevel = annotation.AnnotationLevel.Value.FromGraphQl(), + StartLine = annotation.Location.Start.Line, + EndLine = annotation.Location.End.Line, + }).ToList() + }).ToList(), + ApplicationName = suite.App != null ? suite.App.Name : "Private App" }).ToList(), Statuses = commit.Commit.Status .Select(context => @@ -784,7 +811,7 @@ async Task GetPullRequestLastCommitAdapter(HostAddress addres State = statusContext.State.FromGraphQl(), Context = statusContext.Context, TargetUrl = statusContext.TargetUrl, - Description = statusContext.Description, + Description = statusContext.Description }).ToList() ).SingleOrDefault() } @@ -921,6 +948,8 @@ class LastCommitAdapter public List CheckSuites { get; set; } public List Statuses { get; set; } + + public string HeadSha { get; set; } } } } diff --git a/src/GitHub.InlineReviews/Tags/InlineCommentGlyphFactory.cs b/src/GitHub.InlineReviews/Tags/InlineCommentGlyphFactory.cs index 6d5c4ac613..f43c3265ea 100644 --- a/src/GitHub.InlineReviews/Tags/InlineCommentGlyphFactory.cs +++ b/src/GitHub.InlineReviews/Tags/InlineCommentGlyphFactory.cs @@ -56,12 +56,10 @@ static UserControl CreateGlyph(InlineCommentTag tag) { return new AddInlineCommentGlyph(); } - else if (showTag != null) + + if (showTag != null) { - return new ShowInlineCommentGlyph() - { - Opacity = showTag.Thread.IsStale ? 0.5 : 1, - }; + return new ShowInlineCommentGlyph(); } throw new ArgumentException($"Unknown 'InlineCommentTag' type '{tag}'"); diff --git a/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs b/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs index 3891003d63..d3e5d651b3 100644 --- a/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs +++ b/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs @@ -84,24 +84,58 @@ public IEnumerable> GetTags(NormalizedSnapshotSpanCol { var startLine = span.Start.GetContainingLine().LineNumber; var endLine = span.End.GetContainingLine().LineNumber; - var linesWithComments = new BitArray((endLine - startLine) + 1); + var linesWithTags = new BitArray((endLine - startLine) + 1); var spanThreads = file.InlineCommentThreads.Where(x => x.LineNumber >= startLine && - x.LineNumber <= endLine); + x.LineNumber <= endLine) + .ToArray(); - foreach (var thread in spanThreads) + var spanThreadsByLine = spanThreads.ToDictionary(model => model.LineNumber); + + Dictionary spanAnnotationsByLine = null; + if (side == DiffSide.Right) + { + var spanAnnotations = file.InlineAnnotations.Where(x => + x.EndLine - 1 >= startLine && + x.EndLine - 1 <= endLine); + + spanAnnotationsByLine = spanAnnotations + .GroupBy(model => model.EndLine) + .ToDictionary(models => models.Key - 1, models => models.ToArray()); + } + + var lines = spanThreadsByLine.Keys.Union(spanAnnotationsByLine?.Keys ?? Enumerable.Empty()); + foreach (var line in lines) { var snapshot = span.Snapshot; - var line = snapshot.GetLineFromLineNumber(thread.LineNumber); + var snapshotLine = snapshot.GetLineFromLineNumber(line); - if ((side == DiffSide.Left && thread.DiffLineType == DiffChangeType.Delete) || - (side == DiffSide.Right && thread.DiffLineType != DiffChangeType.Delete)) + if (spanThreadsByLine.TryGetValue(line, out var thread)) { - linesWithComments[thread.LineNumber - startLine] = true; + var isThreadDeleteSide = thread.DiffLineType == DiffChangeType.Delete; + var sidesMatch = side == DiffSide.Left && isThreadDeleteSide || side == DiffSide.Right && !isThreadDeleteSide; + if (!sidesMatch) + { + thread = null; + } + } + + InlineAnnotationModel[] annotations = null; + spanAnnotationsByLine?.TryGetValue(line, out annotations); + + if (thread != null || annotations != null) + { + linesWithTags[line - startLine] = true; + + var showInlineTag = new ShowInlineCommentTag(currentSession, line, thread?.DiffLineType ?? DiffChangeType.Add) + { + Thread = thread, + Annotations = annotations + }; result.Add(new TagSpan( - new SnapshotSpan(line.Start, line.End), - new ShowInlineCommentTag(currentSession, thread))); + new SnapshotSpan(snapshotLine.Start, snapshotLine.End), + showInlineTag)); } } @@ -113,7 +147,7 @@ public IEnumerable> GetTags(NormalizedSnapshotSpanCol if (lineNumber >= startLine && lineNumber <= endLine && - !linesWithComments[lineNumber - startLine] + !linesWithTags[lineNumber - startLine] && (side == DiffSide.Right || line.Type == DiffChangeType.Delete)) { var snapshotLine = span.Snapshot.GetLineFromLineNumber(lineNumber); diff --git a/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml b/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml index 77e7386777..1e0add5448 100644 --- a/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml +++ b/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml @@ -9,16 +9,25 @@ - - - + + + + + + + diff --git a/src/GitHub.InlineReviews/Tags/ShowInlineCommentTag.cs b/src/GitHub.InlineReviews/Tags/ShowInlineCommentTag.cs index b1071754cf..ab72d05db3 100644 --- a/src/GitHub.InlineReviews/Tags/ShowInlineCommentTag.cs +++ b/src/GitHub.InlineReviews/Tags/ShowInlineCommentTag.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using GitHub.Extensions; using GitHub.Models; using GitHub.Services; @@ -14,20 +15,21 @@ public class ShowInlineCommentTag : InlineCommentTag /// Initializes a new instance of the class. /// /// The pull request session. - /// A model holding the details of the thread. - public ShowInlineCommentTag( - IPullRequestSession session, - IInlineCommentThreadModel thread) - : base(session, thread.LineNumber, thread.DiffLineType) + /// 0-based index of the inline tag + /// The diff type for the inline comment + public ShowInlineCommentTag(IPullRequestSession session, int lineNumber, DiffChangeType diffLineType) + : base(session, lineNumber, diffLineType) { - Guard.ArgumentNotNull(thread, nameof(thread)); - - Thread = thread; } /// /// Gets a model holding details of the thread at the tagged line. /// - public IInlineCommentThreadModel Thread { get; } + public IInlineCommentThreadModel Thread { get; set; } + + /// + /// Gets a list of models holding details of the annotations at the tagged line. + /// + public IReadOnlyList Annotations { get; set; } } } diff --git a/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml b/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml new file mode 100644 index 0000000000..cf5726b99d --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml.cs b/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml.cs new file mode 100644 index 0000000000..e8b5ac4a68 --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/ShowInlineGlyph.xaml.cs @@ -0,0 +1,14 @@ +using System; +using System.Windows.Controls; + +namespace GitHub.InlineReviews.Tags +{ + public partial class ShowInlineGlyph : UserControl + { + public ShowInlineGlyph() + { + InitializeComponent(); + } + + } +} diff --git a/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs index efaf81971d..50431ece39 100644 --- a/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs @@ -33,12 +33,14 @@ public sealed class InlineCommentPeekViewModel : ReactiveObject, IDisposable IPullRequestSession session; IPullRequestSessionFile file; IPullRequestReviewCommentThreadViewModel thread; + IReadOnlyList annotations; IDisposable fileSubscription; IDisposable sessionSubscription; IDisposable threadSubscription; ITrackingPoint triggerPoint; string relativePath; DiffSide side; + bool availableForComment; /// /// Initializes a new instance of the class. @@ -86,6 +88,21 @@ public InlineCommentPeekViewModel(IInlineCommentPeekService peekService, Observable.Return(previousCommentCommand.Enabled)); } + public bool AvailableForComment + { + get { return availableForComment; } + private set { this.RaiseAndSetIfChanged(ref availableForComment, value); } + } + + /// + /// Gets the annotations displayed. + /// + public IReadOnlyList Annotations + { + get { return annotations; } + private set { this.RaiseAndSetIfChanged(ref annotations, value); } + } + /// /// Gets the thread of comments to display. /// @@ -168,27 +185,39 @@ async Task UpdateThread() Thread = null; threadSubscription?.Dispose(); + Annotations = null; + if (file == null) return; var lineAndLeftBuffer = peekService.GetLineNumber(peekSession, triggerPoint); var lineNumber = lineAndLeftBuffer.Item1; var leftBuffer = lineAndLeftBuffer.Item2; + + AvailableForComment = + file.Diff.Any(chunk => chunk.Lines + .Any(line => line.NewLineNumber == lineNumber)); + var thread = file.InlineCommentThreads?.FirstOrDefault(x => x.LineNumber == lineNumber && ((leftBuffer && x.DiffLineType == DiffChangeType.Delete) || (!leftBuffer && x.DiffLineType != DiffChangeType.Delete))); - var vm = factory.CreateViewModel(); + + Annotations = file.InlineAnnotations?.Where(model => model.EndLine - 1 == lineNumber) + .Select(model => new InlineAnnotationViewModel(model)) + .ToArray(); + + var threadModel = factory.CreateViewModel(); if (thread?.Comments.Count > 0) { - await vm.InitializeAsync(session, file, thread, true); + await threadModel.InitializeAsync(session, file, thread, true); } else { - await vm.InitializeNewAsync(session, file, lineNumber, side, true); + await threadModel.InitializeNewAsync(session, file, lineNumber, side, true); } - Thread = vm; + Thread = threadModel; } async Task SessionChanged(IPullRequestSession pullRequestSession) diff --git a/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml index e8a7a4269a..55cb7fb4af 100644 --- a/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml +++ b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml @@ -3,10 +3,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:ghfvs="https://github.com/github/VisualStudio" xmlns:local="clr-namespace:GitHub.InlineReviews.Views" xmlns:cache="clr-namespace:GitHub.UI.Helpers;assembly=GitHub.UI" xmlns:ui="clr-namespace:GitHub.UI;assembly=GitHub.UI" + xmlns:ghfvs="https://github.com/github/VisualStudio" mc:Ignorable="d" d:DesignHeight="200" d:DesignWidth="500"> @@ -104,9 +104,19 @@ - - - + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml.cs b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml.cs index 77e1511a6d..10cc2c5e06 100644 --- a/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml.cs +++ b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml.cs @@ -15,17 +15,19 @@ public InlineCommentPeekView() InitializeComponent(); desiredHeight = new Subject(); - threadView.LayoutUpdated += ThreadViewLayoutUpdated; + threadView.LayoutUpdated += ChildLayoutUpdated; + annotationsView.LayoutUpdated += ChildLayoutUpdated; threadScroller.PreviewMouseWheel += ScrollViewerUtilities.FixMouseWheelScroll; } public IObservable DesiredHeight => desiredHeight; - void ThreadViewLayoutUpdated(object sender, EventArgs e) + void ChildLayoutUpdated(object sender, EventArgs e) { var otherControlsHeight = ActualHeight - threadScroller.ActualHeight; var threadViewHeight = threadView.DesiredSize.Height + threadView.Margin.Top + threadView.Margin.Bottom; - desiredHeight.OnNext(threadViewHeight + otherControlsHeight); + var annotationsViewHeight = annotationsView.DesiredSize.Height + annotationsView.Margin.Top + annotationsView.Margin.Bottom; + desiredHeight.OnNext(threadViewHeight + annotationsViewHeight + otherControlsHeight); } } } diff --git a/src/GitHub.InlineReviews/packages.config b/src/GitHub.InlineReviews/packages.config index 68889b5182..5b8e94fa19 100644 --- a/src/GitHub.InlineReviews/packages.config +++ b/src/GitHub.InlineReviews/packages.config @@ -36,7 +36,7 @@ - + diff --git a/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml b/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml index 86d1616bb9..ec7257d64b 100644 --- a/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml @@ -2,39 +2,20 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:sampleData="https://github.com/github/VisualStudio" xmlns:local="clr-namespace:GitHub.VisualStudio.Views" - xmlns:ghfvs="https://github.com/github/VisualStudio" - mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> + mc:Ignorable="d" d:DesignHeight="400" d:DesignWidth="500"> - - - - - + - + + diff --git a/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml.cs b/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml.cs index 4fb4fc78d5..f92c491604 100644 --- a/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml.cs +++ b/src/GitHub.VisualStudio.UI/Views/CommentThreadView.xaml.cs @@ -9,7 +9,6 @@ public partial class CommentThreadView : UserControl public CommentThreadView() { InitializeComponent(); - PreviewMouseWheel += ScrollViewerUtilities.FixMouseWheelScroll; } } } diff --git a/src/GitHub.VisualStudio.UI/Views/CommentView.xaml b/src/GitHub.VisualStudio.UI/Views/CommentView.xaml index 527b36c50f..5368bb67f8 100644 --- a/src/GitHub.VisualStudio.UI/Views/CommentView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/CommentView.xaml @@ -37,80 +37,97 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -149,7 +166,7 @@ AcceptsReturn="True" AcceptsTab="True" IsReadOnly="{Binding IsReadOnly}" - Margin="4 0" + Margin="4 0 4 4" Text="{Binding Body, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap" VerticalAlignment="Center" diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestAnnotationsView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestAnnotationsView.xaml new file mode 100644 index 0000000000..f31c2bb315 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestAnnotationsView.xaml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + for + + + # + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestAnnotationsView.xaml.cs b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestAnnotationsView.xaml.cs new file mode 100644 index 0000000000..222b268468 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestAnnotationsView.xaml.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel.Composition; +using System.Windows.Forms; +using GitHub.Exports; +using GitHub.Services; +using GitHub.UI; +using GitHub.ViewModels.GitHubPane; +using ReactiveUI; +using UserControl = System.Windows.Controls.UserControl; + +namespace GitHub.VisualStudio.Views.GitHubPane +{ + [ExportViewFor(typeof(IPullRequestAnnotationsViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public partial class PullRequestAnnotationsView : UserControl + { + public PullRequestAnnotationsView() + { + InitializeComponent(); + } + } +} diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCheckView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCheckView.xaml index d04daeeda1..d67839c532 100644 --- a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCheckView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestCheckView.xaml @@ -24,11 +24,10 @@ - - - + - + + @@ -38,16 +37,21 @@ - - diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestDetailView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestDetailView.xaml index 50119e213a..cdfdae874e 100644 --- a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestDetailView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestDetailView.xaml @@ -252,7 +252,7 @@ - + diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestFilesView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestFilesView.xaml index 23b8e8499b..806e58d2a1 100644 --- a/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestFilesView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/PullRequestFilesView.xaml @@ -6,6 +6,7 @@ xmlns:ghfvs="https://github.com/github/VisualStudio" xmlns:local="clr-namespace:GitHub.VisualStudio.Views.GitHubPane" xmlns:imaging="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.Imaging" + xmlns:catalog="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.ImageCatalog" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" Name="root"> @@ -98,6 +99,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +