diff --git a/lib/Octokit.GraphQL.0.0.2-alpha.nupkg b/lib/Octokit.GraphQL.0.0.2-alpha.nupkg deleted file mode 100644 index 88686b7729..0000000000 Binary files a/lib/Octokit.GraphQL.0.0.2-alpha.nupkg and /dev/null differ diff --git a/lib/Octokit.GraphQL.0.0.4-alpha.nupkg b/lib/Octokit.GraphQL.0.0.4-alpha.nupkg new file mode 100644 index 0000000000..1a994c026d Binary files /dev/null and b/lib/Octokit.GraphQL.0.0.4-alpha.nupkg differ diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 58f63e5ef0..f9128fd0cc 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -51,12 +51,12 @@ ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll True - - ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.dll + + ..\..\packages\Octokit.GraphQL.0.0.4-alpha\lib\netstandard1.1\Octokit.GraphQL.dll True - - ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + ..\..\packages\Octokit.GraphQL.0.0.4-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll True diff --git a/src/GitHub.Api/packages.config b/src/GitHub.Api/packages.config index 6356b0e89c..67e299e22d 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/Factories/ModelServiceFactory.cs b/src/GitHub.App/Factories/ModelServiceFactory.cs index 5ac1d26cdc..a5ef967749 100644 --- a/src/GitHub.App/Factories/ModelServiceFactory.cs +++ b/src/GitHub.App/Factories/ModelServiceFactory.cs @@ -16,7 +16,6 @@ namespace GitHub.Factories public sealed class ModelServiceFactory : IModelServiceFactory, IDisposable { readonly IApiClientFactory apiClientFactory; - readonly IGraphQLClientFactory graphQLClientFactory; readonly IHostCacheFactory hostCacheFactory; readonly IAvatarProvider avatarProvider; readonly Dictionary cache = new Dictionary(); @@ -25,12 +24,10 @@ public sealed class ModelServiceFactory : IModelServiceFactory, IDisposable [ImportingConstructor] public ModelServiceFactory( IApiClientFactory apiClientFactory, - IGraphQLClientFactory graphQLClientFactory, IHostCacheFactory hostCacheFactory, IAvatarProvider avatarProvider) { this.apiClientFactory = apiClientFactory; - this.graphQLClientFactory = graphQLClientFactory; this.hostCacheFactory = hostCacheFactory; this.avatarProvider = avatarProvider; } @@ -47,7 +44,6 @@ public async Task CreateAsync(IConnection connection) { result = new ModelService( await apiClientFactory.Create(connection.HostAddress), - await graphQLClientFactory.CreateConnection(connection.HostAddress), await hostCacheFactory.Create(connection.HostAddress), avatarProvider); result.InsertUser(AccountCacheItem.Create(connection.User)); diff --git a/src/GitHub.App/GitHub.App.csproj b/src/GitHub.App/GitHub.App.csproj index 9011acecaf..56edeb3562 100644 --- a/src/GitHub.App/GitHub.App.csproj +++ b/src/GitHub.App/GitHub.App.csproj @@ -145,12 +145,12 @@ ..\..\packages\Microsoft.VisualStudio.Utilities.14.3.25407\lib\net45\Microsoft.VisualStudio.Utilities.dll True - - ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.dll + + ..\..\packages\Octokit.GraphQL.0.0.4-alpha\lib\netstandard1.1\Octokit.GraphQL.dll True - - ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + ..\..\packages\Octokit.GraphQL.0.0.4-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll True @@ -209,12 +209,10 @@ - - + - @@ -225,6 +223,7 @@ + @@ -256,7 +255,7 @@ - + @@ -268,8 +267,6 @@ - - True True diff --git a/src/GitHub.App/Models/IssueCommentModel.cs b/src/GitHub.App/Models/IssueCommentModel.cs deleted file mode 100644 index 5031aa2bd4..0000000000 --- a/src/GitHub.App/Models/IssueCommentModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace GitHub.Models -{ - public class IssueCommentModel : ICommentModel - { - public int Id { get; set; } - public string NodeId { get; set; } - public string Body { get; set; } - public DateTimeOffset CreatedAt { get; set; } - public IAccount User { get; set; } - } -} diff --git a/src/GitHub.App/Models/PullRequestFileModel.cs b/src/GitHub.App/Models/PullRequestFileModel.cs deleted file mode 100644 index f8a90c0e30..0000000000 --- a/src/GitHub.App/Models/PullRequestFileModel.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace GitHub.Models -{ - public class PullRequestFileModel : IPullRequestFileModel - { - public PullRequestFileModel(string fileName, string sha, PullRequestFileStatus status) - { - FileName = fileName; - Sha = sha; - Status = status; - } - - public string FileName { get; } - public string Sha { get; } - public PullRequestFileStatus Status { get; } - } -} diff --git a/src/GitHub.App/Models/PullRequestModel.cs b/src/GitHub.App/Models/PullRequestModel.cs index 8e9d6439d7..6a560fa2ea 100644 --- a/src/GitHub.App/Models/PullRequestModel.cs +++ b/src/GitHub.App/Models/PullRequestModel.cs @@ -162,32 +162,6 @@ public string Body public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; } public IAccount Author { get; set; } - public IReadOnlyList ChangedFiles { get; set; } = new IPullRequestFileModel[0]; - public IReadOnlyList Comments { get; set; } = new ICommentModel[0]; - - IReadOnlyList reviews = new IPullRequestReviewModel[0]; - public IReadOnlyList Reviews - { - get { return reviews; } - set - { - Guard.ArgumentNotNull(value, nameof(value)); - reviews = value; - this.RaisePropertyChange(); - } - } - - IReadOnlyList reviewComments = new IPullRequestReviewCommentModel[0]; - public IReadOnlyList ReviewComments - { - get { return reviewComments; } - set - { - Guard.ArgumentNotNull(value, nameof(value)); - reviewComments = value; - this.RaisePropertyChange(); - } - } IAccount assignee; public IAccount Assignee diff --git a/src/GitHub.App/Models/PullRequestReviewCommentModel.cs b/src/GitHub.App/Models/PullRequestReviewCommentModel.cs deleted file mode 100644 index 7ae45548e6..0000000000 --- a/src/GitHub.App/Models/PullRequestReviewCommentModel.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace GitHub.Models -{ - public class PullRequestReviewCommentModel : IPullRequestReviewCommentModel - { - public int Id { get; set; } - public string NodeId { get; set; } - public int PullRequestReviewId { get; set; } - public string Path { get; set; } - public int? Position { get; set; } - public int? OriginalPosition { get; set; } - public string CommitId { get; set; } - public string OriginalCommitId { get; set; } - public string DiffHunk { get; set; } - public IAccount User { get; set; } - public string Body { get; set; } - public DateTimeOffset CreatedAt { get; set; } - public bool IsPending { get; set; } - } -} diff --git a/src/GitHub.App/Models/PullRequestReviewModel.cs b/src/GitHub.App/Models/PullRequestReviewModel.cs deleted file mode 100644 index aab24ab076..0000000000 --- a/src/GitHub.App/Models/PullRequestReviewModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace GitHub.Models -{ - public class PullRequestReviewModel : IPullRequestReviewModel - { - public long Id { get; set; } - public string NodeId { get; set; } - public IAccount User { get; set; } - public string Body { get; set; } - public PullRequestReviewState State { get; set; } - public string CommitId { get; set; } - public DateTimeOffset? SubmittedAt { get; set; } - } -} diff --git a/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs index a4bd06b8a0..a174c3e299 100644 --- a/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs @@ -35,13 +35,12 @@ public PullRequestDetailViewModelDesigner() { var repoPath = @"C:\Repo"; - Model = new PullRequestModel(419, - "Error handling/bubbling from viewmodels to views to viewhosts", - new AccountDesigner { Login = "shana", IsUser = true }, - DateTime.Now.Subtract(TimeSpan.FromDays(3))) + Model = new PullRequestDetailModel { - State = PullRequestStateEnum.Open, - CommitCount = 9, + Number = 419, + Title = "Error handling/bubbling from viewmodels to views to viewhosts", + Author = new ActorModel { Login = "shana" }, + UpdatedAt = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(3)), }; SourceBranchDisplayName = "shana/error-handling"; @@ -71,22 +70,22 @@ public PullRequestDetailViewModelDesigner() { new PullRequestReviewSummaryViewModel { - Id = 2, - User = new AccountDesigner { Login = "grokys", IsUser = true }, + Id = "id1", + User = new ActorViewModel { Login = "grokys" }, State = PullRequestReviewState.Pending, FileCommentCount = 0, }, new PullRequestReviewSummaryViewModel { - Id = 1, - User = new AccountDesigner { Login = "jcansdale", IsUser = true }, + Id = "id", + User = new ActorViewModel { Login = "jcansdale" }, State = PullRequestReviewState.Approved, FileCommentCount = 5, }, new PullRequestReviewSummaryViewModel { - Id = 2, - User = new AccountDesigner { Login = "shana", IsUser = true }, + Id = "id3", + User = new ActorViewModel { Login = "shana" }, State = PullRequestReviewState.ChangesRequested, FileCommentCount = 5, }, @@ -98,11 +97,12 @@ public PullRequestDetailViewModelDesigner() Files = new PullRequestFilesViewModelDesigner(); } - public IPullRequestModel Model { get; } + public PullRequestDetailModel Model { get; } public IPullRequestSession Session { get; } public ILocalRepositoryModel LocalRepository { get; } public string RemoteRepositoryOwner { get; } public int Number { get; set; } + public IActorViewModel Author { get; set; } public string SourceBranchDisplayName { get; set; } public string TargetBranchDisplayName { get; set; } public int CommentCount { get; set; } diff --git a/src/GitHub.App/SampleData/PullRequestListViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestListViewModelDesigner.cs index 9b5c512756..796e275901 100644 --- a/src/GitHub.App/SampleData/PullRequestListViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestListViewModelDesigner.cs @@ -18,6 +18,7 @@ public class PullRequestListViewModelDesigner : PanePageViewModelBase, IPullRequ public PullRequestListViewModelDesigner() { var prs = new TrackingCollection(Observable.Empty()); + prs.Add(new PullRequestModel(399, "Let's try doing this differently", new AccountDesigner { Login = "shana", IsUser = true }, DateTimeOffset.Now - TimeSpan.FromDays(1)) @@ -58,7 +59,7 @@ public PullRequestListViewModelDesigner() public IReadOnlyList Repositories { get; } public IRemoteRepositoryModel SelectedRepository { get; set; } - public IPullRequestModel CheckedOutPullRequest { get; set; } + public PullRequestDetailModel CheckedOutPullRequest { get; set; } public ITrackingCollection PullRequests { get; set; } public IPullRequestModel SelectedPullRequest { get; set; } diff --git a/src/GitHub.App/SampleData/PullRequestReviewAuthoringViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestReviewAuthoringViewModelDesigner.cs index a13faba254..15c6e68f57 100644 --- a/src/GitHub.App/SampleData/PullRequestReviewAuthoringViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestReviewAuthoringViewModelDesigner.cs @@ -12,11 +12,13 @@ public class PullRequestReviewAuthoringViewModelDesigner : PanePageViewModelBase { public PullRequestReviewAuthoringViewModelDesigner() { - PullRequestModel = new PullRequestModel( - 419, - "Fix a ton of potential crashers, odd code and redundant calls in ModelService", - new AccountDesigner { Login = "Haacked", IsUser = true }, - DateTimeOffset.Now - TimeSpan.FromDays(2)); + PullRequestModel = new PullRequestDetailModel + { + Number = 419, + Title = "Fix a ton of potential crashers, odd code and redundant calls in ModelService", + Author = new ActorModel { Login = "Haacked" }, + UpdatedAt = DateTimeOffset.Now - TimeSpan.FromDays(2), + }; Files = new PullRequestFilesViewModelDesigner(); @@ -42,10 +44,10 @@ public PullRequestReviewAuthoringViewModelDesigner() public IReadOnlyList FileComments { get; } public IPullRequestFilesViewModel Files { get; } public ILocalRepositoryModel LocalRepository { get; set; } - public IPullRequestReviewModel Model { get; set; } + public PullRequestReviewModel Model { get; set; } public ReactiveCommand NavigateToPullRequest { get; } public string OperationError { get; set; } - public IPullRequestModel PullRequestModel { get; set; } + public PullRequestDetailModel PullRequestModel { get; set; } public string RemoteRepositoryOwner { get; set; } public ReactiveCommand Approve { get; } public ReactiveCommand Comment { get; } diff --git a/src/GitHub.App/SampleData/PullRequestReviewViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestReviewViewModelDesigner.cs index 6e9c3f5686..dfa5963707 100644 --- a/src/GitHub.App/SampleData/PullRequestReviewViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestReviewViewModelDesigner.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using GitHub.Models; using GitHub.ViewModels.GitHubPane; using ReactiveUI; @@ -13,17 +12,19 @@ public class PullRequestReviewViewModelDesigner : PanePageViewModelBase, IPullRe { public PullRequestReviewViewModelDesigner() { - PullRequestModel = new PullRequestModel( - 419, - "Fix a ton of potential crashers, odd code and redundant calls in ModelService", - new AccountDesigner { Login = "Haacked", IsUser = true }, - DateTimeOffset.Now - TimeSpan.FromDays(2)); + PullRequestModel = new PullRequestDetailModel + { + Number = 419, + Title = "Fix a ton of potential crashers, odd code and redundant calls in ModelService", + Author = new ActorModel { Login = "Haacked" }, + UpdatedAt = DateTimeOffset.Now - TimeSpan.FromDays(2), + }; Model = new PullRequestReviewModel { - + SubmittedAt = DateTimeOffset.Now - TimeSpan.FromDays(1), - User = new AccountDesigner { Login = "Haacked", IsUser = true }, + Author = new ActorModel { Login = "Haacked" }, }; Body = @"Just a few comments. I don't feel too strongly about them though. @@ -63,10 +64,10 @@ public PullRequestReviewViewModelDesigner() public bool IsExpanded { get; set; } public bool HasDetails { get; set; } public ILocalRepositoryModel LocalRepository { get; set; } - public IPullRequestReviewModel Model { get; set; } + public PullRequestReviewModel Model { get; set; } public ReactiveCommand NavigateToPullRequest { get; } public IReadOnlyList OutdatedFileComments { get; set; } - public IPullRequestModel PullRequestModel { get; set; } + public PullRequestDetailModel PullRequestModel { get; set; } public string RemoteRepositoryOwner { get; set; } public string StateDisplay { get; set; } } diff --git a/src/GitHub.App/SampleData/PullRequestUserReviewsViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestUserReviewsViewModelDesigner.cs index 6f240a0577..b8a9a4d10b 100644 --- a/src/GitHub.App/SampleData/PullRequestUserReviewsViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/PullRequestUserReviewsViewModelDesigner.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using GitHub.Models; +using GitHub.ViewModels; using GitHub.ViewModels.GitHubPane; using ReactiveUI; @@ -13,22 +14,25 @@ public class PullRequestUserReviewsViewModelDesigner : PanePageViewModelBase, IP { public PullRequestUserReviewsViewModelDesigner() { - User = new AccountDesigner { Login = "Haacked", IsUser = true }; + var userModel = new ActorModel { Login = "Haacked" }; + + User = new ActorViewModel(userModel); PullRequestNumber = 123; PullRequestTitle = "Error handling/bubbling from viewmodels to views to viewhosts"; + Reviews = new[] { new PullRequestReviewViewModelDesigner() { IsExpanded = true, HasDetails = true, - FileComments = new PullRequestReviewFileCommentViewModel[0], + FileComments = new IPullRequestReviewFileCommentViewModel[0], StateDisplay = "approved", Model = new PullRequestReviewModel { State = PullRequestReviewState.Approved, SubmittedAt = DateTimeOffset.Now - TimeSpan.FromDays(1), - User = User, + Author = userModel, }, }, new PullRequestReviewViewModelDesigner() @@ -40,7 +44,7 @@ public PullRequestUserReviewsViewModelDesigner() { State = PullRequestReviewState.ChangesRequested, SubmittedAt = DateTimeOffset.Now - TimeSpan.FromDays(2), - User = User, + Author = userModel, }, }, new PullRequestReviewViewModelDesigner() @@ -52,7 +56,7 @@ public PullRequestUserReviewsViewModelDesigner() { State = PullRequestReviewState.Commented, SubmittedAt = DateTimeOffset.Now - TimeSpan.FromDays(2), - User = User, + Author = userModel, }, } }; @@ -61,7 +65,7 @@ public PullRequestUserReviewsViewModelDesigner() public ILocalRepositoryModel LocalRepository { get; set; } public string RemoteRepositoryOwner { get; set; } public int PullRequestNumber { get; set; } - public IAccount User { get; set; } + public IActorViewModel User { get; set; } public IReadOnlyList Reviews { get; set; } public string PullRequestTitle { get; set; } public ReactiveCommand NavigateToPullRequest { get; } diff --git a/src/GitHub.App/Services/ModelService.cs b/src/GitHub.App/Services/ModelService.cs index 1928ac8cb8..3c165df248 100644 --- a/src/GitHub.App/Services/ModelService.cs +++ b/src/GitHub.App/Services/ModelService.cs @@ -36,16 +36,13 @@ public class ModelService : IModelService readonly IBlobCache hostCache; readonly IAvatarProvider avatarProvider; - readonly Octokit.GraphQL.IConnection graphql; public ModelService( IApiClient apiClient, - Octokit.GraphQL.IConnection graphql, IBlobCache hostCache, IAvatarProvider avatarProvider) { this.ApiClient = apiClient; - this.graphql = graphql; this.hostCache = hostCache; this.avatarProvider = avatarProvider; } @@ -215,33 +212,7 @@ IObservable GetUserFromCache() public IObservable GetPullRequest(string owner, string name, int number) { - return Observable.Defer(() => - { - return hostCache.GetAndRefreshObject(PRPrefix + '|' + number, () => - Observable.CombineLatest( - ApiClient.GetPullRequest(owner, name, number), - ApiClient.GetPullRequestFiles(owner, name, number).ToList(), - ApiClient.GetIssueComments(owner, name, number).ToList(), - GetPullRequestReviews(owner, name, number).ToObservable(), - GetPullRequestReviewComments(owner, name, number).ToObservable(), - (pr, files, comments, reviews, reviewComments) => new - { - PullRequest = pr, - Files = files, - Comments = comments, - Reviews = reviews, - ReviewComments = reviewComments - }) - .Select(x => PullRequestCacheItem.Create( - x.PullRequest, - (IReadOnlyList)x.Files, - (IReadOnlyList)x.Comments, - (IReadOnlyList)x.Reviews, - (IReadOnlyList)x.ReviewComments)), - TimeSpan.Zero, - TimeSpan.FromDays(7)) - .Select(Create); - }); + throw new NotImplementedException(); } public IObservable GetRepository(string owner, string repo) @@ -388,159 +359,6 @@ IObservable> GetOrganizationRepositories(s }); } -#pragma warning disable CS0618 // DatabaseId is marked obsolete by GraphQL but we need it - async Task> GetPullRequestReviews(string owner, string name, int number) - { - string cursor = null; - var result = new List(); - - while (true) - { - var query = new Query() - .Repository(owner, name) - .PullRequest(number) - .Reviews(first: 30, after: cursor) - .Select(x => new - { - x.PageInfo.HasNextPage, - x.PageInfo.EndCursor, - Items = x.Nodes.Select(y => new PullRequestReviewModel - { - Id = y.DatabaseId.Value, - NodeId = y.Id, - Body = y.Body, - CommitId = y.Commit.Oid, - State = FromGraphQL(y.State), - SubmittedAt = y.SubmittedAt, - User = Create(y.Author.Login, y.Author.AvatarUrl(null)) - }).ToList() - }); - - var page = await graphql.Run(query); - result.AddRange(page.Items); - - if (page.HasNextPage) - cursor = page.EndCursor; - else - return result; - } - } - - async Task> GetPullRequestReviewComments(string owner, string name, int number) - { - var result = new List(); - - // Reads a single page of reviews and for each review the first page of review comments. - var query = new Query() - .Repository(owner, name) - .PullRequest(number) - .Reviews(first: 100, after: Var("cursor")) - .Select(x => new - { - x.PageInfo.HasNextPage, - x.PageInfo.EndCursor, - Reviews = x.Nodes.Select(y => new - { - y.Id, - CommentPage = y.Comments(100, null, null, null).Select(z => new - { - z.PageInfo.HasNextPage, - z.PageInfo.EndCursor, - Items = z.Nodes.Select(a => new PullRequestReviewCommentModel - { - Id = a.DatabaseId.Value, - NodeId = a.Id, - Body = a.Body, - CommitId = a.Commit.Oid, - CreatedAt = a.CreatedAt.Value, - DiffHunk = a.DiffHunk, - OriginalCommitId = a.OriginalCommit.Oid, - OriginalPosition = a.OriginalPosition, - Path = a.Path, - Position = a.Position, - PullRequestReviewId = y.DatabaseId.Value, - User = Create(a.Author.Login, a.Author.AvatarUrl(null)), - IsPending = y.State == Octokit.GraphQL.Model.PullRequestReviewState.Pending, - }).ToList(), - }).Single() - }).ToList() - }).Compile(); - - var vars = new Dictionary - { - { "cursor", null } - }; - - // Read all pages of reviews. - while (true) - { - var reviewPage = await graphql.Run(query, vars); - - foreach (var review in reviewPage.Reviews) - { - result.AddRange(review.CommentPage.Items); - - // The the review has >1 page of review comments, read the remaining pages. - if (review.CommentPage.HasNextPage) - { - result.AddRange(await GetPullRequestReviewComments(review.Id, review.CommentPage.EndCursor)); - } - } - - if (reviewPage.HasNextPage) - vars["cursor"] = reviewPage.EndCursor; - else - return result; - } - } - - private async Task> GetPullRequestReviewComments(string reviewId, string commentCursor) - { - var result = new List(); - var query = new Query() - .Node(reviewId) - .Cast() - .Select(x => new - { - CommentPage = x.Comments(100, Var("cursor"), null, null).Select(z => new - { - z.PageInfo.HasNextPage, - z.PageInfo.EndCursor, - Items = z.Nodes.Select(a => new PullRequestReviewCommentModel - { - Id = a.DatabaseId.Value, - NodeId = a.Id, - Body = a.Body, - CommitId = a.Commit.Oid, - CreatedAt = a.CreatedAt.Value, - DiffHunk = a.DiffHunk, - OriginalCommitId = a.OriginalCommit.Oid, - OriginalPosition = a.OriginalPosition, - Path = a.Path, - Position = a.Position, - PullRequestReviewId = x.DatabaseId.Value, - User = Create(a.Author.Login, a.Author.AvatarUrl(null)), - }).ToList(), - }).Single() - }).Compile(); - var vars = new Dictionary - { - { "cursor", commentCursor } - }; - - while (true) - { - var page = await graphql.Run(query, vars); - result.AddRange(page.CommentPage.Items); - - if (page.CommentPage.HasNextPage) - vars["cursor"] = page.CommentPage.EndCursor; - else - return result; - } - } -#pragma warning restore CS0618 // Type or member is obsolete - public IObservable GetBranches(IRepositoryModel repo) { var keyobs = GetUserFromCache() @@ -618,51 +436,13 @@ IPullRequestModel Create(PullRequestCacheItem prCacheItem) Assignee = prCacheItem.Assignee != null ? Create(prCacheItem.Assignee) : null, Base = Create(prCacheItem.Base), Body = prCacheItem.Body ?? string.Empty, - ChangedFiles = prCacheItem.ChangedFiles.Select(x => - (IPullRequestFileModel)new PullRequestFileModel(x.FileName, x.Sha, x.Status)).ToList(), - Comments = prCacheItem.Comments.Select(x => - (ICommentModel)new IssueCommentModel - { - Id = x.Id, - Body = x.Body, - User = Create(x.User), - CreatedAt = x.CreatedAt ?? DateTimeOffset.MinValue, - }).ToList(), - Reviews = prCacheItem.Reviews.Select(x => - (IPullRequestReviewModel)new PullRequestReviewModel - { - Id = x.Id, - NodeId = x.NodeId, - User = Create(x.User), - Body = x.Body, - State = x.State, - CommitId = x.CommitId, - SubmittedAt = x.SubmittedAt, - }).ToList(), - ReviewComments = prCacheItem.ReviewComments.Select(x => - (IPullRequestReviewCommentModel)new PullRequestReviewCommentModel - { - Id = x.Id, - NodeId = x.NodeId, - PullRequestReviewId = x.PullRequestReviewId, - Path = x.Path, - Position = x.Position, - OriginalPosition = x.OriginalPosition, - CommitId = x.CommitId, - OriginalCommitId = x.OriginalCommitId, - DiffHunk = x.DiffHunk, - User = Create(x.User), - Body = x.Body, - CreatedAt = x.CreatedAt, - IsPending = x.IsPending, - }).ToList(), CommentCount = prCacheItem.CommentCount, CommitCount = prCacheItem.CommitCount, CreatedAt = prCacheItem.CreatedAt, Head = Create(prCacheItem.Head), - State = prCacheItem.State.HasValue ? - prCacheItem.State.Value : - prCacheItem.IsOpen.Value ? PullRequestStateEnum.Open : PullRequestStateEnum.Closed, + State = prCacheItem.State.HasValue ? + prCacheItem.State.Value : + prCacheItem.IsOpen.Value ? PullRequestStateEnum.Open : PullRequestStateEnum.Closed, }; } @@ -746,37 +526,12 @@ public class PullRequestCacheItem : CacheItem { public static PullRequestCacheItem Create(PullRequest pr) { - return new PullRequestCacheItem( - pr, - new PullRequestFile[0], - new IssueComment[0], - new IPullRequestReviewModel[0], - new IPullRequestReviewCommentModel[0]); - } - - public static PullRequestCacheItem Create( - PullRequest pr, - IReadOnlyList files, - IReadOnlyList comments, - IReadOnlyList reviews, - IReadOnlyList reviewComments) - { - return new PullRequestCacheItem(pr, files, comments, reviews, reviewComments); + return new PullRequestCacheItem(pr); } public PullRequestCacheItem() {} public PullRequestCacheItem(PullRequest pr) - : this(pr, new PullRequestFile[0], new IssueComment[0], new IPullRequestReviewModel[0], new IPullRequestReviewCommentModel[0]) - { - } - - public PullRequestCacheItem( - PullRequest pr, - IReadOnlyList files, - IReadOnlyList comments, - IReadOnlyList reviews, - IReadOnlyList reviewComments) { Title = pr.Title; Number = pr.Number; @@ -801,10 +556,6 @@ public PullRequestCacheItem(PullRequest pr) CreatedAt = pr.CreatedAt; UpdatedAt = pr.UpdatedAt; Body = pr.Body; - ChangedFiles = files.Select(x => new PullRequestFileCacheItem(x)).ToList(); - Comments = comments.Select(x => new IssueCommentCacheItem(x)).ToList(); - Reviews = reviews.Select(x => new PullRequestReviewCacheItem(x)).ToList(); - ReviewComments = reviewComments.Select(x => new PullRequestReviewCommentCacheItem(x)).ToList(); State = GetState(pr); IsOpen = pr.State == ItemState.Open; Merged = pr.Merged; @@ -823,11 +574,7 @@ public PullRequestCacheItem(PullRequest pr) public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; } public string Body { get; set; } - public IList ChangedFiles { get; set; } = new PullRequestFileCacheItem[0]; - public IList Comments { get; set; } = new IssueCommentCacheItem[0]; - public IList Reviews { get; set; } = new PullRequestReviewCacheItem[0]; - public IList ReviewComments { get; set; } = new PullRequestReviewCommentCacheItem[0]; - + // Nullable for compatibility with old caches. public PullRequestStateEnum? State { get; set; } @@ -852,115 +599,6 @@ static PullRequestStateEnum GetState(PullRequest pullRequest) } } - public class PullRequestFileCacheItem - { - public PullRequestFileCacheItem() - { - } - - public PullRequestFileCacheItem(PullRequestFile file) - { - FileName = file.FileName; - Sha = file.Sha; - Status = (PullRequestFileStatus)Enum.Parse(typeof(PullRequestFileStatus), file.Status, true); - } - - public string FileName { get; set; } - public string Sha { get; set; } - public PullRequestFileStatus Status { get; set; } - } - - public class IssueCommentCacheItem - { - public IssueCommentCacheItem() - { - } - - public IssueCommentCacheItem(IssueComment comment) - { - Id = comment.Id; - User = new AccountCacheItem(comment.User); - Body = comment.Body; - CreatedAt = comment.CreatedAt; - } - - public int Id { get; } - public AccountCacheItem User { get; set; } - public string Body { get; set; } - public DateTimeOffset? CreatedAt { get; set; } - } - - public class PullRequestReviewCacheItem - { - public PullRequestReviewCacheItem() - { - } - - public PullRequestReviewCacheItem(IPullRequestReviewModel review) - { - Id = review.Id; - NodeId = review.NodeId; - User = new AccountCacheItem - { - Login = review.User.Login, - AvatarUrl = review.User.AvatarUrl, - }; - Body = review.Body; - State = review.State; - SubmittedAt = review.SubmittedAt; - } - - public long Id { get; set; } - public string NodeId { get; set; } - public AccountCacheItem User { get; set; } - public string Body { get; set; } - public GitHub.Models.PullRequestReviewState State { get; set; } - public string CommitId { get; set; } - public DateTimeOffset? SubmittedAt { get; set; } - } - - public class PullRequestReviewCommentCacheItem - { - public PullRequestReviewCommentCacheItem() - { - } - - public PullRequestReviewCommentCacheItem(IPullRequestReviewCommentModel comment) - { - Id = comment.Id; - NodeId = comment.NodeId; - PullRequestReviewId = comment.PullRequestReviewId; - Path = comment.Path; - Position = comment.Position; - OriginalPosition = comment.OriginalPosition; - CommitId = comment.CommitId; - OriginalCommitId = comment.OriginalCommitId; - DiffHunk = comment.DiffHunk; - User = new AccountCacheItem - { - Login = comment.User.Login, - AvatarUrl = comment.User.AvatarUrl, - }; - Body = comment.Body; - CreatedAt = comment.CreatedAt; - IsPending = comment.IsPending; - } - - public int Id { get; } - public string NodeId { get; } - public int PullRequestReviewId { get; set; } - public string Path { get; set; } - public int? Position { get; set; } - public int? OriginalPosition { get; set; } - public string CommitId { get; set; } - public string OriginalCommitId { get; set; } - public string DiffHunk { get; set; } - public AccountCacheItem User { get; set; } - public string Body { get; set; } - public DateTimeOffset CreatedAt { get; set; } - public bool IsPending { get; set; } - } - public class GitReferenceCacheItem { public string Ref { get; set; } diff --git a/src/GitHub.App/Services/PullRequestService.cs b/src/GitHub.App/Services/PullRequestService.cs index 45486f15f1..c82bb8f7af 100644 --- a/src/GitHub.App/Services/PullRequestService.cs +++ b/src/GitHub.App/Services/PullRequestService.cs @@ -273,7 +273,7 @@ static async Task ReadLinesAsync(TextReader reader, Action progress) } } - public IObservable Checkout(ILocalRepositoryModel repository, IPullRequestModel pullRequest, string localBranchName) + public IObservable Checkout(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest, string localBranchName) { return Observable.Defer(async () => { @@ -285,7 +285,7 @@ public IObservable Checkout(ILocalRepositoryModel repository, IPullRequest { await gitClient.Checkout(repo, localBranchName); } - else if (repository.CloneUrl.ToRepositoryUrl() == pullRequest.Head.RepositoryCloneUrl.ToRepositoryUrl()) + else if (repository.CloneUrl.Owner == pullRequest.HeadRepositoryOwner) { var remote = await gitClient.GetHttpRemote(repo, "origin"); await gitClient.Fetch(repo, remote.Name); @@ -293,18 +293,18 @@ public IObservable Checkout(ILocalRepositoryModel repository, IPullRequest } else { - var refSpec = $"{pullRequest.Head.Ref}:{localBranchName}"; - var remoteName = await CreateRemote(repo, pullRequest.Head.RepositoryCloneUrl); + var refSpec = $"{pullRequest.HeadRefName}:{localBranchName}"; + var remoteName = await CreateRemote(repo, repository.CloneUrl.WithOwner(pullRequest.HeadRepositoryOwner)); await gitClient.Fetch(repo, remoteName); await gitClient.Fetch(repo, remoteName, new[] { refSpec }); await gitClient.Checkout(repo, localBranchName); - await gitClient.SetTrackingBranch(repo, localBranchName, $"refs/remotes/{remoteName}/{pullRequest.Head.Ref}"); + await gitClient.SetTrackingBranch(repo, localBranchName, $"refs/remotes/{remoteName}/{pullRequest.HeadRefName}"); } // Store the PR number in the branch config with the key "ghfvs-pr". var prConfigKey = $"branch.{localBranchName}.{SettingGHfVSPullRequest}"; - await gitClient.SetConfig(repo, prConfigKey, BuildGHfVSConfigKeyValue(pullRequest)); + await gitClient.SetConfig(repo, prConfigKey, BuildGHfVSConfigKeyValue(pullRequest.BaseRepositoryOwner, pullRequest.Number)); return Observable.Return(Unit.Default); } @@ -350,21 +350,21 @@ public IObservable CalculateHistoryDivergence(ILocalRepos }); } - public async Task GetMergeBase(ILocalRepositoryModel repository, IPullRequestModel pullRequest) + public async Task GetMergeBase(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest) { using (var repo = gitService.GetRepository(repository.LocalPath)) { return await gitClient.GetPullRequestMergeBase( repo, - pullRequest.Base.RepositoryCloneUrl, - pullRequest.Base.Sha, - pullRequest.Head.Sha, - pullRequest.Base.Ref, + repository.CloneUrl.WithOwner(pullRequest.BaseRepositoryOwner), + pullRequest.BaseRefSha, + pullRequest.HeadRefSha, + pullRequest.BaseRefName, pullRequest.Number); } } - public IObservable GetTreeChanges(ILocalRepositoryModel repository, IPullRequestModel pullRequest) + public IObservable GetTreeChanges(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest) { return Observable.Defer(async () => { @@ -373,13 +373,13 @@ public IObservable GetTreeChanges(ILocalRepositoryModel repository, { var remote = await gitClient.GetHttpRemote(repo, "origin"); await gitClient.Fetch(repo, remote.Name); - var changes = await gitClient.Compare(repo, pullRequest.Base.Sha, pullRequest.Head.Sha, detectRenames: true); + var changes = await gitClient.Compare(repo, pullRequest.BaseRefSha, pullRequest.HeadRefSha, detectRenames: true); return Observable.Return(changes); } }); } - public IObservable GetLocalBranches(ILocalRepositoryModel repository, IPullRequestModel pullRequest) + public IObservable GetLocalBranches(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest) { return Observable.Defer(() => { @@ -392,7 +392,7 @@ public IObservable GetLocalBranches(ILocalRepositoryModel repository, I }); } - public IObservable EnsureLocalBranchesAreMarkedAsPullRequests(ILocalRepositoryModel repository, IPullRequestModel pullRequest) + public IObservable EnsureLocalBranchesAreMarkedAsPullRequests(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest) { return Observable.Defer(async () => { @@ -405,7 +405,7 @@ public IObservable EnsureLocalBranchesAreMarkedAsPullRequests(ILocalReposi { if (!await IsBranchMarkedAsPullRequest(repo, branch.Name, pullRequest)) { - await MarkBranchAsPullRequest(repo, branch.Name, pullRequest); + await MarkBranchAsPullRequest(repo, branch.Name, pullRequest.BaseRepositoryOwner, pullRequest.Number); result = true; } } @@ -415,17 +415,12 @@ public IObservable EnsureLocalBranchesAreMarkedAsPullRequests(ILocalReposi }); } - public bool IsPullRequestFromRepository(ILocalRepositoryModel repository, IPullRequestModel pullRequest) + public bool IsPullRequestFromRepository(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest) { - if (pullRequest.Head?.RepositoryCloneUrl != null) - { - return repository.CloneUrl?.ToRepositoryUrl() == pullRequest.Head.RepositoryCloneUrl.ToRepositoryUrl(); - } - - return false; + return pullRequest.HeadRepositoryOwner == repository.CloneUrl.Owner; } - public IObservable SwitchToBranch(ILocalRepositoryModel repository, IPullRequestModel pullRequest) + public IObservable SwitchToBranch(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest) { return Observable.Defer(async () => { @@ -459,7 +454,7 @@ public IObservable SwitchToBranch(ILocalRepositoryModel repository, IPullR } await gitClient.Checkout(repo, branchName); - await MarkBranchAsPullRequest(repo, branchName, pullRequest); + await MarkBranchAsPullRequest(repo, branchName, pullRequest.BaseRepositoryOwner, pullRequest.Number); } } @@ -486,7 +481,7 @@ public IObservable SwitchToBranch(ILocalRepositoryModel repository, IPullR public async Task ExtractToTempFile( ILocalRepositoryModel repository, - IPullRequestModel pullRequest, + PullRequestDetailModel pullRequest, string relativePath, string commitSha, Encoding encoding) @@ -622,15 +617,15 @@ string CreateUniqueRemoteName(IRepository repo, string name) IEnumerable GetLocalBranchesInternal( ILocalRepositoryModel localRepository, IRepository repository, - IPullRequestModel pullRequest) + PullRequestDetailModel pullRequest) { if (IsPullRequestFromRepository(localRepository, pullRequest)) { - return new[] { pullRequest.Head.Ref }; + return new[] { pullRequest.HeadRefName }; } else { - var key = BuildGHfVSConfigKeyValue(pullRequest); + var key = BuildGHfVSConfigKeyValue(pullRequest.BaseRepositoryOwner, pullRequest.Number); return repository.Config .Select(x => new { Branch = BranchCapture.Match(x.Key).Groups["branch"].Value, Value = x.Value }) @@ -639,20 +634,20 @@ string CreateUniqueRemoteName(IRepository repo, string name) } } - async Task IsBranchMarkedAsPullRequest(IRepository repo, string branchName, IPullRequestModel pullRequest) + async Task IsBranchMarkedAsPullRequest(IRepository repo, string branchName, PullRequestDetailModel pullRequest) { var prConfigKey = $"branch.{branchName}.{SettingGHfVSPullRequest}"; var value = ParseGHfVSConfigKeyValue(await gitClient.GetConfig(repo, prConfigKey)); return value != null && - value.Item1 == pullRequest.Base.RepositoryCloneUrl.Owner && + value.Item1 == pullRequest.BaseRepositoryOwner && value.Item2 == pullRequest.Number; } - async Task MarkBranchAsPullRequest(IRepository repo, string branchName, IPullRequestModel pullRequest) + async Task MarkBranchAsPullRequest(IRepository repo, string branchName, string owner, int number) { // Store the PR number in the branch config with the key "ghfvs-pr". var prConfigKey = $"branch.{branchName}.{SettingGHfVSPullRequest}"; - await gitClient.SetConfig(repo, prConfigKey, BuildGHfVSConfigKeyValue(pullRequest)); + await gitClient.SetConfig(repo, prConfigKey, BuildGHfVSConfigKeyValue(owner, number)); } async Task PushAndCreatePR(IModelService modelService, @@ -674,7 +669,7 @@ async Task MarkBranchAsPullRequest(IRepository repo, string branchName, IPullReq await Task.Delay(TimeSpan.FromSeconds(5)); var ret = await modelService.CreatePullRequest(sourceRepository, targetRepository, sourceBranch, targetBranch, title, body); - await MarkBranchAsPullRequest(repo, sourceBranch.Name, ret); + await MarkBranchAsPullRequest(repo, sourceBranch.Name, targetRepository.CloneUrl.Owner, ret.Number); gitExt.RefreshActiveRepositories(); await usageTracker.IncrementCounter(x => x.NumberOfUpstreamPullRequests); return ret; @@ -709,10 +704,9 @@ static string CalculateTempFileName(string relativePath, string commitSha, Encod return Path.Combine(tempDir, tempFileName); } - static string BuildGHfVSConfigKeyValue(IPullRequestModel pullRequest) + static string BuildGHfVSConfigKeyValue(string owner, int number) { - return pullRequest.Base.RepositoryCloneUrl.Owner + '#' + - pullRequest.Number.ToString(CultureInfo.InvariantCulture); + return owner + '#' + number.ToString(CultureInfo.InvariantCulture); } static Tuple ParseGHfVSConfigKeyValue(string value) diff --git a/src/GitHub.App/ViewModels/ActorViewModel.cs b/src/GitHub.App/ViewModels/ActorViewModel.cs new file mode 100644 index 0000000000..5e8807a0bd --- /dev/null +++ b/src/GitHub.App/ViewModels/ActorViewModel.cs @@ -0,0 +1,29 @@ +using System; +using System.Windows.Media.Imaging; +using GitHub.Models; +using GitHub.Services; + +namespace GitHub.ViewModels +{ + public class ActorViewModel : ViewModelBase, IActorViewModel + { + const string DefaultAvatar = "pack://application:,,,/GitHub.App;component/Images/default_user_avatar.png"; + + public ActorViewModel() + { + } + + public ActorViewModel(ActorModel model) + { + Login = model?.Login ?? "[unknown]"; + Avatar = model?.AvatarUrl != null ? + new BitmapImage(new Uri(model.AvatarUrl)) : + AvatarProvider.CreateBitmapImage(DefaultAvatar); + AvatarUrl = model?.AvatarUrl ?? DefaultAvatar; + } + + public BitmapSource Avatar { get; set; } + public string AvatarUrl { get; set; } + public string Login { get; set; } + } +} \ No newline at end of file diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs index 0bea90b7ca..38a1d4b478 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs @@ -39,10 +39,10 @@ public sealed class PullRequestDetailViewModel : PanePageViewModelBase, IPullReq readonly ITeamExplorerContext teamExplorerContext; readonly ISyncSubmodulesCommand syncSubmodulesCommand; IModelService modelService; - IPullRequestModel model; + PullRequestDetailModel model; + IActorViewModel author; string sourceBranchDisplayName; string targetBranchDisplayName; - int commentCount; string body; IReadOnlyList reviews; IPullRequestCheckoutState checkoutState; @@ -127,7 +127,7 @@ public sealed class PullRequestDetailViewModel : PanePageViewModelBase, IPullReq /// /// Gets the underlying pull request model. /// - public IPullRequestModel Model + public PullRequestDetailModel Model { get { return model; } private set @@ -163,6 +163,15 @@ private set /// 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. /// @@ -186,15 +195,6 @@ public string TargetBranchDisplayName private set { this.RaiseAndSetIfChanged(ref targetBranchDisplayName, value); } } - /// - /// Gets the number of comments made on the pull request. - /// - public int CommentCount - { - get { return commentCount; } - private set { this.RaiseAndSetIfChanged(ref commentCount, value); } - } - /// Gets a value indicating whether the pull request branch is checked out. /// public bool IsCheckedOut @@ -331,8 +331,8 @@ public Uri WebUrl Number = number; WebUrl = localRepository.CloneUrl.ToRepositoryUrl(owner).Append("pull/" + number); modelService = await modelServiceFactory.CreateAsync(connection); - - await Refresh(); + Session = await sessionManager.GetSession(owner, repo, number); + await Load(Session.PullRequest); teamExplorerContext.StatusChanged += RefreshIfActive; } catch (Exception ex) @@ -361,20 +361,19 @@ void RefreshIfActive(object sender, EventArgs e) /// Loads the view model from octokit models. /// /// The pull request model. - public async Task Load(IPullRequestModel pullRequest) + public async Task Load(PullRequestDetailModel pullRequest) { try { var firstLoad = (Model == null); Model = pullRequest; - Session = await sessionManager.GetSession(pullRequest); + Author = new ActorViewModel(pullRequest.Author); Title = Resources.PullRequestNavigationItemText + " #" + pullRequest.Number; IsBusy = true; - IsFromFork = !pullRequestsService.IsPullRequestFromRepository(LocalRepository, Model); - SourceBranchDisplayName = GetBranchDisplayName(IsFromFork, pullRequest.Head?.Label); - TargetBranchDisplayName = GetBranchDisplayName(IsFromFork, pullRequest.Base?.Label); - CommentCount = pullRequest.Comments.Count + pullRequest.ReviewComments.Count; + IsFromFork = !pullRequestsService.IsPullRequestFromRepository(LocalRepository, pullRequest); + SourceBranchDisplayName = GetBranchDisplayName(IsFromFork, pullRequest.HeadRepositoryOwner, pullRequest.HeadRefName); + TargetBranchDisplayName = GetBranchDisplayName(IsFromFork, pullRequest.BaseRepositoryOwner, pullRequest.BaseRefName); Body = !string.IsNullOrWhiteSpace(pullRequest.Body) ? pullRequest.Body : Resources.NoDescriptionProvidedMarkdown; Reviews = PullRequestReviewSummaryViewModel.BuildByUser(Session.User, pullRequest).ToList(); @@ -434,7 +433,7 @@ public async Task Load(IPullRequestModel pullRequest) var clean = await pullRequestsService.IsWorkingDirectoryClean(LocalRepository); string disabled = null; - if (pullRequest.Head == null || !pullRequest.Head.RepositoryCloneUrl.IsValidUri) + if (pullRequest.HeadRepositoryOwner == null) { disabled = Resources.SourceRepositoryNoLongerAvailable; } @@ -480,8 +479,8 @@ public override async Task Refresh() Error = null; OperationError = null; IsBusy = true; - var pullRequest = await modelService.GetPullRequest(RemoteRepositoryOwner, LocalRepository.Name, Number); - await Load(pullRequest); + await Session.Refresh(); + await Load(Session.PullRequest); } catch (Exception ex) { @@ -539,11 +538,11 @@ void SubscribeOperationError(ReactiveCommand command) command.IsExecuting.Select(x => x).Subscribe(x => OperationError = null); } - static string GetBranchDisplayName(bool isFromFork, string targetBranchLabel) + static string GetBranchDisplayName(bool isFromFork, string owner, string label) { - if (targetBranchLabel != null) + if (owner != null) { - return isFromFork ? targetBranchLabel : targetBranchLabel.Split(':')[1]; + return isFromFork ? owner + ':' + label : label; } else { diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs index d05f186794..0588b8a1e5 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModel.cs @@ -175,7 +175,7 @@ static PullRequestDirectoryNode GetDirectory(string path, Dictionary canApproveRequestChanges; IReadOnlyList fileComments; @@ -39,22 +37,19 @@ public class PullRequestReviewAuthoringViewModel : PanePageViewModelBase, IPullR public PullRequestReviewAuthoringViewModel( IPullRequestEditorService editorService, IPullRequestSessionManager sessionManager, - IModelServiceFactory modelServiceFactory, IPullRequestFilesViewModel files) { Guard.ArgumentNotNull(editorService, nameof(editorService)); Guard.ArgumentNotNull(sessionManager, nameof(sessionManager)); - Guard.ArgumentNotNull(modelServiceFactory, nameof(modelServiceFactory)); Guard.ArgumentNotNull(files, nameof(files)); this.editorService = editorService; this.sessionManager = sessionManager; - this.modelServiceFactory = modelServiceFactory; canApproveRequestChanges = this.WhenAnyValue( x => x.Model, x => x.PullRequestModel, - (review, pr) => review != null && pr != null && review.User.Login != pr.Author.Login) + (review, pr) => review != null && pr != null && review.Author.Login != pr.Author.Login) .ToProperty(this, x => x.CanApproveRequestChanges); Files = files; @@ -83,14 +78,14 @@ public class PullRequestReviewAuthoringViewModel : PanePageViewModelBase, IPullR public string RemoteRepositoryOwner { get; private set; } /// - public IPullRequestReviewModel Model + public PullRequestReviewModel Model { get { return model; } private set { this.RaiseAndSetIfChanged(ref model, value); } } /// - public IPullRequestModel PullRequestModel + public PullRequestDetailModel PullRequestModel { get { return pullRequestModel; } private set { this.RaiseAndSetIfChanged(ref pullRequestModel, value); } @@ -150,12 +145,8 @@ public IReadOnlyList FileComments { LocalRepository = localRepository; RemoteRepositoryOwner = owner; - modelService = await modelServiceFactory.CreateAsync(connection); - var pullRequest = await modelService.GetPullRequest( - RemoteRepositoryOwner, - LocalRepository.Name, - pullRequestNumber); - await Load(pullRequest); + session = await sessionManager.GetSession(owner, repo, pullRequestNumber); + await Load(session.PullRequest); } finally { @@ -170,11 +161,8 @@ public override async Task Refresh() { Error = null; IsBusy = true; - var pullRequest = await modelService.GetPullRequest( - RemoteRepositoryOwner, - LocalRepository.Name, - PullRequestModel.Number); - await Load(pullRequest); + await session.Refresh(); + await Load(session.PullRequest); } catch (Exception ex) { @@ -185,25 +173,24 @@ public override async Task Refresh() LocalRepository.Name, PullRequestModel.Number, Model.Id, - modelService.ApiClient.HostAddress.Title); + session.LocalRepository.CloneUrl.Host); Error = ex; IsBusy = false; } } - async Task Load(IPullRequestModel pullRequest) + async Task Load(PullRequestDetailModel pullRequest) { try { - session = await sessionManager.GetSession(pullRequest); PullRequestModel = pullRequest; Model = pullRequest.Reviews.FirstOrDefault(x => - x.State == PullRequestReviewState.Pending && x.User.Login == session.User.Login) ?? + x.State == PullRequestReviewState.Pending && x.Author.Login == session.User.Login) ?? new PullRequestReviewModel { Body = string.Empty, - User = session.User, + Author = session.User, State = PullRequestReviewState.Pending, }; @@ -221,16 +208,16 @@ async Task Load(IPullRequestModel pullRequest) bool FilterComments(IInlineCommentThreadModel thread) { - return thread.Comments.Any(x => x.PullRequestReviewId == Model.Id); + return thread.Comments.Any(x => x.Review.Id == Model.Id); } async Task UpdateFileComments() { - var result = new List(); + var result = new List(); - if (Model.Id == 0 && session.PendingReviewId != 0) + if (Model.Id == null && session.PendingReviewId != null) { - ((PullRequestReviewModel)Model).Id = session.PendingReviewId; + Model.Id = session.PendingReviewId; } foreach (var file in await session.GetAllFiles()) @@ -239,12 +226,13 @@ async Task UpdateFileComments() { foreach (var comment in thread.Comments) { - if (comment.PullRequestReviewId == Model.Id) + if (comment.Review.Id == Model.Id) { - result.Add(new PullRequestReviewFileCommentViewModel( + result.Add(new PullRequestReviewCommentViewModel( editorService, session, - comment)); + thread.RelativePath, + comment.Comment)); } } } @@ -281,7 +269,7 @@ async Task DoCancel(object arg) try { - if (Model?.Id != 0) + if (Model?.Id != null) { await session.CancelReview(); } diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewFileCommentViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewCommentViewModel.cs similarity index 78% rename from src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewFileCommentViewModel.cs rename to src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewCommentViewModel.cs index 010a2d480c..c3bb7295d5 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewFileCommentViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewCommentViewModel.cs @@ -12,17 +12,18 @@ namespace GitHub.ViewModels.GitHubPane /// /// A view model for a file comment in a . /// - public class PullRequestReviewFileCommentViewModel : IPullRequestReviewFileCommentViewModel + public class PullRequestReviewCommentViewModel : IPullRequestReviewFileCommentViewModel { readonly IPullRequestEditorService editorService; readonly IPullRequestSession session; - readonly IPullRequestReviewCommentModel model; + readonly PullRequestReviewCommentModel model; IInlineCommentThreadModel thread; - public PullRequestReviewFileCommentViewModel( + public PullRequestReviewCommentViewModel( IPullRequestEditorService editorService, IPullRequestSession session, - IPullRequestReviewCommentModel model) + string relativePath, + PullRequestReviewCommentModel model) { Guard.ArgumentNotNull(editorService, nameof(editorService)); Guard.ArgumentNotNull(session, nameof(session)); @@ -31,6 +32,7 @@ public class PullRequestReviewFileCommentViewModel : IPullRequestReviewFileComme this.editorService = editorService; this.session = session; this.model = model; + RelativePath = relativePath; Open = ReactiveCommand.CreateAsyncTask(DoOpen); } @@ -39,7 +41,7 @@ public class PullRequestReviewFileCommentViewModel : IPullRequestReviewFileComme public string Body => model.Body; /// - public string RelativePath => model.Path; + public string RelativePath { get; set; } /// public ReactiveCommand Open { get; } @@ -50,9 +52,8 @@ async Task DoOpen(object o) { if (thread == null) { - var commit = model.Position.HasValue ? model.CommitId : model.OriginalCommitId; - var file = await session.GetFile(RelativePath, commit); - thread = file.InlineCommentThreads.FirstOrDefault(t => t.Comments.Any(c => c.Id == model.Id)); + var file = await session.GetFile(RelativePath, model.Thread.CommitSha); + thread = file.InlineCommentThreads.FirstOrDefault(t => t.Comments.Any(c => c.Comment.Id == model.Id)); } if (thread != null && thread.LineNumber != -1) diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewSummaryViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewSummaryViewModel.cs index b321b35bbe..e181ade697 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewSummaryViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewSummaryViewModel.cs @@ -12,10 +12,10 @@ namespace GitHub.ViewModels.GitHubPane public class PullRequestReviewSummaryViewModel : IPullRequestReviewSummaryViewModel { /// - public long Id { get; set; } + public string Id { get; set; } /// - public IAccount User { get; set; } + public IActorViewModel User { get; set; } /// public PullRequestReviewState State { get; set; } @@ -36,45 +36,42 @@ public class PullRequestReviewSummaryViewModel : IPullRequestReviewSummaryViewMo /// right of the Pull Request page on GitHub. /// public static IEnumerable BuildByUser( - IAccount currentUser, - IPullRequestModel pullRequest) + ActorModel currentUser, + PullRequestDetailModel pullRequest) { var existing = new Dictionary(); foreach (var review in pullRequest.Reviews.OrderBy(x => x.Id)) { - if (review.State == PullRequestReviewState.Pending && review.User.Login != currentUser.Login) + if (review.State == PullRequestReviewState.Pending && review.Author.Login != currentUser.Login) continue; PullRequestReviewSummaryViewModel previous; - existing.TryGetValue(review.User.Login, out previous); + existing.TryGetValue(review.Author.Login, out previous); var previousPriority = ToPriority(previous); var reviewPriority = ToPriority(review.State); if (reviewPriority >= previousPriority) { - var count = pullRequest.ReviewComments - .Where(x => x.PullRequestReviewId == review.Id) - .Count(); - existing[review.User.Login] = new PullRequestReviewSummaryViewModel + existing[review.Author.Login] = new PullRequestReviewSummaryViewModel { Id = review.Id, - User = review.User, + User = new ActorViewModel(review.Author), State = review.State, - FileCommentCount = count + FileCommentCount = review.Comments.Count, }; } } - var result = existing.Values.OrderBy(x => x.User).AsEnumerable(); + var result = existing.Values.OrderBy(x => x.User.Login).AsEnumerable(); if (!result.Any(x => x.State == PullRequestReviewState.Pending)) { var newReview = new PullRequestReviewSummaryViewModel { State = PullRequestReviewState.Pending, - User = currentUser, + User = new ActorViewModel(currentUser), }; result = result.Concat(new[] { newReview }); } diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewViewModel.cs index f84e2a9d8e..8ff45e8442 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestReviewViewModel.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; using GitHub.Extensions; -using GitHub.Logging; using GitHub.Models; using GitHub.Services; using ReactiveUI; -using Serilog; namespace GitHub.ViewModels.GitHubPane { @@ -21,13 +19,11 @@ public class PullRequestReviewViewModel : ViewModelBase, IPullRequestReviewViewM /// /// The pull request editor service. /// The pull request session. - /// The pull request model. /// The pull request review model. public PullRequestReviewViewModel( IPullRequestEditorService editorService, IPullRequestSession session, - IPullRequestModel pullRequest, - IPullRequestReviewModel model) + PullRequestReviewModel model) { Guard.ArgumentNotNull(editorService, nameof(editorService)); Guard.ArgumentNotNull(session, nameof(session)); @@ -40,16 +36,17 @@ public class PullRequestReviewViewModel : ViewModelBase, IPullRequestReviewViewM var comments = new List(); var outdated = new List(); - foreach (var comment in pullRequest.ReviewComments) + foreach (var comment in model.Comments) { - if (comment.PullRequestReviewId == model.Id) + if (comment.Thread != null) { - var vm = new PullRequestReviewFileCommentViewModel( + var vm = new PullRequestReviewCommentViewModel( editorService, session, + comment.Thread.Path, comment); - if (comment.Position.HasValue) + if (comment.Thread.Position != null) comments.Add(vm); else outdated.Add(vm); @@ -65,7 +62,7 @@ public class PullRequestReviewViewModel : ViewModelBase, IPullRequestReviewViewM } /// - public IPullRequestReviewModel Model { get; } + public PullRequestReviewModel Model { get; } /// public string Body { get; } diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestUserReviewsViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestUserReviewsViewModel.cs index 9bcb3b7905..6f75160792 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestUserReviewsViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestUserReviewsViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; @@ -26,26 +27,22 @@ public class PullRequestUserReviewsViewModel : PanePageViewModelBase, IPullReque readonly IPullRequestEditorService editorService; readonly IPullRequestSessionManager sessionManager; - readonly IModelServiceFactory modelServiceFactory; - IModelService modelService; IPullRequestSession session; - IAccount user; + string login; + IActorViewModel user; string title; IReadOnlyList reviews; [ImportingConstructor] public PullRequestUserReviewsViewModel( IPullRequestEditorService editorService, - IPullRequestSessionManager sessionManager, - IModelServiceFactory modelServiceFactory) + IPullRequestSessionManager sessionManager) { Guard.ArgumentNotNull(editorService, nameof(editorService)); Guard.ArgumentNotNull(sessionManager, nameof(sessionManager)); - Guard.ArgumentNotNull(modelServiceFactory, nameof(modelServiceFactory)); this.editorService = editorService; this.sessionManager = sessionManager; - this.modelServiceFactory = modelServiceFactory; NavigateToPullRequest = ReactiveCommand.Create().OnExecuteCompleted(_ => NavigateTo(Invariant($"{LocalRepository.Owner}/{LocalRepository.Name}/pull/{PullRequestNumber}"))); @@ -60,7 +57,7 @@ public class PullRequestUserReviewsViewModel : PanePageViewModelBase, IPullReque /// public int PullRequestNumber { get; private set; } - public IAccount User + public IActorViewModel User { get { return user; } private set { this.RaiseAndSetIfChanged(ref user, value); } @@ -84,6 +81,7 @@ public string PullRequestTitle public ReactiveCommand NavigateToPullRequest { get; } /// + [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "login")] public async Task InitializeAsync( ILocalRepositoryModel localRepository, IConnection connection, @@ -104,9 +102,9 @@ public string PullRequestTitle LocalRepository = localRepository; RemoteRepositoryOwner = owner; PullRequestNumber = pullRequestNumber; - modelService = await modelServiceFactory.CreateAsync(connection); - User = await modelService.GetUser(login); - await Refresh(); + this.login = login; + session = await sessionManager.GetSession(owner, repo, pullRequestNumber); + await Load(session.PullRequest); } finally { @@ -121,8 +119,8 @@ public override async Task Refresh() { Error = null; IsBusy = true; - var pullRequest = await modelService.GetPullRequest(RemoteRepositoryOwner, LocalRepository.Name, PullRequestNumber); - await Load(User, pullRequest); + await session.Refresh(); + await Load(session.PullRequest); } catch (Exception ex) { @@ -132,21 +130,20 @@ public override async Task Refresh() RemoteRepositoryOwner, LocalRepository.Name, PullRequestNumber, - modelService.ApiClient.HostAddress.Title); + LocalRepository.CloneUrl.Host); Error = ex; IsBusy = false; } } /// - async Task Load(IAccount author, IPullRequestModel pullRequest) + async Task Load(PullRequestDetailModel pullRequest) { IsBusy = true; try { - session = await sessionManager.GetSession(pullRequest); - User = author; + await Task.Delay(0); PullRequestTitle = pullRequest.Title; var reviews = new List(); @@ -154,17 +151,29 @@ async Task Load(IAccount author, IPullRequestModel pullRequest) foreach (var review in pullRequest.Reviews.OrderByDescending(x => x.SubmittedAt)) { - if (review.User.Login == author.Login && - review.State != PullRequestReviewState.Pending) + if (review.Author.Login == login) { - var vm = new PullRequestReviewViewModel(editorService, session, pullRequest, review); - vm.IsExpanded = isFirst; - reviews.Add(vm); - isFirst = false; + if (User == null) + { + User = new ActorViewModel(review.Author); + } + + if (review.State != PullRequestReviewState.Pending) + { + var vm = new PullRequestReviewViewModel(editorService, session, review); + vm.IsExpanded = isFirst; + reviews.Add(vm); + isFirst = false; + } } } Reviews = reviews; + + if (User == null) + { + User = new ActorViewModel(new ActorModel { Login = login }); + } } finally { diff --git a/src/GitHub.App/packages.config b/src/GitHub.App/packages.config index aadd56f421..172480f69c 100644 --- a/src/GitHub.App/packages.config +++ b/src/GitHub.App/packages.config @@ -21,7 +21,7 @@ - + diff --git a/src/GitHub.Exports.Reactive/GitHub.Exports.Reactive.csproj b/src/GitHub.Exports.Reactive/GitHub.Exports.Reactive.csproj index a55d6aa910..a180b28aad 100644 --- a/src/GitHub.Exports.Reactive/GitHub.Exports.Reactive.csproj +++ b/src/GitHub.Exports.Reactive/GitHub.Exports.Reactive.csproj @@ -171,6 +171,7 @@ + @@ -225,6 +226,7 @@ + diff --git a/src/GitHub.Exports.Reactive/Models/IInlineCommentThreadModel.cs b/src/GitHub.Exports.Reactive/Models/IInlineCommentThreadModel.cs index 1036eae959..ed5cd93908 100644 --- a/src/GitHub.Exports.Reactive/Models/IInlineCommentThreadModel.cs +++ b/src/GitHub.Exports.Reactive/Models/IInlineCommentThreadModel.cs @@ -9,9 +9,9 @@ namespace GitHub.Models public interface IInlineCommentThreadModel { /// - /// Gets or sets the comments in the thread. + /// Gets the comments in the thread. /// - IReadOnlyList Comments { get; } + IReadOnlyList Comments { get; } /// /// Gets the last five lines of the thread's diff hunk, in reverse order. diff --git a/src/GitHub.Exports.Reactive/Models/IPullRequestSessionFile.cs b/src/GitHub.Exports.Reactive/Models/IPullRequestSessionFile.cs index 18492e7ed4..e13eb31a6c 100644 --- a/src/GitHub.Exports.Reactive/Models/IPullRequestSessionFile.cs +++ b/src/GitHub.Exports.Reactive/Models/IPullRequestSessionFile.cs @@ -6,8 +6,8 @@ namespace GitHub.Models { public enum DiffSide { - Right, Left, + Right, } /// diff --git a/src/GitHub.Exports.Reactive/Models/InlineCommentModel.cs b/src/GitHub.Exports.Reactive/Models/InlineCommentModel.cs new file mode 100644 index 0000000000..8593b45169 --- /dev/null +++ b/src/GitHub.Exports.Reactive/Models/InlineCommentModel.cs @@ -0,0 +1,26 @@ +using System; + +namespace GitHub.Models +{ + /// + /// Relates a to an + /// . + /// + public class InlineCommentModel + { + /// + /// Gets or sets the thread that the comment appears in. + /// + public IInlineCommentThreadModel Thread { get; set; } + + /// + /// Gets or sets the review that the comment appears in. + /// + public PullRequestReviewModel Review { get; set; } + + /// + /// Gets or sets the comment. + /// + public PullRequestReviewCommentModel Comment { get; set; } + } +} diff --git a/src/GitHub.Exports.Reactive/Services/IPullRequestService.cs b/src/GitHub.Exports.Reactive/Services/IPullRequestService.cs index 7ad69d7866..15d130265b 100644 --- a/src/GitHub.Exports.Reactive/Services/IPullRequestService.cs +++ b/src/GitHub.Exports.Reactive/Services/IPullRequestService.cs @@ -36,7 +36,7 @@ public interface IPullRequestService /// The pull request details. /// The name of the local branch. /// - IObservable Checkout(ILocalRepositoryModel repository, IPullRequestModel pullRequest, string localBranchName); + IObservable Checkout(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest, string localBranchName); /// /// Carries out a pull on the current branch. @@ -71,7 +71,7 @@ public interface IPullRequestService /// The repository. /// The pull request details. /// - IObservable GetLocalBranches(ILocalRepositoryModel repository, IPullRequestModel pullRequest); + IObservable GetLocalBranches(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest); /// /// Ensures that all local branches for the specified pull request are marked as PR branches. @@ -87,7 +87,7 @@ public interface IPullRequestService /// for the specified pull request are indeed marked and returns a value indicating whether any branches /// have had the mark added. /// - IObservable EnsureLocalBranchesAreMarkedAsPullRequests(ILocalRepositoryModel repository, IPullRequestModel pullRequest); + IObservable EnsureLocalBranchesAreMarkedAsPullRequests(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest); /// /// Determines whether the specified pull request is from the specified repository. @@ -95,7 +95,7 @@ public interface IPullRequestService /// The repository. /// The pull request details. /// - bool IsPullRequestFromRepository(ILocalRepositoryModel repository, IPullRequestModel pullRequest); + bool IsPullRequestFromRepository(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest); /// /// Switches to an existing branch for the specified pull request. @@ -103,7 +103,7 @@ public interface IPullRequestService /// The repository. /// The pull request details. /// - IObservable SwitchToBranch(ILocalRepositoryModel repository, IPullRequestModel pullRequest); + IObservable SwitchToBranch(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest); /// /// Gets the history divergence between the current HEAD and the specified pull request. @@ -119,7 +119,7 @@ public interface IPullRequestService /// The repository. /// The pull request details. /// - Task GetMergeBase(ILocalRepositoryModel repository, IPullRequestModel pullRequest); + Task GetMergeBase(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest); /// /// Gets the changes between the pull request base and head. @@ -127,7 +127,7 @@ public interface IPullRequestService /// The repository. /// The pull request details. /// - IObservable GetTreeChanges(ILocalRepositoryModel repository, IPullRequestModel pullRequest); + IObservable GetTreeChanges(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest); /// /// Gets the pull request associated with the current branch. @@ -139,7 +139,7 @@ public interface IPullRequestService /// /// /// This method does not do an API request - it simply checks the mark left in the git - /// config by . + /// config by . /// IObservable> GetPullRequestForCurrentBranch(ILocalRepositoryModel repository); @@ -164,7 +164,7 @@ public interface IPullRequestService /// The path to the temporary file. Task ExtractToTempFile( ILocalRepositoryModel repository, - IPullRequestModel pullRequest, + PullRequestDetailModel pullRequest, string relativePath, string commitSha, Encoding encoding); diff --git a/src/GitHub.Exports.Reactive/Services/IPullRequestSession.cs b/src/GitHub.Exports.Reactive/Services/IPullRequestSession.cs index 422fa15d92..7f1f22e103 100644 --- a/src/GitHub.Exports.Reactive/Services/IPullRequestSession.cs +++ b/src/GitHub.Exports.Reactive/Services/IPullRequestSession.cs @@ -19,12 +19,12 @@ public interface IPullRequestSession /// /// Gets the current user. /// - IAccount User { get; } + ActorModel User { get; } /// /// Gets the pull request. /// - IPullRequestModel PullRequest { get; } + PullRequestDetailModel PullRequest { get; } /// /// Gets an observable that indicates that has been updated. @@ -34,7 +34,7 @@ public interface IPullRequestSession /// pull request model may be updated in-place which will not result in a PropertyChanged /// notification. /// - IObservable PullRequestChanged { get; } + IObservable PullRequestChanged { get; } /// /// Gets the local repository. @@ -60,7 +60,7 @@ public interface IPullRequestSession /// /// Gets the ID of the current pending pull request review for the user. /// - long PendingReviewId { get; } + string PendingReviewId { get; } /// /// Gets all files touched by the pull request. @@ -97,8 +97,7 @@ public interface IPullRequestSession /// The relative path of the file to comment on. /// The diff between the PR head and base. /// The line index in the diff to comment on. - /// A comment model. - Task PostReviewComment( + Task PostReviewComment( string body, string commitId, string path, @@ -109,16 +108,16 @@ public interface IPullRequestSession /// Posts a PR review comment reply. /// /// The comment body. - /// The GraphQL ID of the comment to reply to. - /// A comment model. - Task PostReviewComment( + /// The GraphQL ID of the comment to reply to. + /// + Task PostReviewComment( string body, - string inReplyToNodeId); + string inReplyTo); /// /// Starts a new pending pull request review. /// - Task StartReview(); + Task StartReview(); /// /// Cancels the currently pending review. @@ -134,15 +133,7 @@ public interface IPullRequestSession /// The review body. /// The review event. /// The review model. - Task PostReview(string body, PullRequestReviewEvent e); - - /// - /// Updates the pull request session with a new pull request model in response to a refresh - /// from the server. - /// - /// The new pull request model. - /// A task which completes when the session has completed updating. - Task Update(IPullRequestModel pullRequest); + Task PostReview(string body, PullRequestReviewEvent e); /// /// Deletes a pull request comment. @@ -157,6 +148,11 @@ public interface IPullRequestSession /// The node id of the pull request comment /// The replacement comment body. /// A comment model. - Task EditComment(string commentNodeId, string body); + Task EditComment(string commentNodeId, string body); + + /// + /// Refreshes the pull request session. + /// A task which completes when the session has completed refreshing. + Task Refresh(); } } diff --git a/src/GitHub.Exports.Reactive/Services/IPullRequestSessionManager.cs b/src/GitHub.Exports.Reactive/Services/IPullRequestSessionManager.cs index 2d58852145..de2ee24405 100644 --- a/src/GitHub.Exports.Reactive/Services/IPullRequestSessionManager.cs +++ b/src/GitHub.Exports.Reactive/Services/IPullRequestSessionManager.cs @@ -66,15 +66,13 @@ public interface IPullRequestSessionManager : INotifyPropertyChanged string GetRelativePath(ITextBuffer buffer); /// - /// Gets a pull request session for a pull request that may not be checked out. + /// Gets an for a pull request. /// - /// The pull request model. + /// The repository owner. + /// The repository name. + /// The pull request number. /// An . - /// - /// If the provided pull request model represents the current session then that will be - /// returned. If not, a new pull request session object will be created. - /// - Task GetSession(IPullRequestModel pullRequest); + Task GetSession(string owner, string name, int number); /// /// Gets information about the pull request that a Visual Studio text buffer is a part of. diff --git a/src/GitHub.Exports.Reactive/Services/PullRequestSessionExtensions.cs b/src/GitHub.Exports.Reactive/Services/PullRequestSessionExtensions.cs index 4d8f1dca8f..05fc393f58 100644 --- a/src/GitHub.Exports.Reactive/Services/PullRequestSessionExtensions.cs +++ b/src/GitHub.Exports.Reactive/Services/PullRequestSessionExtensions.cs @@ -18,7 +18,10 @@ public static class PullRequestSessionExtensions public static string GetHeadBranchDisplay(this IPullRequestSession session) { Guard.ArgumentNotNull(session, nameof(session)); - return GetBranchDisplay(session.IsPullRequestFromFork(), session.PullRequest?.Head?.Label); + return GetBranchDisplay( + session.IsPullRequestFromFork(), + session.PullRequest?.HeadRepositoryOwner, + session.PullRequest?.HeadRefName); } /// @@ -30,7 +33,10 @@ public static string GetHeadBranchDisplay(this IPullRequestSession session) public static string GetBaseBranchDisplay(this IPullRequestSession session) { Guard.ArgumentNotNull(session, nameof(session)); - return GetBranchDisplay(session.IsPullRequestFromFork(), session.PullRequest?.Base?.Label); + return GetBranchDisplay( + session.IsPullRequestFromFork(), + session.PullRequest?.BaseRepositoryOwner, + session.PullRequest?.BaseRefName); } /// @@ -42,16 +48,15 @@ public static bool IsPullRequestFromFork(this IPullRequestSession session) { Guard.ArgumentNotNull(session, nameof(session)); - var headUrl = session.PullRequest.Head.RepositoryCloneUrl?.ToRepositoryUrl(); - var localUrl = session.LocalRepository.CloneUrl?.ToRepositoryUrl(); - return headUrl != null && localUrl != null ? headUrl != localUrl : false; + return session.PullRequest != null && + session.PullRequest.HeadRepositoryOwner != session.PullRequest.BaseRepositoryOwner; } - static string GetBranchDisplay(bool fork, string label) + static string GetBranchDisplay(bool fork, string owner, string label) { - if (label != null) + if (owner != null && label != null) { - return fork ? label : label.Split(':').Last(); + return fork ? owner + ':' + label : label; } return "[invalid]"; diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs index 41e20eb3f3..9a11d0dd40 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestDetailViewModel.cs @@ -69,7 +69,7 @@ public interface IPullRequestDetailViewModel : IPanePageViewModel, IOpenInBrowse /// /// Gets the underlying pull request model. /// - IPullRequestModel Model { get; } + PullRequestDetailModel Model { get; } /// /// Gets the session for the pull request. @@ -95,6 +95,11 @@ public interface IPullRequestDetailViewModel : IPanePageViewModel, IOpenInBrowse /// int Number { get; } + /// + /// Gets the Pull Request author. + /// + IActorViewModel Author { get; } + /// /// Gets a string describing how to display the pull request's source branch. /// @@ -105,11 +110,6 @@ public interface IPullRequestDetailViewModel : IPanePageViewModel, IOpenInBrowse /// string TargetBranchDisplayName { get; } - /// - /// Gets the number of comments made on the pull request. - /// - int CommentCount { get; } - /// /// Gets a value indicating whether the pull request branch is checked out. /// diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestListViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestListViewModel.cs index 5a7d91e3b6..f4a2d1b782 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestListViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestListViewModel.cs @@ -33,7 +33,7 @@ public interface IPullRequestListViewModel : ISearchablePageViewModel, IOpenInBr IRemoteRepositoryModel SelectedRepository { get; set; } ITrackingCollection PullRequests { get; } IPullRequestModel SelectedPullRequest { get; } - IPullRequestModel CheckedOutPullRequest { get; } + PullRequestDetailModel CheckedOutPullRequest { get; } IReadOnlyList States { get; set; } PullRequestState SelectedState { get; set; } ObservableCollection Authors { get; } diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewAuthoringViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewAuthoringViewModel.cs index 764027f559..2a76cb5654 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewAuthoringViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewAuthoringViewModel.cs @@ -30,12 +30,12 @@ public interface IPullRequestReviewAuthoringViewModel : IPanePageViewModel, IDis /// /// Gets the underlying pull request review model. /// - IPullRequestReviewModel Model { get; } + PullRequestReviewModel Model { get; } /// /// Gets the underlying pull request model. /// - IPullRequestModel PullRequestModel { get; } + PullRequestDetailModel PullRequestModel { get; } /// /// Gets or sets the body of the pull request review to be submitted. diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewSummaryViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewSummaryViewModel.cs index 178a1d8d0f..c4af548211 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewSummaryViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewSummaryViewModel.cs @@ -10,12 +10,12 @@ public interface IPullRequestReviewSummaryViewModel /// /// Gets the ID of the pull request review. /// - long Id { get; set; } + string Id { get; set; } /// /// Gets the user who submitted the review. /// - IAccount User { get; set; } + IActorViewModel User { get; set; } /// /// Gets the state of the review. diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewViewModel.cs index 5a680e9eee..a211ea5d11 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestReviewViewModel.cs @@ -12,7 +12,7 @@ public interface IPullRequestReviewViewModel : IViewModel /// /// Gets the underlying pull request review model. /// - IPullRequestReviewModel Model { get; } + PullRequestReviewModel Model { get; } /// /// Gets the body of the review. diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestUserReviewsViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestUserReviewsViewModel.cs index df18bf8547..0974ff65aa 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestUserReviewsViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IPullRequestUserReviewsViewModel.cs @@ -42,7 +42,7 @@ public interface IPullRequestUserReviewsViewModel : IPanePageViewModel /// /// Gets the user whose reviews are being shown. /// - IAccount User { get; } + IActorViewModel User { get; } /// /// Gets a command that navigates to the parent pull request in the GitHub pane. diff --git a/src/GitHub.Exports.Reactive/ViewModels/IActorViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/IActorViewModel.cs new file mode 100644 index 0000000000..4f7da2000f --- /dev/null +++ b/src/GitHub.Exports.Reactive/ViewModels/IActorViewModel.cs @@ -0,0 +1,11 @@ +using System.Windows.Media.Imaging; + +namespace GitHub.ViewModels +{ + public interface IActorViewModel : IViewModel + { + BitmapSource Avatar { get; } + string AvatarUrl { get; } + string Login { get; } + } +} \ No newline at end of file diff --git a/src/GitHub.Exports/GitHub.Exports.csproj b/src/GitHub.Exports/GitHub.Exports.csproj index 417643cbeb..f7125a9281 100644 --- a/src/GitHub.Exports/GitHub.Exports.csproj +++ b/src/GitHub.Exports/GitHub.Exports.csproj @@ -164,15 +164,20 @@ + - + - - + + + + + + @@ -208,7 +213,6 @@ - @@ -274,7 +278,6 @@ - diff --git a/src/GitHub.Exports/Models/ActorModel.cs b/src/GitHub.Exports/Models/ActorModel.cs new file mode 100644 index 0000000000..aa387747f5 --- /dev/null +++ b/src/GitHub.Exports/Models/ActorModel.cs @@ -0,0 +1,20 @@ +using System; + +namespace GitHub.Models +{ + /// + /// Represents an actor (a User or a Bot). + /// + public class ActorModel + { + /// + /// Gets or sets the URL of the actor's avatar. + /// + public string AvatarUrl { get; set; } + + /// + /// Gets or sets the actor's login. + /// + public string Login { get; set; } + } +} diff --git a/src/GitHub.Exports/Models/ICommentModel.cs b/src/GitHub.Exports/Models/CommentModel.cs similarity index 64% rename from src/GitHub.Exports/Models/ICommentModel.cs rename to src/GitHub.Exports/Models/CommentModel.cs index 121c7862fd..eb2520b807 100644 --- a/src/GitHub.Exports/Models/ICommentModel.cs +++ b/src/GitHub.Exports/Models/CommentModel.cs @@ -5,31 +5,31 @@ namespace GitHub.Models /// /// An issue or pull request review comment. /// - public interface ICommentModel + public class CommentModel { /// /// Gets the ID of the comment. /// - int Id { get; } - - /// - /// Gets the GraphQL ID of the comment. - /// - string NodeId { get; } + public string Id { get; set; } /// /// Gets the author of the comment. /// - IAccount User { get; } + public ActorModel Author { get; set; } /// /// Gets the body of the comment. /// - string Body { get; } + public string Body { get; set; } /// /// Gets the creation time of the comment. /// - DateTimeOffset CreatedAt { get; } + public DateTimeOffset CreatedAt { get; set; } + + /// + /// Gets the HTTP URL permalink for the comment. + /// + public string Url { get; set; } } } diff --git a/src/GitHub.Exports/Models/IPullRequestFileModel.cs b/src/GitHub.Exports/Models/IPullRequestFileModel.cs deleted file mode 100644 index ad0a0fbabb..0000000000 --- a/src/GitHub.Exports/Models/IPullRequestFileModel.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace GitHub.Models -{ - public enum PullRequestFileStatus - { - Modified, - Added, - Removed, - Renamed, - } - - public interface IPullRequestFileModel - { - string FileName { get; } - string Sha { get; } - PullRequestFileStatus Status { get; } - } -} \ No newline at end of file diff --git a/src/GitHub.Exports/Models/IPullRequestModel.cs b/src/GitHub.Exports/Models/IPullRequestModel.cs index 40996c78dc..8ff71e861c 100644 --- a/src/GitHub.Exports/Models/IPullRequestModel.cs +++ b/src/GitHub.Exports/Models/IPullRequestModel.cs @@ -9,8 +9,8 @@ namespace GitHub.Models public enum PullRequestStateEnum { Open, - Merged, Closed, + Merged, } public interface IPullRequestModel : ICopyable, @@ -31,9 +31,5 @@ public interface IPullRequestModel : ICopyable, DateTimeOffset UpdatedAt { get; } IAccount Author { get; } IAccount Assignee { get; } - IReadOnlyList ChangedFiles { get; } - IReadOnlyList Comments { get; } - IReadOnlyList Reviews { get; set; } - IReadOnlyList ReviewComments { get; set; } } } diff --git a/src/GitHub.Exports/Models/IPullRequestReviewCommentModel.cs b/src/GitHub.Exports/Models/IPullRequestReviewCommentModel.cs deleted file mode 100644 index b2399aa15a..0000000000 --- a/src/GitHub.Exports/Models/IPullRequestReviewCommentModel.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; - -namespace GitHub.Models -{ - /// - /// Represents a comment on a changed file in a pull request. - /// - public interface IPullRequestReviewCommentModel : ICommentModel - { - /// - /// Gets the ID of the related pull request review. - /// - int PullRequestReviewId { get; set; } - - /// - /// The relative path to the file that the comment was made on. - /// - string Path { get; } - - /// - /// The line number in the diff between and - /// that the comment appears on. - /// - int? Position { get; } - - /// - /// The line number in the diff between and - /// that the comment was originally left on. - /// - int? OriginalPosition { get; } - - /// - /// The commit that the comment appears on. - /// - string CommitId { get; } - - /// - /// The commit that the comment was originally left on. - /// - string OriginalCommitId { get; } - - /// - /// The diff hunk used to match the pull request. - /// - string DiffHunk { get; } - - /// - /// Gets a value indicating whether the comment is part of a pending review. - /// - bool IsPending { get; set; } - } -} diff --git a/src/GitHub.Exports/Models/PullRequestDetailModel.cs b/src/GitHub.Exports/Models/PullRequestDetailModel.cs new file mode 100644 index 0000000000..ee6ce03a67 --- /dev/null +++ b/src/GitHub.Exports/Models/PullRequestDetailModel.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; + +namespace GitHub.Models +{ + /// + /// Holds the details of a Pull Request. + /// + public class PullRequestDetailModel + { + /// + /// Gets or sets the GraphQL ID of the pull request. + /// + public string Id { get; set; } + + /// + /// Gets or sets the pull request number. + /// + public int Number { get; set; } + + /// + /// Gets or sets the pull request author. + /// + public ActorModel Author { get; set; } + + /// + /// Gets or sets the pull request title. + /// + public string Title { get; set; } + + /// + /// Gets or sets the pull request state (open, closed, merged). + /// + public PullRequestStateEnum State { get; set; } + + /// + /// Gets or sets the pull request body markdown. + /// + public string Body { get; set; } + + /// + /// Gets or sets the name of the base branch (e.g. "master"). + /// + public string BaseRefName { get; set; } + + /// + /// Gets or sets the SHA of the base branch. + /// + public string BaseRefSha { get; set; } + + /// + /// Gets or sets the owner login of the repository containing the base branch. + /// + public string BaseRepositoryOwner { get; set; } + + /// + /// Gets or sets the name of the head branch (e.g. "feature-branch"). + /// + public string HeadRefName { get; set; } + + /// + /// Gets or sets the SHA of the head branch. + /// + public string HeadRefSha { get; set; } + + /// + /// Gets or sets the owner login of the repository containing the head branch. + /// + public string HeadRepositoryOwner { get; set; } + + /// + /// Gets or sets the date/time at which the pull request was last updated. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// Gets or sets a collection of files changed by the pull request. + /// + public IReadOnlyList ChangedFiles { get; set; } + + /// + /// Gets or sets a collection of pull request reviews. + /// + public IReadOnlyList Reviews { get; set; } + + /// + /// Gets or sets a collection of pull request review comment threads. + /// + /// + /// The collection groups the comments in the various + /// into threads, as such each pull request review comment will appear in both collections. + /// + public IReadOnlyList Threads { get; set; } + } +} diff --git a/src/GitHub.Exports/Models/PullRequestFileModel.cs b/src/GitHub.Exports/Models/PullRequestFileModel.cs new file mode 100644 index 0000000000..773be8577c --- /dev/null +++ b/src/GitHub.Exports/Models/PullRequestFileModel.cs @@ -0,0 +1,51 @@ +using System; + +namespace GitHub.Models +{ + /// + /// Describes the possible values for . + /// + public enum PullRequestFileStatus + { + /// + /// The file was modified in the pull request. + /// + Modified, + + /// + /// The file was added by the pull request. + /// + Added, + + /// + /// The file was removed by the pull request. + /// + Removed, + + /// + /// The file was moved or renamed by the pull request. + /// + Renamed, + } + + /// + /// Holds details of a file changed by a pull request. + /// + public class PullRequestFileModel + { + /// + /// Gets or sets the path to the changed file, relative to the repository. + /// + public string FileName { get; set; } + + /// + /// Gets or sets the SHA of the changed file. + /// + public string Sha { get; set; } + + /// + /// Gets or sets the status of the changed file (modified, added, removed etc). + /// + public PullRequestFileStatus Status { get; set; } + } +} diff --git a/src/GitHub.Exports/Models/PullRequestReviewCommentModel.cs b/src/GitHub.Exports/Models/PullRequestReviewCommentModel.cs new file mode 100644 index 0000000000..ef224242e1 --- /dev/null +++ b/src/GitHub.Exports/Models/PullRequestReviewCommentModel.cs @@ -0,0 +1,15 @@ +using System; + +namespace GitHub.Models +{ + /// + /// Holds details about a pull request review comment. + /// + public class PullRequestReviewCommentModel : CommentModel + { + /// + /// Gets or sets the associated thread that contains the comment. + /// + public PullRequestReviewThreadModel Thread { get; set; } + } +} diff --git a/src/GitHub.Exports/Models/IPullRequestReviewModel.cs b/src/GitHub.Exports/Models/PullRequestReviewModel.cs similarity index 53% rename from src/GitHub.Exports/Models/IPullRequestReviewModel.cs rename to src/GitHub.Exports/Models/PullRequestReviewModel.cs index 4e633ccb68..eb0719b262 100644 --- a/src/GitHub.Exports/Models/IPullRequestReviewModel.cs +++ b/src/GitHub.Exports/Models/PullRequestReviewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace GitHub.Models { @@ -34,43 +35,43 @@ public enum PullRequestReviewState } /// - /// Represents a review of a pull request. + /// Holds details about a pull request review. /// - public interface IPullRequestReviewModel + public class PullRequestReviewModel { /// - /// Gets the ID of the review. + /// Gets or sets the GraphQL ID of the pull request review. /// - long Id { get; } + public string Id { get; set; } /// - /// Gets the GraphQL ID for the review. + /// Gets or sets the author of the pull request review. /// - string NodeId { get; set; } + public ActorModel Author { get; set; } /// - /// Gets the author of the review. + /// Gets or sets the review's body markdown. /// - IAccount User { get; } + public string Body { get; set; } /// - /// Gets the body of the review. + /// Gets or sets the review's state (approved, requested changes, commented etc). /// - string Body { get; } + public PullRequestReviewState State { get; set; } /// - /// Gets the state of the review. + /// Gets or sets the SHA at which the review was left. /// - PullRequestReviewState State { get; } + public string CommitId { get; set; } /// - /// Gets the SHA of the commit that the review was submitted on. + /// Gets or sets the date/time at which the review was submitted. /// - string CommitId { get; } + public DateTimeOffset? SubmittedAt { get; set; } /// - /// Gets the date/time that the review was submitted. + /// Gets or sets the review comments. /// - DateTimeOffset? SubmittedAt { get; } + public IReadOnlyList Comments { get; set; } = Array.Empty(); } } diff --git a/src/GitHub.Exports/Models/PullRequestReviewThreadModel.cs b/src/GitHub.Exports/Models/PullRequestReviewThreadModel.cs new file mode 100644 index 0000000000..76e05823c5 --- /dev/null +++ b/src/GitHub.Exports/Models/PullRequestReviewThreadModel.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; + +namespace GitHub.Models +{ + /// + /// Represents a thread of s. + /// + public class PullRequestReviewThreadModel + { + /// + /// Gets or sets the GraphQL ID of the thread. + /// + public string Id { get; set; } + + /// + /// Gets or sets the path to the file that the thread is on, relative to the repository. + /// + public string Path { get; set; } + + /// + /// Gets or sets the SHA of the commmit that the thread starts on. + /// + public string CommitSha { get; set; } + + /// + /// Gets or sets the diff hunk for the thread. + /// + public string DiffHunk { get; set; } + + /// + /// Gets or sets a value indicating whether the thread is outdated. + /// + public bool IsOutdated { get; set; } + + /// + /// Gets or sets the line position in the diff that the thread starts on. + /// + /// + /// This property reflects the updated for the current + /// . If the thread is outdated, it will return null. + /// + public int? Position { get; set; } + + /// + /// Gets or sets the line position in the diff that the thread was originally started on. + /// + /// + /// This property represents a line in the diff between the + /// and the pull request branch's merge base at which the thread was originally started. + /// + public int OriginalPosition { get; set; } + + /// + /// Gets or sets the SHA of the commmit that the thread was originally started on. + /// + public string OriginalCommitSha { get; set; } + + /// + /// Gets or sets the comments in the thread. + /// + public IReadOnlyList Comments { get; set; } + } +} diff --git a/src/GitHub.Exports/Primitives/UriString.cs b/src/GitHub.Exports/Primitives/UriString.cs index 643e46e8c4..46ac6b0d0e 100644 --- a/src/GitHub.Exports/Primitives/UriString.cs +++ b/src/GitHub.Exports/Primitives/UriString.cs @@ -216,6 +216,13 @@ public override string ToString() return Value; } + /// + /// Makes a copy of the URI with the specified owner. + /// + /// The owner. + /// A new . + public UriString WithOwner(string owner) => ToUriString(ToRepositoryUrl(owner)); + protected UriString(SerializationInfo info, StreamingContext context) : this(GetSerializedValue(info)) { diff --git a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj index 5f35f609f4..30b44930d6 100644 --- a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj +++ b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj @@ -367,12 +367,12 @@ ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll True - - ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.dll + + ..\..\packages\Octokit.GraphQL.0.0.4-alpha\lib\netstandard1.1\Octokit.GraphQL.dll True - - ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + ..\..\packages\Octokit.GraphQL.0.0.4-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll True diff --git a/src/GitHub.InlineReviews/Models/InlineCommentThreadModel.cs b/src/GitHub.InlineReviews/Models/InlineCommentThreadModel.cs index 4619d3c545..8ead806ddb 100644 --- a/src/GitHub.InlineReviews/Models/InlineCommentThreadModel.cs +++ b/src/GitHub.InlineReviews/Models/InlineCommentThreadModel.cs @@ -30,7 +30,7 @@ class InlineCommentThreadModel : ReactiveObject, IInlineCommentThreadModel string relativePath, string commitSha, IList diffMatch, - IEnumerable comments) + IEnumerable comments) { Guard.ArgumentNotNull(relativePath, nameof(relativePath)); Guard.ArgumentNotNull(commitSha, nameof(commitSha)); @@ -41,10 +41,15 @@ class InlineCommentThreadModel : ReactiveObject, IInlineCommentThreadModel DiffLineType = diffMatch[0].Type; CommitSha = commitSha; RelativePath = relativePath; + + foreach (var comment in comments) + { + comment.Thread = this; + } } /// - public IReadOnlyList Comments { get; } + public IReadOnlyList Comments { get; } /// public IList DiffMatch { get; } diff --git a/src/GitHub.InlineReviews/SampleData/CommentThreadViewModelDesigner.cs b/src/GitHub.InlineReviews/SampleData/CommentThreadViewModelDesigner.cs index 11432ac83f..eac1dca542 100644 --- a/src/GitHub.InlineReviews/SampleData/CommentThreadViewModelDesigner.cs +++ b/src/GitHub.InlineReviews/SampleData/CommentThreadViewModelDesigner.cs @@ -1,9 +1,10 @@ using System; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; +using System.Reactive; using GitHub.InlineReviews.ViewModels; using GitHub.Models; -using GitHub.SampleData; +using GitHub.ViewModels; using ReactiveUI; namespace GitHub.InlineReviews.SampleData @@ -14,16 +15,11 @@ class CommentThreadViewModelDesigner : ICommentThreadViewModel public ObservableCollection Comments { get; } = new ObservableCollection(); - public IAccount CurrentUser { get; set; } - = new AccountDesigner { Login = "shana", IsUser = true }; + public IActorViewModel CurrentUser { get; set; } + = new ActorViewModel { Login = "shana" }; - public ReactiveCommand PostComment { get; } - public ReactiveCommand EditComment { get; } - public ReactiveCommand DeleteComment { get; } - - public Uri GetCommentUrl(int id) - { - throw new NotImplementedException(); - } + public ReactiveCommand PostComment { get; } + public ReactiveCommand EditComment { get; } + public ReactiveCommand DeleteComment { get; } } } diff --git a/src/GitHub.InlineReviews/SampleData/CommentViewModelDesigner.cs b/src/GitHub.InlineReviews/SampleData/CommentViewModelDesigner.cs index 4c735d5111..dca3732639 100644 --- a/src/GitHub.InlineReviews/SampleData/CommentViewModelDesigner.cs +++ b/src/GitHub.InlineReviews/SampleData/CommentViewModelDesigner.cs @@ -2,10 +2,8 @@ using System.Reactive; using System.Diagnostics.CodeAnalysis; using GitHub.InlineReviews.ViewModels; -using GitHub.Models; -using GitHub.SampleData; -using GitHub.UI; using ReactiveUI; +using GitHub.ViewModels; namespace GitHub.InlineReviews.SampleData { @@ -14,11 +12,10 @@ class CommentViewModelDesigner : ReactiveObject, ICommentViewModel { public CommentViewModelDesigner() { - User = new AccountDesigner { Login = "shana", IsUser = true }; + Author = new ActorViewModel { Login = "shana" }; } - public int Id { get; set; } - public string NodeId { get; set; } + public string Id { get; set; } public string Body { get; set; } public string ErrorMessage { get; set; } public CommentEditState EditState { get; set; } @@ -27,7 +24,8 @@ public CommentViewModelDesigner() public bool CanDelete { get; } = true; public ICommentThreadViewModel Thread { get; } public DateTimeOffset UpdatedAt => DateTime.Now.Subtract(TimeSpan.FromDays(3)); - public IAccount User { get; set; } + public IActorViewModel Author { get; set; } + public Uri WebUrl { get; } public ReactiveCommand BeginEdit { get; } public ReactiveCommand CancelEdit { get; } diff --git a/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs index 6f0d7ce542..d816271980 100644 --- a/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs +++ b/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs @@ -3,6 +3,7 @@ using System.Reactive.Subjects; using System.Threading.Tasks; using GitHub.Models; +using GitHub.Primitives; using Microsoft.VisualStudio.Text; using Octokit; @@ -54,7 +55,7 @@ public interface IPullRequestSessionService /// A collection of objects with updated line numbers. /// IReadOnlyList BuildCommentThreads( - IPullRequestModel pullRequest, + PullRequestDetailModel pullRequest, string relativePath, IReadOnlyList diff, string headSha); @@ -134,6 +135,26 @@ public interface IPullRequestSessionService /// Task ReadFileAsync(string path); + /// + /// Reads a for a specified pull request. + /// + /// The host address. + /// The repository owner. + /// The repository name. + /// The pull request number. + /// A task returning the pull request model. + Task ReadPullRequestDetail(HostAddress address, string owner, string name, int number); + + /// + /// Reads the current viewer for the specified address.. + /// + /// The host address. + /// A task returning the viewer. + /// + /// A "Viewer" is the GraphQL term for the currently authenticated user. + /// + Task ReadViewer(HostAddress address); + /// /// Find the merge base for a pull request. /// @@ -142,7 +163,7 @@ public interface IPullRequestSessionService /// /// The merge base SHA for the PR. /// - Task GetPullRequestMergeBase(ILocalRepositoryModel repository, IPullRequestModel pullRequest); + Task GetPullRequestMergeBase(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest); /// /// Gets the GraphQL ID for a pull request. @@ -176,10 +197,9 @@ public interface IPullRequestSessionService /// The local repository. /// The user posting the review. /// The GraphQL ID of the pull request. - /// - Task CreatePendingReview( + /// The updated state of the pull request. + Task CreatePendingReview( ILocalRepositoryModel localRepository, - IAccount user, string pullRequestId); /// @@ -187,7 +207,8 @@ public interface IPullRequestSessionService /// /// The local repository. /// The GraphQL ID of the review. - Task CancelPendingReview( + /// The updated state of the pull request. + Task CancelPendingReview( ILocalRepositoryModel localRepository, string reviewId); @@ -195,17 +216,14 @@ public interface IPullRequestSessionService /// Posts PR review with no comments. /// /// The local repository. - /// The owner of the repository fork to post to. - /// The user posting the review. - /// The pull request number. + /// The GraphQL ID of the pull request. /// The SHA of the commit being reviewed. /// The review body. /// The review event. - Task PostReview( + /// The updated state of the pull request. + Task PostReview( ILocalRepositoryModel localRepository, - string remoteRepositoryOwner, - IAccount user, - int number, + string pullRequestId, string commitId, string body, PullRequestReviewEvent e); @@ -214,13 +232,12 @@ public interface IPullRequestSessionService /// Submits a pending PR review. /// /// The local repository. - /// The user posting the review. /// The GraphQL ID of the pending review. /// The review body. /// The review event. - Task SubmitPendingReview( + /// The updated state of the pull request. + Task SubmitPendingReview( ILocalRepositoryModel localRepository, - IAccount user, string pendingReviewId, string body, PullRequestReviewEvent e); @@ -229,29 +246,38 @@ public interface IPullRequestSessionService /// Posts a new pending PR review comment. /// /// The local repository. - /// The user posting the comment. /// The GraphQL ID of the pending review. /// The comment body. /// THe SHA of the commit to comment on. /// The relative path of the file to comment on. /// The line index in the diff to comment on. - /// A model representing the posted comment. + /// The updated state of the pull request. /// - /// The method posts a new pull request comment to a pending review started by - /// . + /// This method posts a new pull request comment to a pending review started by + /// . /// - Task PostPendingReviewComment( + Task PostPendingReviewComment( ILocalRepositoryModel localRepository, - IAccount user, string pendingReviewId, string body, string commitId, string path, int position); - Task PostPendingReviewCommentReply( + /// + /// Posts a new pending PR review comment reply. + /// + /// The local repository. + /// The GraphQL ID of the pending review. + /// The comment body. + /// The GraphQL ID of the comment to reply to. + /// The updated state of the pull request. + /// + /// The method posts a new pull request comment to a pending review started by + /// . + /// + Task PostPendingReviewCommentReply( ILocalRepositoryModel localRepository, - IAccount user, string pendingReviewId, string body, string inReplyTo); @@ -260,20 +286,19 @@ public interface IPullRequestSessionService /// Posts a new standalone PR review comment. /// /// The local repository. - /// The user posting the comment. - /// The pull request node id. + /// The GraphQL ID of the pull request. /// The comment body. /// THe SHA of the commit to comment on. /// The relative path of the file to comment on. /// The line index in the diff to comment on. - /// A model representing the posted comment. + /// The updated state of the pull request. /// /// The method posts a new standalone pull request comment that is not attached to a pending /// pull request review. /// - Task PostStandaloneReviewComment(ILocalRepositoryModel localRepository, - IAccount user, - string pullRequestNodeId, + Task PostStandaloneReviewComment( + ILocalRepositoryModel localRepository, + string pullRequestId, string body, string commitId, string path, @@ -283,16 +308,15 @@ public interface IPullRequestSessionService /// Posts a PR review comment reply. /// /// The local repository. - /// The user posting the comment. - /// The pull request node id. + /// The GraphQL ID of the pull request. /// The comment body. - /// The comment node id to reply to. - /// A model representing the posted comment. - Task PostStandaloneReviewCommentReply(ILocalRepositoryModel localRepository, - IAccount user, - string pullRequestNodeId, + /// The GraphQL ID of the comment to reply to. + /// The updated state of the pull request. + Task PostStandaloneReviewCommentReply( + ILocalRepositoryModel localRepository, + string pullRequestId, string body, - string inReplyToNodeId); + string inReplyTo); /// /// Delete a PR review comment. @@ -301,11 +325,10 @@ public interface IPullRequestSessionService /// The owner of the repository fork to delete from. /// The user deleting the comment. /// The pull request comment number. - /// - Task DeleteComment( + /// The updated state of the pull request. + Task DeleteComment( ILocalRepositoryModel localRepository, string remoteRepositoryOwner, - IAccount user, int number); /// @@ -316,10 +339,9 @@ public interface IPullRequestSessionService /// The user deleting the comment. /// The pull request comment node id. /// The replacement comment body. - /// A model representing the edited comment. - Task EditComment(ILocalRepositoryModel localRepository, + /// The updated state of the pull request. + Task EditComment(ILocalRepositoryModel localRepository, string remoteRepositoryOwner, - IAccount user, string commentNodeId, string body); } diff --git a/src/GitHub.InlineReviews/Services/PullRequestSession.cs b/src/GitHub.InlineReviews/Services/PullRequestSession.cs index e62a0af729..5076ab45e6 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestSession.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestSession.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Reactive.Subjects; using static System.FormattableString; +using GitHub.Primitives; namespace GitHub.InlineReviews.Services { @@ -32,16 +33,15 @@ public class PullRequestSession : ReactiveObject, IPullRequestSession bool isCheckedOut; string mergeBase; IReadOnlyList files; - IPullRequestModel pullRequest; + PullRequestDetailModel pullRequest; string pullRequestNodeId; - Subject pullRequestChanged = new Subject(); + Subject pullRequestChanged = new Subject(); bool hasPendingReview; - string pendingReviewNodeId { get; set; } public PullRequestSession( IPullRequestSessionService service, - IAccount user, - IPullRequestModel pullRequest, + ActorModel user, + PullRequestDetailModel pullRequest, ILocalRepositoryModel localRepository, string repositoryOwner, bool isCheckedOut) @@ -127,41 +127,35 @@ public string GetRelativePath(string path) } /// - public async Task PostReviewComment( + public async Task PostReviewComment( string body, string commitId, string path, IReadOnlyList diff, int position) { - IPullRequestReviewCommentModel model; - if (!HasPendingReview) { - var pullRequestNodeId = await GetPullRequestNodeId(); - model = await service.PostStandaloneReviewComment( + var model = await service.PostStandaloneReviewComment( LocalRepository, - User, - pullRequestNodeId, + PullRequest.Id, body, commitId, path, position); + await Update(model); } else { - model = await service.PostPendingReviewComment( + var model = await service.PostPendingReviewComment( LocalRepository, - User, - pendingReviewNodeId, + PendingReviewId, body, commitId, path, position); + await Update(model); } - - await AddComment(model); - return model; } /// @@ -171,59 +165,48 @@ public string GetRelativePath(string path) await service.DeleteComment( LocalRepository, RepositoryOwner, - User, number); - - await RemoveComment(number); } /// - public async Task EditComment(string commentNodeId, string body) + public async Task EditComment(string commentNodeId, string body) { var model = await service.EditComment( LocalRepository, RepositoryOwner, - User, commentNodeId, body); - await ReplaceComment(model); - return model; + await Update(model); } /// - public async Task PostReviewComment( + public async Task PostReviewComment( string body, - string inReplyToNodeId) + string inReplyTo) { - IPullRequestReviewCommentModel model; - if (!HasPendingReview) { - var pullRequestNodeId = await GetPullRequestNodeId(); - model = await service.PostStandaloneReviewCommentReply( + var model = await service.PostStandaloneReviewCommentReply( LocalRepository, - User, - pullRequestNodeId, + PullRequest.Id, body, - inReplyToNodeId); + inReplyTo); + await Update(model); } else { - model = await service.PostPendingReviewCommentReply( + var model = await service.PostPendingReviewCommentReply( LocalRepository, - User, - pendingReviewNodeId, + PendingReviewId, body, - inReplyToNodeId); + inReplyTo); + await Update(model); } - - await AddComment(model); - return model; } /// - public async Task StartReview() + public async Task StartReview() { if (HasPendingReview) { @@ -232,11 +215,9 @@ public async Task StartReview() var model = await service.CreatePendingReview( LocalRepository, - User, await GetPullRequestNodeId()); - await AddReview(model); - return model; + await Update(model); } /// @@ -247,31 +228,26 @@ public async Task CancelReview() throw new InvalidOperationException("There is no pending review to cancel."); } - await service.CancelPendingReview(LocalRepository, pendingReviewNodeId); + await service.CancelPendingReview(LocalRepository, PendingReviewId); PullRequest.Reviews = PullRequest.Reviews - .Where(x => x.NodeId != pendingReviewNodeId) - .ToList(); - PullRequest.ReviewComments = PullRequest.ReviewComments - .Where(x => x.PullRequestReviewId != PendingReviewId) + .Where(x => x.Id != PendingReviewId) .ToList(); await Update(PullRequest); } /// - public async Task PostReview(string body, Octokit.PullRequestReviewEvent e) + public async Task PostReview(string body, Octokit.PullRequestReviewEvent e) { - IPullRequestReviewModel model; + PullRequestDetailModel model; - if (pendingReviewNodeId == null) + if (PendingReviewId == null) { model = await service.PostReview( LocalRepository, - RepositoryOwner, - User, - PullRequest.Number, - PullRequest.Head.Sha, + PullRequest.Id, + PullRequest.HeadRefSha, body, e); } @@ -279,18 +255,28 @@ public async Task PostReview(string body, Octokit.PullR { model = await service.SubmitPendingReview( LocalRepository, - User, - pendingReviewNodeId, + PendingReviewId, body, e); } - await AddReview(model); - return model; + await Update(model); + } + + /// + public async Task Refresh() + { + var address = HostAddress.Create(LocalRepository.CloneUrl); + var model = await service.ReadPullRequestDetail( + address, + RepositoryOwner, + LocalRepository.Name, + PullRequest.Number); + await Update(model); } /// - public async Task Update(IPullRequestModel pullRequestModel) + async Task Update(PullRequestDetailModel pullRequestModel) { PullRequest = pullRequestModel; mergeBase = null; @@ -304,58 +290,27 @@ public async Task Update(IPullRequestModel pullRequestModel) pullRequestChanged.OnNext(pullRequestModel); } - async Task AddComment(IPullRequestReviewCommentModel comment) - { - PullRequest.ReviewComments = PullRequest.ReviewComments - .Concat(new[] { comment }) - .ToList(); - await Update(PullRequest); - } - - async Task ReplaceComment(IPullRequestReviewCommentModel comment) + async Task AddComment(PullRequestReviewCommentModel comment) { - PullRequest.ReviewComments = PullRequest.ReviewComments - .Select(model => model.Id == comment.Id ? comment: model) - .ToList(); - - await Update(PullRequest); - } - - async Task RemoveComment(int commentId) - { - PullRequest.ReviewComments = PullRequest.ReviewComments - .Where(model => model.Id != commentId) - .ToList(); - - await Update(PullRequest); - } - - async Task AddReview(IPullRequestReviewModel review) - { - PullRequest.Reviews = PullRequest.Reviews - .Where(x => x.NodeId != review.NodeId) - .Concat(new[] { review }) - .ToList(); + var review = PullRequest.Reviews.FirstOrDefault(x => x.Id == PendingReviewId); - if (review.State != PullRequestReviewState.Pending) + if (review == null) { - foreach (var comment in PullRequest.ReviewComments) - { - if (comment.PullRequestReviewId == review.Id) - { - comment.IsPending = false; - } - } + throw new KeyNotFoundException("Could not find pending review."); } + review.Comments = review.Comments + .Concat(new[] { comment }) + .ToList(); await Update(PullRequest); } async Task UpdateFile(PullRequestSessionFile file) { + await Task.Delay(0); var mergeBaseSha = await GetMergeBase(); - file.BaseSha = PullRequest.Base.Sha; - file.CommitSha = file.IsTrackingHead ? PullRequest.Head.Sha : file.CommitSha; + file.BaseSha = PullRequest.BaseRefSha; + 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); } @@ -363,19 +318,17 @@ async Task UpdateFile(PullRequestSessionFile file) void UpdatePendingReview() { var pendingReview = PullRequest.Reviews - .FirstOrDefault(x => x.State == PullRequestReviewState.Pending && x.User.Login == User.Login); + .FirstOrDefault(x => x.State == PullRequestReviewState.Pending && x.Author.Login == User.Login); if (pendingReview != null) { HasPendingReview = true; - pendingReviewNodeId = pendingReview.NodeId; PendingReviewId = pendingReview.Id; } else { HasPendingReview = false; - pendingReviewNodeId = null; - PendingReviewId = 0; + PendingReviewId = null; } } @@ -428,10 +381,10 @@ public bool IsCheckedOut } /// - public IAccount User { get; } + public ActorModel User { get; } /// - public IPullRequestModel PullRequest + public PullRequestDetailModel PullRequest { get { return pullRequest; } private set @@ -449,7 +402,7 @@ private set } /// - public IObservable PullRequestChanged => pullRequestChanged; + public IObservable PullRequestChanged => pullRequestChanged; /// public ILocalRepositoryModel LocalRepository { get; } @@ -465,7 +418,7 @@ public bool HasPendingReview } /// - public long PendingReviewId { get; private set; } + public string PendingReviewId { get; private set; } IEnumerable FilePaths { diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs index 50e5b0dbba..56109be303 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs @@ -13,6 +13,7 @@ using GitHub.InlineReviews.Models; using GitHub.Logging; using GitHub.Models; +using GitHub.Primitives; using GitHub.Services; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -32,8 +33,6 @@ public class PullRequestSessionManager : ReactiveObject, IPullRequestSessionMana static readonly ILogger log = LogManager.ForContext(); readonly IPullRequestService service; readonly IPullRequestSessionService sessionService; - readonly IConnectionManager connectionManager; - readonly IModelServiceFactory modelServiceFactory; readonly Dictionary, WeakReference> sessions = new Dictionary, WeakReference>(); TaskCompletionSource initialized; @@ -45,27 +44,19 @@ public class PullRequestSessionManager : ReactiveObject, IPullRequestSessionMana /// /// The PR service to use. /// The PR session service to use. - /// The connectionManager to use. - /// The ModelService factory. /// The team explorer service to use. [ImportingConstructor] public PullRequestSessionManager( IPullRequestService service, IPullRequestSessionService sessionService, - IConnectionManager connectionManager, - IModelServiceFactory modelServiceFactory, ITeamExplorerContext teamExplorerContext) { Guard.ArgumentNotNull(service, nameof(service)); Guard.ArgumentNotNull(sessionService, nameof(sessionService)); - Guard.ArgumentNotNull(connectionManager, nameof(connectionManager)); - Guard.ArgumentNotNull(modelServiceFactory, nameof(modelServiceFactory)); Guard.ArgumentNotNull(teamExplorerContext, nameof(teamExplorerContext)); this.service = service; this.sessionService = sessionService; - this.connectionManager = connectionManager; - this.modelServiceFactory = modelServiceFactory; initialized = new TaskCompletionSource(null); Observable.FromEventPattern(teamExplorerContext, nameof(teamExplorerContext.StatusChanged)) @@ -155,11 +146,11 @@ public string GetRelativePath(ITextBuffer buffer) } /// - public async Task GetSession(IPullRequestModel pullRequest) + public async Task GetSession(string owner, string name, int number) { - Guard.ArgumentNotNull(pullRequest, nameof(pullRequest)); + var session = await GetSessionInternal(owner, name, number); - if (await service.EnsureLocalBranchesAreMarkedAsPullRequests(repository, pullRequest)) + if (await service.EnsureLocalBranchesAreMarkedAsPullRequests(repository, session.PullRequest)) { // The branch for the PR was not previously marked with the PR number in the git // config so we didn't pick up that the current branch is a PR branch. That has @@ -167,7 +158,7 @@ public async Task GetSession(IPullRequestModel pullRequest) await StatusChanged(); } - return await GetSessionInternal(pullRequest); + return session; } /// @@ -215,19 +206,14 @@ async Task StatusChanged() if (pr != null) { var changePR = - pr.Item1 != (session?.PullRequest.Base.RepositoryCloneUrl.Owner) || + pr.Item1 != (session?.PullRequest.BaseRepositoryOwner) || pr.Item2 != (session?.PullRequest.Number); if (changePR) { - var modelService = await connectionManager.GetModelService(repository, modelServiceFactory); - var pullRequest = await modelService?.GetPullRequest(pr.Item1, repository.Name, pr.Item2); - if (pullRequest != null) - { - var newSession = await GetSessionInternal(pullRequest); - if (newSession != null) newSession.IsCheckedOut = true; - session = newSession; - } + var newSession = await GetSessionInternal(pr.Item1, repository.Name, pr.Item2); + if (newSession != null) newSession.IsCheckedOut = true; + session = newSession; } } else @@ -244,11 +230,11 @@ async Task StatusChanged() } } - async Task GetSessionInternal(IPullRequestModel pullRequest) + async Task GetSessionInternal(string owner, string name, int number) { PullRequestSession session = null; WeakReference weakSession; - var key = Tuple.Create(pullRequest.Base.RepositoryCloneUrl.Owner, pullRequest.Number); + var key = Tuple.Create(owner, number); if (sessions.TryGetValue(key, out weakSession)) { @@ -257,23 +243,17 @@ async Task GetSessionInternal(IPullRequestModel pullRequest) if (session == null) { - var modelService = await connectionManager.GetModelService(repository, modelServiceFactory); - - if (modelService != null) - { - session = new PullRequestSession( - sessionService, - await modelService.GetCurrentUser(), - pullRequest, - repository, - key.Item1, - false); - sessions[key] = new WeakReference(session); - } - } - else - { - await session.Update(pullRequest); + var address = HostAddress.Create(repository.CloneUrl); + var pullRequest = await sessionService.ReadPullRequestDetail(address, owner, name, number); + + session = new PullRequestSession( + sessionService, + await sessionService.ReadViewer(address), + pullRequest, + repository, + key.Item1, + false); + sessions[key] = new WeakReference(session); } return session; @@ -287,12 +267,12 @@ async Task UpdateLiveFile(PullRequestSessionLiveFile file, bool rebuildThreads) { var mergeBase = await session.GetMergeBase(); var contents = sessionService.GetContents(file.TextBuffer); - file.BaseSha = session.PullRequest.Base.Sha; + file.BaseSha = session.PullRequest.BaseRefSha; file.CommitSha = await CalculateCommitSha(session, file, contents); file.Diff = await sessionService.Diff( session.LocalRepository, mergeBase, - session.PullRequest.Head.Sha, + session.PullRequest.HeadRefSha, file.RelativePath, contents); @@ -302,7 +282,7 @@ async Task UpdateLiveFile(PullRequestSessionLiveFile file, bool rebuildThreads) session.PullRequest, file.RelativePath, file.Diff, - session.PullRequest.Head.Sha); + session.PullRequest.HeadRefSha); } else { diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs index f9d71cbcf4..0e0eb3eb39 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs @@ -23,6 +23,7 @@ using ReactiveUI; using Serilog; using PullRequestReviewEvent = Octokit.PullRequestReviewEvent; +using static Octokit.GraphQL.Variable; // GraphQL DatabaseId field are marked as deprecated, but we need them for interop with REST. #pragma warning disable CS0618 @@ -36,6 +37,8 @@ namespace GitHub.InlineReviews.Services public class PullRequestSessionService : IPullRequestSessionService { static readonly ILogger log = LogManager.ForContext(); + static ICompiledQuery readPullRequest; + static ICompiledQuery readViewer; readonly IGitService gitService; readonly IGitClient gitClient; @@ -43,7 +46,6 @@ public class PullRequestSessionService : IPullRequestSessionService readonly IApiClientFactory apiClientFactory; readonly IGraphQLClientFactory graphqlFactory; readonly IUsageTracker usageTracker; - readonly IDictionary, string> mergeBaseCache; [ImportingConstructor] @@ -85,22 +87,22 @@ public virtual async Task> Diff(ILocalRepositoryModel r /// public IReadOnlyList BuildCommentThreads( - IPullRequestModel pullRequest, + PullRequestDetailModel pullRequest, string relativePath, IReadOnlyList diff, string headSha) { relativePath = relativePath.Replace("\\", "/"); - var commentsByPosition = pullRequest.ReviewComments - .Where(x => x.Path == relativePath && x.OriginalPosition.HasValue) + var threadsByPosition = pullRequest.Threads + .Where(x => x.Path == relativePath && !x.IsOutdated) .OrderBy(x => x.Id) - .GroupBy(x => Tuple.Create(x.OriginalCommitId, x.OriginalPosition.Value)); + .GroupBy(x => Tuple.Create(x.OriginalCommitSha, x.OriginalPosition)); var threads = new List(); - foreach (var comments in commentsByPosition) + foreach (var thread in threadsByPosition) { - var hunk = comments.First().DiffHunk; + var hunk = thread.First().DiffHunk; var chunks = DiffUtilities.ParseFragment(hunk); var chunk = chunks.Last(); var diffLines = chunk.Lines.Reverse().Take(5).ToList(); @@ -111,12 +113,16 @@ public virtual async Task> Diff(ILocalRepositoryModel r continue; } - var thread = new InlineCommentThreadModel( + var inlineThread = new InlineCommentThreadModel( relativePath, headSha, diffLines, - comments); - threads.Add(thread); + thread.SelectMany(t => t.Comments.Select(c => new InlineCommentModel + { + Comment = c, + Review = pullRequest.Reviews.FirstOrDefault(x => x.Comments.Contains(c)), + }))); + threads.Add(inlineThread); } UpdateCommentThreads(threads, diff); @@ -124,11 +130,11 @@ public virtual async Task> Diff(ILocalRepositoryModel r } /// - public IReadOnlyList> UpdateCommentThreads( + public IReadOnlyList> UpdateCommentThreads( IReadOnlyList threads, IReadOnlyList diff) { - var changedLines = new List>(); + var changedLines = new List>(); foreach (var thread in threads) { @@ -151,7 +157,7 @@ public virtual async Task> Diff(ILocalRepositoryModel r if (changed) { - var side = thread.DiffLineType == DiffChangeType.Delete ? DiffSide.Left : DiffSide.Right; + var side = thread.DiffLineType == DiffChangeType.Delete ? GitHub.Models.DiffSide.Left : GitHub.Models.DiffSide.Right; if (oldLineNumber != -1) changedLines.Add(Tuple.Create(oldLineNumber, side)); if (newLineNumber != -1 && newLineNumber != oldLineNumber) changedLines.Add(Tuple.Create(newLineNumber, side)); } @@ -260,6 +266,108 @@ public async Task ReadFileAsync(string path) return null; } + public virtual async Task ReadPullRequestDetail(HostAddress address, string owner, string name, int number) + { + if (readPullRequest == null) + { + readPullRequest = new Query() + .Repository(Var(nameof(owner)), Var(nameof(name))) + .PullRequest(Var(nameof(number))) + .Select(pr => new PullRequestDetailModel + { + Id = pr.Id.Value, + Number = pr.Number, + Author = new ActorModel + { + Login = pr.Author.Login, + AvatarUrl = pr.Author.AvatarUrl(null), + }, + Title = pr.Title, + Body = pr.Body, + BaseRefSha = pr.BaseRefOid, + BaseRefName = pr.BaseRefName, + BaseRepositoryOwner = pr.Repository.Owner.Login, + HeadRefName = pr.HeadRefName, + HeadRefSha = pr.HeadRefOid, + HeadRepositoryOwner = pr.HeadRepositoryOwner != null ? pr.HeadRepositoryOwner.Login : null, + State = (PullRequestStateEnum)pr.State, + 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, + SubmittedAt = review.SubmittedAt, + Author = new ActorModel + { + Login = review.Author.Login, + AvatarUrl = review.Author.AvatarUrl(null), + }, + Comments = review.Comments(null, null, null, null).AllPages().Select(comment => new CommentAdapter + { + Id = comment.Id.Value, + Author = new ActorModel + { + Login = comment.Author.Login, + AvatarUrl = comment.Author.AvatarUrl(null), + }, + Body = comment.Body, + Path = comment.Path, + CommitSha = comment.Commit.Oid, + DiffHunk = comment.DiffHunk, + Position = comment.Position, + OriginalPosition = comment.OriginalPosition, + OriginalCommitId = comment.OriginalCommit.Oid, + ReplyTo = comment.ReplyTo != null ? comment.ReplyTo.Id.Value : null, + CreatedAt = comment.CreatedAt, + Url = comment.Url, + }).ToList(), + }).ToList(), + }).Compile(); + } + + var vars = new Dictionary + { + { nameof(owner), owner }, + { nameof(name), name }, + { nameof(number), number }, + }; + + var connection = await graphqlFactory.CreateConnection(address); + var result = await connection.Run(readPullRequest, vars); + + var apiClient = await apiClientFactory.Create(address); + var files = await apiClient.GetPullRequestFiles(owner, name, number).ToList(); + + result.ChangedFiles = files.Select(file => new PullRequestFileModel + { + FileName = file.FileName, + Sha = file.Sha, + Status = (PullRequestFileStatus)Enum.Parse(typeof(PullRequestFileStatus), file.Status, true), + }).ToList(); + + BuildPullRequestThreads(result); + return result; + } + + public virtual async Task ReadViewer(HostAddress address) + { + if (readViewer == null) + { + readViewer = new Query() + .Viewer + .Select(x => new ActorModel + { + Login = x.Login, + AvatarUrl = x.AvatarUrl(null), + }).Compile(); + } + + var connection = await graphqlFactory.CreateConnection(address); + return await connection.Run(readViewer); + } + public async Task GetGraphQLPullRequestId( ILocalRepositoryModel localRepository, string repositoryOwner, @@ -273,14 +381,14 @@ public async Task ReadFileAsync(string path) .PullRequest(number) .Select(x => x.Id); - return await graphql.Run(query); + return (await graphql.Run(query)).Value; } /// - public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel repository, IPullRequestModel pullRequest) + public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest) { - var baseSha = pullRequest.Base.Sha; - var headSha = pullRequest.Head.Sha; + var baseSha = pullRequest.BaseRefSha; + var headSha = pullRequest.HeadRefSha; var key = new Tuple(baseSha, headSha); string mergeBase; @@ -291,9 +399,9 @@ public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel using (var repo = await GetRepository(repository)) { - var targetUrl = pullRequest.Base.RepositoryCloneUrl; - var headUrl = pullRequest.Head.RepositoryCloneUrl; - var baseRef = pullRequest.Base.Ref; + var targetUrl = repository.CloneUrl.WithOwner(pullRequest.BaseRepositoryOwner); + var headUrl = repository.CloneUrl.WithOwner(pullRequest.HeadRepositoryOwner); + var baseRef = pullRequest.BaseRefName; var pullNumber = pullRequest.Number; try { @@ -320,9 +428,8 @@ public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel } /// - public async Task CreatePendingReview( + public async Task CreatePendingReview( ILocalRepositoryModel localRepository, - IAccount user, string pullRequestId) { var address = HostAddress.Create(localRepository.CloneUrl.Host); @@ -330,28 +437,24 @@ public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel var review = new AddPullRequestReviewInput { - PullRequestId = pullRequestId, + PullRequestId = new ID(pullRequestId), }; - var addReview = new Mutation() + var mutation = new Mutation() .AddPullRequestReview(review) - .Select(x => new PullRequestReviewModel + .Select(x => new { - Id = x.PullRequestReview.DatabaseId.Value, - Body = x.PullRequestReview.Body, - CommitId = x.PullRequestReview.Commit.Oid, - NodeId = x.PullRequestReview.Id, - State = FromGraphQL(x.PullRequestReview.State), - User = user, + x.PullRequestReview.Repository.Owner.Login, + x.PullRequestReview.PullRequest.Number }); - var result = await graphql.Run(addReview); + var result = await graphql.Run(mutation); await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentStartReview); - return result; + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); } /// - public async Task CancelPendingReview( + public async Task CancelPendingReview( ILocalRepositoryModel localRepository, string reviewId) { @@ -360,52 +463,55 @@ public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel var delete = new DeletePullRequestReviewInput { - PullRequestReviewId = reviewId, + PullRequestReviewId = new ID(reviewId), }; - var deleteReview = new Mutation() + var mutation = new Mutation() .DeletePullRequestReview(delete) - .Select(x => x.ClientMutationId); + .Select(x => new + { + x.PullRequestReview.Repository.Owner.Login, + x.PullRequestReview.PullRequest.Number + }); - await graphql.Run(deleteReview); + var result = await graphql.Run(mutation); + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); } /// - public async Task PostReview( + public async Task PostReview( ILocalRepositoryModel localRepository, - string remoteRepositoryOwner, - IAccount user, - int number, + string pullRequestId, string commitId, string body, PullRequestReviewEvent e) { var address = HostAddress.Create(localRepository.CloneUrl.Host); - var apiClient = await apiClientFactory.Create(address); - - var result = await apiClient.PostPullRequestReview( - remoteRepositoryOwner, - localRepository.Name, - number, - commitId, - body, - e); - - await usageTracker.IncrementCounter(x => x.NumberOfPRReviewPosts); + var graphql = await graphqlFactory.CreateConnection(address); - return new PullRequestReviewModel + var addReview = new AddPullRequestReviewInput { - Id = result.Id, - Body = result.Body, - CommitId = result.CommitId, - State = (GitHub.Models.PullRequestReviewState)result.State.Value, - User = user, + Body = body, + CommitOID = commitId, + Event = ToGraphQl(e), + PullRequestId = new ID(pullRequestId), }; + + var mutation = new Mutation() + .AddPullRequestReview(addReview) + .Select(x => new + { + x.PullRequestReview.Repository.Owner.Login, + x.PullRequestReview.PullRequest.Number + }); + + var result = await graphql.Run(mutation); + await usageTracker.IncrementCounter(x => x.NumberOfPRReviewPosts); + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); } - public async Task SubmitPendingReview( + public async Task SubmitPendingReview( ILocalRepositoryModel localRepository, - IAccount user, string pendingReviewId, string body, PullRequestReviewEvent e) @@ -417,30 +523,25 @@ public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel { Body = body, Event = ToGraphQl(e), - PullRequestReviewId = pendingReviewId, + PullRequestReviewId = new ID(pendingReviewId), }; var mutation = new Mutation() .SubmitPullRequestReview(submit) - .Select(x => new PullRequestReviewModel + .Select(x => new { - Body = body, - CommitId = x.PullRequestReview.Commit.Oid, - Id = x.PullRequestReview.DatabaseId.Value, - NodeId = x.PullRequestReview.Id, - State = (GitHub.Models.PullRequestReviewState)x.PullRequestReview.State, - User = user, + x.PullRequestReview.Repository.Owner.Login, + x.PullRequestReview.PullRequest.Number }); var result = await graphql.Run(mutation); await usageTracker.IncrementCounter(x => x.NumberOfPRReviewPosts); - return result; + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); } /// - public async Task PostPendingReviewComment( + public async Task PostPendingReviewComment( ILocalRepositoryModel localRepository, - IAccount user, string pendingReviewId, string body, string commitId, @@ -456,37 +557,25 @@ public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel CommitOID = commitId, Path = path, Position = position, - PullRequestReviewId = pendingReviewId, + PullRequestReviewId = new ID(pendingReviewId), }; var addComment = new Mutation() .AddPullRequestReviewComment(comment) - .Select(x => new PullRequestReviewCommentModel + .Select(x => new { - Id = x.Comment.DatabaseId.Value, - NodeId = x.Comment.Id, - Body = x.Comment.Body, - CommitId = x.Comment.Commit.Oid, - Path = x.Comment.Path, - Position = x.Comment.Position, - CreatedAt = x.Comment.CreatedAt.Value, - DiffHunk = x.Comment.DiffHunk, - OriginalPosition = x.Comment.OriginalPosition, - OriginalCommitId = x.Comment.OriginalCommit.Oid, - PullRequestReviewId = x.Comment.PullRequestReview.DatabaseId.Value, - User = user, - IsPending = true, + x.Comment.Repository.Owner.Login, + x.Comment.PullRequest.Number }); var result = await graphql.Run(addComment); await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost); - return result; + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); } /// - public async Task PostPendingReviewCommentReply( + public async Task PostPendingReviewCommentReply( ILocalRepositoryModel localRepository, - IAccount user, string pendingReviewId, string body, string inReplyTo) @@ -497,39 +586,27 @@ public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel var comment = new AddPullRequestReviewCommentInput { Body = body, - InReplyTo = inReplyTo, - PullRequestReviewId = pendingReviewId, + InReplyTo = new ID(inReplyTo), + PullRequestReviewId = new ID(pendingReviewId), }; var addComment = new Mutation() .AddPullRequestReviewComment(comment) - .Select(x => new PullRequestReviewCommentModel + .Select(x => new { - Id = x.Comment.DatabaseId.Value, - NodeId = x.Comment.Id, - Body = x.Comment.Body, - CommitId = x.Comment.Commit.Oid, - Path = x.Comment.Path, - Position = x.Comment.Position, - CreatedAt = x.Comment.CreatedAt.Value, - DiffHunk = x.Comment.DiffHunk, - OriginalPosition = x.Comment.OriginalPosition, - OriginalCommitId = x.Comment.OriginalCommit.Oid, - PullRequestReviewId = x.Comment.PullRequestReview.DatabaseId.Value, - User = user, - IsPending = true, + x.Comment.Repository.Owner.Login, + x.Comment.PullRequest.Number }); var result = await graphql.Run(addComment); await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost); - return result; + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); } /// - public async Task PostStandaloneReviewComment( + public async Task PostStandaloneReviewComment( ILocalRepositoryModel localRepository, - IAccount user, - string pullRequestNodeId, + string pullRequestId, string body, string commitId, string path, @@ -543,7 +620,7 @@ public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel Body = body, CommitOID = commitId, Event = Octokit.GraphQL.Model.PullRequestReviewEvent.Comment, - PullRequestId = pullRequestNodeId, + PullRequestId = new ID(pullRequestId), Comments = new[] { new DraftPullRequestReviewComment @@ -557,51 +634,33 @@ public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel var mutation = new Mutation() .AddPullRequestReview(addReview) - .Select(payload => - payload.PullRequestReview - .Comments(1, null, null, null) - .Nodes.Select(x => new PullRequestReviewCommentModel - { - Id = x.DatabaseId.Value, - NodeId = x.Id, - Body = x.Body, - CommitId = x.Commit.Oid, - Path = x.Path, - Position = x.Position, - CreatedAt = x.CreatedAt.Value, - DiffHunk = x.DiffHunk, - OriginalPosition = x.OriginalPosition, - OriginalCommitId = x.OriginalCommit.Oid, - PullRequestReviewId = x.PullRequestReview.DatabaseId.Value, - User = user, - IsPending = false - })); - - var result = (await graphql.Run(mutation)).First(); + .Select(x => new + { + x.PullRequestReview.Repository.Owner.Login, + x.PullRequestReview.PullRequest.Number + }); + + var result = await graphql.Run(mutation); await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost); - return result; + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); } /// - public async Task PostStandaloneReviewCommentReply( + public async Task PostStandaloneReviewCommentReply( ILocalRepositoryModel localRepository, - IAccount user, - string pullRequestNodeId, + string pullRequestId, string body, - string inReplyToNodeId) + string inReplyTo) { - var review = await CreatePendingReview(localRepository, user, pullRequestNodeId); - var comment = await PostPendingReviewCommentReply(localRepository, user, review.NodeId, body, inReplyToNodeId); - await SubmitPendingReview(localRepository, user, review.NodeId, null, PullRequestReviewEvent.Comment); - comment.IsPending = false; - return comment; + var review = await CreatePendingReview(localRepository, pullRequestId); + var comment = await PostPendingReviewCommentReply(localRepository, review.Id, body, inReplyTo); + return await SubmitPendingReview(localRepository, review.Id, null, PullRequestReviewEvent.Comment); } /// - public async Task DeleteComment( + public async Task DeleteComment( ILocalRepositoryModel localRepository, string remoteRepositoryOwner, - IAccount user, int number) { var address = HostAddress.Create(localRepository.CloneUrl.Host); @@ -613,12 +672,12 @@ public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel number); await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentDelete); + return await ReadPullRequestDetail(address, remoteRepositoryOwner, localRepository.Name, number); } /// - public async Task EditComment(ILocalRepositoryModel localRepository, + public async Task EditComment(ILocalRepositoryModel localRepository, string remoteRepositoryOwner, - IAccount user, string commentNodeId, string body) { @@ -628,30 +687,19 @@ public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel var updatePullRequestReviewCommentInput = new UpdatePullRequestReviewCommentInput { Body = body, - PullRequestReviewCommentId = commentNodeId + PullRequestReviewCommentId = new ID(commentNodeId), }; var editComment = new Mutation().UpdatePullRequestReviewComment(updatePullRequestReviewCommentInput) - .Select(x => new PullRequestReviewCommentModel + .Select(x => new { - Id = x.PullRequestReviewComment.DatabaseId.Value, - NodeId = x.PullRequestReviewComment.Id, - Body = x.PullRequestReviewComment.Body, - CommitId = x.PullRequestReviewComment.Commit.Oid, - Path = x.PullRequestReviewComment.Path, - Position = x.PullRequestReviewComment.Position, - CreatedAt = x.PullRequestReviewComment.CreatedAt.Value, - DiffHunk = x.PullRequestReviewComment.DiffHunk, - OriginalPosition = x.PullRequestReviewComment.OriginalPosition, - OriginalCommitId = x.PullRequestReviewComment.OriginalCommit.Oid, - PullRequestReviewId = x.PullRequestReviewComment.PullRequestReview.DatabaseId.Value, - User = user, - IsPending = !x.PullRequestReviewComment.PublishedAt.HasValue, + x.PullRequestReviewComment.Repository.Owner.Login, + x.PullRequestReviewComment.PullRequest.Number }); var result = await graphql.Run(editComment); await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost); - return result; + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); } int GetUpdatedLineNumber(IInlineCommentThreadModel thread, IEnumerable diff) @@ -673,6 +721,65 @@ Task GetRepository(ILocalRepositoryModel repository) return Task.Factory.StartNew(() => gitService.GetRepository(repository.LocalPath)); } + static void BuildPullRequestThreads(PullRequestDetailModel model) + { + var commentsByReplyId = new Dictionary>(); + + // Get all comments that are not replies. + foreach (CommentAdapter comment in model.Reviews.SelectMany(x => x.Comments)) + { + if (comment.ReplyTo == null) + { + commentsByReplyId.Add(comment.Id, new List { comment }); + } + } + + // Get the comments that are replies and place them into the relevant list. + foreach (CommentAdapter comment in model.Reviews.SelectMany(x => x.Comments)) + { + if (comment.ReplyTo != null) + { + List thread = null; + + if (commentsByReplyId.TryGetValue(comment.ReplyTo, out thread)) + { + thread.Add(comment); + break; + } + } + } + + // Build a collection of threads for the information collected above. + var threads = new List(); + + foreach (var threadSource in commentsByReplyId) + { + var adapter = threadSource.Value[0]; + + var thread = new PullRequestReviewThreadModel + { + Comments = threadSource.Value, + CommitSha = adapter.CommitSha, + DiffHunk = adapter.DiffHunk, + Id = adapter.Id, + IsOutdated = adapter.Position == null, + OriginalCommitSha = adapter.OriginalCommitId, + OriginalPosition = adapter.OriginalPosition, + Path = adapter.Path, + Position = adapter.Position, + }; + + // Set a reference to the thread in the comment. + foreach (var comment in threadSource.Value) + { + comment.Thread = thread; + } + + threads.Add(thread); + } + + model.Threads = threads; + } static GitHub.Models.PullRequestReviewState FromGraphQL(Octokit.GraphQL.Model.PullRequestReviewState s) { @@ -693,5 +800,16 @@ static Octokit.GraphQL.Model.PullRequestReviewEvent ToGraphQl(Octokit.PullReques throw new NotSupportedException(); } } + + class CommentAdapter : PullRequestReviewCommentModel + { + public string Path { get; set; } + public string CommitSha { get; set; } + public string DiffHunk { get; set; } + public int? Position { get; set; } + public int OriginalPosition { get; set; } + public string OriginalCommitId { get; set; } + public string ReplyTo { get; set; } + } } } diff --git a/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs b/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs index b0d8ffcb47..3891003d63 100644 --- a/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs +++ b/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs @@ -151,6 +151,7 @@ async Task Initialize() } else { + side = DiffSide.Right; await InitializeLiveFile(); sessionManagerSubscription = sessionManager .WhenAnyValue(x => x.CurrentSession) diff --git a/src/GitHub.InlineReviews/ViewModels/CommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/CommentThreadViewModel.cs index 016a5b1de7..84f35c4695 100644 --- a/src/GitHub.InlineReviews/ViewModels/CommentThreadViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/CommentThreadViewModel.cs @@ -1,7 +1,9 @@ using System; using System.Collections.ObjectModel; +using System.Reactive; using GitHub.Extensions; using GitHub.Models; +using GitHub.ViewModels; using ReactiveUI; namespace GitHub.InlineReviews.ViewModels @@ -11,27 +13,27 @@ namespace GitHub.InlineReviews.ViewModels /// public abstract class CommentThreadViewModel : ReactiveObject, ICommentThreadViewModel { - ReactiveCommand postComment; - private ReactiveCommand editComment; - private ReactiveCommand deleteComment; + ReactiveCommand postComment; + ReactiveCommand editComment; + ReactiveCommand deleteComment; /// /// Intializes a new instance of the class. /// /// The current user. - protected CommentThreadViewModel(IAccount currentUser) + protected CommentThreadViewModel(ActorModel currentUser) { Guard.ArgumentNotNull(currentUser, nameof(currentUser)); Comments = new ObservableCollection(); - CurrentUser = currentUser; + CurrentUser = new ActorViewModel(currentUser); } /// public ObservableCollection Comments { get; } /// - public ReactiveCommand PostComment + public ReactiveCommand PostComment { get { return postComment; } protected set @@ -45,7 +47,7 @@ protected set } } - public ReactiveCommand EditComment + public ReactiveCommand EditComment { get { return editComment; } protected set @@ -57,7 +59,7 @@ protected set } } - public ReactiveCommand DeleteComment + public ReactiveCommand DeleteComment { get { return deleteComment; } protected set @@ -70,9 +72,6 @@ protected set } /// - public IAccount CurrentUser { get; } - - /// - public abstract Uri GetCommentUrl(int id); + public IActorViewModel CurrentUser { get; } } } diff --git a/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs index 14f5848a42..89276e7693 100644 --- a/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs @@ -6,6 +6,7 @@ using GitHub.Extensions; using GitHub.Logging; using GitHub.Models; +using GitHub.ViewModels; using ReactiveUI; using Serilog; @@ -31,39 +32,37 @@ public class CommentViewModel : ReactiveObject, ICommentViewModel /// /// The thread that the comment is a part of. /// The current user. - /// The ID of the comment. - /// The GraphQL ID of the comment. + /// The GraphQL ID of the comment. /// The comment body. /// The comment edit state. - /// The author of the comment. + /// The author of the comment. /// The modified date of the comment. protected CommentViewModel( ICommentThreadViewModel thread, - IAccount currentUser, - int commentId, - string commentNodeId, + IActorViewModel currentUser, + string commentId, string body, CommentEditState state, - IAccount user, - DateTimeOffset updatedAt) + IActorViewModel author, + DateTimeOffset updatedAt, + Uri webUrl) { Guard.ArgumentNotNull(thread, nameof(thread)); Guard.ArgumentNotNull(currentUser, nameof(currentUser)); - Guard.ArgumentNotNull(body, nameof(body)); - Guard.ArgumentNotNull(user, nameof(user)); + Guard.ArgumentNotNull(author, nameof(author)); Thread = thread; CurrentUser = currentUser; Id = commentId; - NodeId = commentNodeId; Body = body; EditState = state; - User = user; + Author = author; UpdatedAt = updatedAt; + WebUrl = webUrl; var canDeleteObservable = this.WhenAnyValue( x => x.EditState, - x => x == CommentEditState.None && user.Login.Equals(currentUser.Login)); + x => x == CommentEditState.None && author.Login == currentUser.Login); canDelete = canDeleteObservable.ToProperty(this, x => x.CanDelete); @@ -71,7 +70,7 @@ public class CommentViewModel : ReactiveObject, ICommentViewModel var canEdit = this.WhenAnyValue( x => x.EditState, - x => x == CommentEditState.Placeholder || (x == CommentEditState.None && user.Login.Equals(currentUser.Login))); + x => x == CommentEditState.Placeholder || (x == CommentEditState.None && author.Login == currentUser.Login)); BeginEdit = ReactiveCommand.Create(canEdit); BeginEdit.Subscribe(DoBeginEdit); @@ -90,7 +89,7 @@ public class CommentViewModel : ReactiveObject, ICommentViewModel CancelEdit.Subscribe(DoCancelEdit); AddErrorHandler(CancelEdit); - OpenOnGitHub = ReactiveCommand.Create(this.WhenAnyValue(x => x.Id, x => x != 0)); + OpenOnGitHub = ReactiveCommand.Create(this.WhenAnyValue(x => x.Id).Select(x => x != null)); } /// @@ -101,9 +100,17 @@ public class CommentViewModel : ReactiveObject, ICommentViewModel /// The comment model. protected CommentViewModel( ICommentThreadViewModel thread, - IAccount currentUser, - ICommentModel model) - : this(thread, currentUser, model.Id, model.NodeId, model.Body, CommentEditState.None, model.User, model.CreatedAt) + ActorModel currentUser, + CommentModel model) + : this( + thread, + new ActorViewModel(currentUser), + model.Id, + model.Body, + CommentEditState.None, + new ActorViewModel(model.Author), + model.CreatedAt, + new Uri(model.Url)) { } @@ -161,21 +168,14 @@ async Task DoCommitEdit(object unused) ErrorMessage = null; IsSubmitting = true; - ICommentModel model; - - if (Id == 0) + if (Id == null) { - model = await Thread.PostComment.ExecuteAsyncTask(Body); + await Thread.PostComment.ExecuteAsyncTask(Body); } else { - model = await Thread.EditComment.ExecuteAsyncTask(new Tuple(NodeId, Body)); + await Thread.EditComment.ExecuteAsyncTask(new Tuple(Id, Body)); } - - Id = model.Id; - NodeId = model.NodeId; - EditState = CommentEditState.None; - UpdatedAt = DateTimeOffset.Now; } catch (Exception e) { @@ -190,10 +190,7 @@ async Task DoCommitEdit(object unused) } /// - public int Id { get; private set; } - - /// - public string NodeId { get; private set; } + public string Id { get; private set; } /// public string Body @@ -243,13 +240,16 @@ public DateTimeOffset UpdatedAt } /// - public IAccount CurrentUser { get; } + public IActorViewModel CurrentUser { get; } /// public ICommentThreadViewModel Thread { get; } /// - public IAccount User { get; } + public IActorViewModel Author { get; } + + /// + public Uri WebUrl { get; } /// public ReactiveCommand BeginEdit { get; } diff --git a/src/GitHub.InlineReviews/ViewModels/ICommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/ICommentThreadViewModel.cs index b24e28ea50..b17290de59 100644 --- a/src/GitHub.InlineReviews/ViewModels/ICommentThreadViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/ICommentThreadViewModel.cs @@ -1,6 +1,8 @@ using System; using System.Collections.ObjectModel; +using System.Reactive; using GitHub.Models; +using GitHub.ViewModels; using ReactiveUI; namespace GitHub.InlineReviews.ViewModels @@ -10,13 +12,6 @@ namespace GitHub.InlineReviews.ViewModels /// public interface ICommentThreadViewModel { - /// - /// Gets the browser URI for a comment in the thread. - /// - /// The ID of the comment. - /// The URI. - Uri GetCommentUrl(int id); - /// /// Gets the comments in the thread. /// @@ -25,21 +20,21 @@ public interface ICommentThreadViewModel /// /// Gets the current user under whos account new comments will be created. /// - IAccount CurrentUser { get; } + IActorViewModel CurrentUser { get; } /// /// Called by a comment in the thread to post itself as a new comment to the API. /// - ReactiveCommand PostComment { get; } + ReactiveCommand PostComment { get; } /// /// Called by a comment in the thread to post itself as an edit to a comment to the API. /// - ReactiveCommand EditComment { get; } + ReactiveCommand EditComment { get; } /// /// Called by a comment in the thread to send a delete of the comment to the API. /// - ReactiveCommand DeleteComment { get; } + ReactiveCommand DeleteComment { get; } } } diff --git a/src/GitHub.InlineReviews/ViewModels/ICommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/ICommentViewModel.cs index fdd0fb2733..8f5e2d292c 100644 --- a/src/GitHub.InlineReviews/ViewModels/ICommentViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/ICommentViewModel.cs @@ -18,15 +18,10 @@ public enum CommentEditState /// public interface ICommentViewModel : IViewModel { - /// - /// Gets the ID of the comment. - /// - int Id { get; } - /// /// Gets the GraphQL ID of the comment. /// - string NodeId { get; } + string Id { get; } /// /// Gets or sets the body of the comment. @@ -67,13 +62,18 @@ public interface ICommentViewModel : IViewModel /// /// Gets the author of the comment. /// - IAccount User { get; } + IActorViewModel Author { get; } /// /// Gets the thread that the comment is a part of. /// ICommentThreadViewModel Thread { get; } + /// + /// Gets the URL of the comment on the web. + /// + Uri WebUrl { get; } + /// /// Gets a command which will begin editing of the comment. /// diff --git a/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs index 9e3e7bd76a..2ada559882 100644 --- a/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs @@ -124,6 +124,7 @@ public async Task Initialize() else { relativePath = sessionManager.GetRelativePath(buffer); + side = DiffSide.Right; file = await sessionManager.GetLiveFile(relativePath, peekSession.TextView, buffer); await SessionChanged(sessionManager.CurrentSession); sessionSubscription = sessionManager.WhenAnyValue(x => x.CurrentSession) diff --git a/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs index 598655d42f..7ec86f0768 100644 --- a/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs @@ -3,11 +3,9 @@ using System.Globalization; using System.Reactive.Linq; using System.Threading.Tasks; -using GitHub.Api; using GitHub.Extensions; using GitHub.Models; using GitHub.Services; -using Octokit; using ReactiveUI; namespace GitHub.InlineReviews.ViewModels @@ -24,7 +22,7 @@ public class InlineCommentThreadViewModel : CommentThreadViewModel /// The comments to display in this inline review. public InlineCommentThreadViewModel( IPullRequestSession session, - IEnumerable comments) + IEnumerable comments) : base(session.User) { Guard.ArgumentNotNull(session, nameof(session)); @@ -45,7 +43,12 @@ public class InlineCommentThreadViewModel : CommentThreadViewModel foreach (var comment in comments) { - Comments.Add(new PullRequestReviewCommentViewModel(session, this, CurrentUser, comment)); + Comments.Add(new PullRequestReviewCommentViewModel( + session, + this, + CurrentUser, + comment.Review, + comment.Comment)); } Comments.Add(PullRequestReviewCommentViewModel.CreatePlaceholder(session, this, CurrentUser)); @@ -56,42 +59,29 @@ public class InlineCommentThreadViewModel : CommentThreadViewModel /// public IPullRequestSession Session { get; } - /// - public override Uri GetCommentUrl(int id) - { - return new Uri(string.Format( - CultureInfo.InvariantCulture, - "{0}/pull/{1}#discussion_r{2}", - Session.LocalRepository.CloneUrl.ToRepositoryUrl(Session.RepositoryOwner), - Session.PullRequest.Number, - id)); - } - - async Task DoPostComment(object parameter) + async Task DoPostComment(object parameter) { Guard.ArgumentNotNull(parameter, nameof(parameter)); var body = (string)parameter; - var nodeId = Comments[0].NodeId; - return await Session.PostReviewComment(body, nodeId); + var replyId = Comments[0].Id; + await Session.PostReviewComment(body, replyId); } - async Task DoEditComment(object parameter) + async Task DoEditComment(object parameter) { Guard.ArgumentNotNull(parameter, nameof(parameter)); var item = (Tuple)parameter; - return await Session.EditComment(item.Item1, item.Item2); + await Session.EditComment(item.Item1, item.Item2); } - async Task DoDeleteComment(object parameter) + async Task DoDeleteComment(object parameter) { Guard.ArgumentNotNull(parameter, nameof(parameter)); var number = (int)parameter; await Session.DeleteComment(number); - - return new object(); } } } diff --git a/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs index a0ca7a2f98..17096ae220 100644 --- a/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs @@ -45,11 +45,11 @@ public class NewInlineCommentThreadViewModel : CommentThreadViewModel this.WhenAnyValue(x => x.NeedsPush, x => !x), DoPostComment); - EditComment = ReactiveCommand.CreateAsyncTask( + EditComment = ReactiveCommand.CreateAsyncTask( Observable.Return(false), o => null); - DeleteComment = ReactiveCommand.CreateAsyncTask( + DeleteComment = ReactiveCommand.CreateAsyncTask( Observable.Return(false), o => null); @@ -91,13 +91,7 @@ public bool NeedsPush private set { this.RaiseAndSetIfChanged(ref needsPush, value); } } - /// - public override Uri GetCommentUrl(int id) - { - throw new NotSupportedException("Cannot navigate to a non-posted comment."); - } - - async Task DoPostComment(object parameter) + async Task DoPostComment(object parameter) { Guard.ArgumentNotNull(parameter, nameof(parameter)); @@ -115,14 +109,12 @@ async Task DoPostComment(object parameter) } var body = (string)parameter; - var model = await Session.PostReviewComment( + await Session.PostReviewComment( body, File.CommitSha, File.RelativePath.Replace("\\", "/"), File.Diff, diffPosition.DiffLineNumber); - - return model; } } } diff --git a/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs index cce40609e6..a311582bbb 100644 --- a/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs +++ b/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs @@ -8,6 +8,7 @@ using GitHub.Logging; using GitHub.Models; using GitHub.Services; +using GitHub.ViewModels; using GitHub.VisualStudio.UI; using ReactiveUI; using Serilog; @@ -29,25 +30,24 @@ public class PullRequestReviewCommentViewModel : CommentViewModel, IPullRequestR /// The pull request session. /// The thread that the comment is a part of. /// The current user. - /// The REST ID of the comment. - /// The GraphQL ID of the comment. + /// The GraphQL ID of the comment. /// The comment body. /// The comment edit state. - /// The author of the comment. + /// The author of the comment. /// The modified date of the comment. /// Whether this is a pending comment. public PullRequestReviewCommentViewModel( IPullRequestSession session, ICommentThreadViewModel thread, - IAccount currentUser, - int commentId, - string commentNodeId, + IActorViewModel currentUser, + string commentId, string body, CommentEditState state, - IAccount user, + IActorViewModel author, DateTimeOffset updatedAt, - bool isPending) - : base(thread, currentUser, commentId, commentNodeId, body, state, user, updatedAt) + bool isPending, + Uri webUrl) + : base(thread, currentUser, commentId, body, state, author, updatedAt, webUrl) { Guard.ArgumentNotNull(session, nameof(session)); @@ -56,7 +56,7 @@ public class PullRequestReviewCommentViewModel : CommentViewModel, IPullRequestR var pendingReviewAndIdObservable = Observable.CombineLatest( session.WhenAnyValue(x => x.HasPendingReview, x => !x), - this.WhenAnyValue(model => model.Id, i => i == 0), + this.WhenAnyValue(model => model.Id).Select(i => i == null), (hasPendingReview, isNewComment) => new { hasPendingReview, isNewComment }); canStartReview = pendingReviewAndIdObservable @@ -83,9 +83,20 @@ public class PullRequestReviewCommentViewModel : CommentViewModel, IPullRequestR public PullRequestReviewCommentViewModel( IPullRequestSession session, ICommentThreadViewModel thread, - IAccount currentUser, - IPullRequestReviewCommentModel model) - : this(session, thread, currentUser, model.Id, model.NodeId, model.Body, CommentEditState.None, model.User, model.CreatedAt, model.IsPending) + IActorViewModel currentUser, + PullRequestReviewModel review, + PullRequestReviewCommentModel model) + : this( + session, + thread, + currentUser, + model.Id, + model.Body, + CommentEditState.None, + new ActorViewModel(model.Author), + model.CreatedAt, + review.State == PullRequestReviewState.Pending, + model.Url != null ? new Uri(model.Url) : null) { } @@ -99,19 +110,19 @@ public class PullRequestReviewCommentViewModel : CommentViewModel, IPullRequestR public static CommentViewModel CreatePlaceholder( IPullRequestSession session, ICommentThreadViewModel thread, - IAccount currentUser) + IActorViewModel currentUser) { return new PullRequestReviewCommentViewModel( session, thread, currentUser, - 0, null, string.Empty, CommentEditState.Placeholder, currentUser, DateTimeOffset.MinValue, - false); + false, + null); } /// diff --git a/src/GitHub.InlineReviews/Views/CommentView.xaml b/src/GitHub.InlineReviews/Views/CommentView.xaml index 5491c741a0..084f4d4409 100644 --- a/src/GitHub.InlineReviews/Views/CommentView.xaml +++ b/src/GitHub.InlineReviews/Views/CommentView.xaml @@ -12,7 +12,7 @@ xmlns:views="clr-namespace:GitHub.InlineReviews.Views" mc:Ignorable="d" d:DesignWidth="300"> - + 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). @@ -54,7 +54,7 @@ + Account="{Binding Author}"/> - + diff --git a/src/GitHub.UI/Controls/TrimmedPathTextBlock.cs b/src/GitHub.UI/Controls/TrimmedPathTextBlock.cs index d272f9fd77..84b000c8e6 100644 --- a/src/GitHub.UI/Controls/TrimmedPathTextBlock.cs +++ b/src/GitHub.UI/Controls/TrimmedPathTextBlock.cs @@ -106,6 +106,11 @@ protected FormattedText FormattedText protected override Size MeasureOverride(Size availableSize) { + if (Text == null) + { + return new Size(); + } + var parts = Text .Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }) .ToList(); diff --git a/src/GitHub.VisualStudio.UI/UI/Controls/AccountAvatar.xaml.cs b/src/GitHub.VisualStudio.UI/UI/Controls/AccountAvatar.xaml.cs index b1629a71a4..7570da7e40 100644 --- a/src/GitHub.VisualStudio.UI/UI/Controls/AccountAvatar.xaml.cs +++ b/src/GitHub.VisualStudio.UI/UI/Controls/AccountAvatar.xaml.cs @@ -11,7 +11,7 @@ public partial class AccountAvatar : UserControl, ICommandSource public static readonly DependencyProperty AccountProperty = DependencyProperty.Register( nameof(Account), - typeof(IAccount), + typeof(object), typeof(AccountAvatar)); public static readonly DependencyProperty CommandProperty = ButtonBase.CommandProperty.AddOwner(typeof(AccountAvatar)); @@ -25,9 +25,9 @@ public AccountAvatar() InitializeComponent(); } - public IAccount Account + public object Account { - get { return (IAccount)GetValue(AccountProperty); } + get { return GetValue(AccountProperty); } set { SetValue(AccountProperty, value); } } diff --git a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj index 5edeed4588..0539195cc3 100644 --- a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj +++ b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj @@ -253,12 +253,12 @@ ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll True - - ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.dll + + ..\..\packages\Octokit.GraphQL.0.0.4-alpha\lib\netstandard1.1\Octokit.GraphQL.dll True - - ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + ..\..\packages\Octokit.GraphQL.0.0.4-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll True @@ -372,6 +372,7 @@ OptionsControl.xaml + ForkRepositoryExecuteView.xaml diff --git a/src/GitHub.VisualStudio/Views/ActorAvatarView.cs b/src/GitHub.VisualStudio/Views/ActorAvatarView.cs new file mode 100644 index 0000000000..12b4b4d38f --- /dev/null +++ b/src/GitHub.VisualStudio/Views/ActorAvatarView.cs @@ -0,0 +1,181 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.Composition; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using GitHub.Exports; +using GitHub.ViewModels; + +namespace GitHub.VisualStudio.Views +{ + [ExportViewFor(typeof(IActorViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class ActorAvatarView : FrameworkElement, ICommandSource + { + public static readonly DependencyProperty CommandProperty = + ButtonBase.CommandProperty.AddOwner(typeof(ActorAvatarView)); + public static readonly DependencyProperty CommandParameterProperty = + ButtonBase.CommandParameterProperty.AddOwner(typeof(ActorAvatarView)); + public static readonly DependencyProperty CommandTargetProperty = + ButtonBase.CommandTargetProperty.AddOwner(typeof(ActorAvatarView)); + + public static readonly DependencyProperty IdleUpdateProperty = + DependencyProperty.Register( + nameof(IdleUpdate), + typeof(bool), + typeof(ActorAvatarView), + new FrameworkPropertyMetadata(false)); + + public static readonly DependencyProperty ViewModelProperty = + DependencyProperty.Register( + nameof(ViewModel), + typeof(IActorViewModel), + typeof(ActorAvatarView), + new FrameworkPropertyMetadata(HandleViewModelChanged)); + + IActorViewModel toRender; + Geometry clip; + + static ActorAvatarView() + { + WidthProperty.OverrideMetadata(typeof(ActorAvatarView), new FrameworkPropertyMetadata(30.0)); + HeightProperty.OverrideMetadata(typeof(ActorAvatarView), new FrameworkPropertyMetadata(30.0)); + } + + public ActorAvatarView() + { + } + + public bool IdleUpdate + { + get { return (bool)GetValue(IdleUpdateProperty); } + set { SetValue(IdleUpdateProperty, value); } + } + + public IActorViewModel ViewModel + { + get { return (IActorViewModel)GetValue(ViewModelProperty); } + set { SetValue(ViewModelProperty, value); } + } + + public ICommand Command + { + get { return (ICommand)GetValue(CommandProperty); } + set { SetValue(CommandProperty, value); } + } + + public object CommandParameter + { + get { return GetValue(CommandParameterProperty); } + set { SetValue(CommandParameterProperty, value); } + } + + public IInputElement CommandTarget + { + get { return (IInputElement)GetValue(CommandTargetProperty); } + set { SetValue(CommandTargetProperty, value); } + } + + protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) + { + this.CaptureMouse(); + } + + protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) + { + if (this.IsMouseCaptured) + { + if (this.InputHitTest(e.GetPosition(this)) != null && + Command?.CanExecute(CommandParameter) == true) + { + Command.Execute(CommandParameter); + } + + this.ReleaseMouseCapture(); + } + } + + protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + { + clip = null; + } + + protected override void OnRender(DrawingContext drawingContext) + { + if (toRender != null) + { + if (clip == null) + { + clip = new RectangleGeometry(new Rect(0, 0, ActualWidth, ActualHeight), 3, 3); + } + + drawingContext.PushClip(clip); + drawingContext.DrawImage(toRender.Avatar, new Rect(0, 0, ActualWidth, ActualHeight)); + drawingContext.Pop(); + } + } + + void ViewModelChanged(DependencyPropertyChangedEventArgs e) + { + var oldValue = e.OldValue as IActorViewModel; + var newValue = e.NewValue as IActorViewModel; + + if (oldValue != null) + { + oldValue.PropertyChanged -= ViewModelPropertyChanged; + } + + if (IdleUpdate) + { + QueueIdleUpdate(); + } + else + { + toRender = newValue; + + if (toRender != null) + { + toRender.PropertyChanged += ViewModelPropertyChanged; + } + } + + InvalidateVisual(); + } + + void QueueIdleUpdate() + { + toRender = null; + ComponentDispatcher.ThreadIdle += OnIdle; + } + + void OnIdle(object sender, EventArgs e) + { + toRender = ViewModel; + + if (toRender != null) + { + toRender.PropertyChanged += ViewModelPropertyChanged; + } + + InvalidateVisual(); + ComponentDispatcher.ThreadIdle -= OnIdle; + } + + void ViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IActorViewModel.Avatar)) + { + QueueIdleUpdate(); + } + } + + static void HandleViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + (d as ActorAvatarView)?.ViewModelChanged(e); + } + } +} diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml index 34d16abf5c..8813d36270 100644 --- a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml +++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml @@ -4,6 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:ghfvs="https://github.com/github/VisualStudio" xmlns:local="clr-namespace:GitHub.VisualStudio.Views.GitHubPane" + xmlns:v="clr-namespace:GitHub.VisualStudio.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:prop="clr-namespace:GitHub.VisualStudio.UI;assembly=GitHub.VisualStudio.UI" xmlns:markdig="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf" @@ -212,10 +213,11 @@ - + diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestReviewSummaryView.xaml b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestReviewSummaryView.xaml index 80e4b85d52..30197716b2 100644 --- a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestReviewSummaryView.xaml +++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestReviewSummaryView.xaml @@ -4,7 +4,9 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:GitHub.VisualStudio.Views.GitHubPane" + xmlns:ghfvs="https://github.com/github/VisualStudio" xmlns:c="clr-namespace:GitHub.VisualStudio.UI.Controls;assembly=GitHub.VisualStudio.UI" + xmlns:v="clr-namespace:GitHub.VisualStudio.Views" xmlns:ui="clr-namespace:GitHub.UI;assembly=GitHub.UI" xmlns:vm="clr-namespace:GitHub.ViewModels.GitHubPane;assembly=GitHub.App" xmlns:cache="clr-namespace:GitHub.UI.Helpers;assembly=GitHub.UI" @@ -16,7 +18,7 @@ - + @@ -33,7 +35,7 @@ + Visibility="{Binding Id, Converter={ui:NotEqualsToVisibilityConverter {x:Null}}}">