diff --git a/lib/Octokit.GraphQL.0.0.1.nupkg b/lib/Octokit.GraphQL.0.0.1.nupkg new file mode 100644 index 0000000000..589a5e0a76 Binary files /dev/null and b/lib/Octokit.GraphQL.0.0.1.nupkg differ diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index a825cfc079..f66d2730ad 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -46,6 +46,18 @@ + + ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\Octokit.GraphQL.0.0.1\lib\netstandard1.1\Octokit.GraphQL.dll + True + + + ..\..\packages\Octokit.GraphQL.0.0.1\lib\netstandard1.1\Octokit.GraphQL.Core.dll + True + ..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll True @@ -66,6 +78,9 @@ ApiClientConfiguration_User.cs + + + diff --git a/src/GitHub.Api/GraphQLClientFactory.cs b/src/GitHub.Api/GraphQLClientFactory.cs new file mode 100644 index 0000000000..8151832a16 --- /dev/null +++ b/src/GitHub.Api/GraphQLClientFactory.cs @@ -0,0 +1,41 @@ +using System; +using System.ComponentModel.Composition; +using System.Threading.Tasks; +using GitHub.Models; +using GitHub.Primitives; +using Octokit.GraphQL; + +namespace GitHub.Api +{ + /// + /// Creates GraphQL s for querying the + /// GitHub GraphQL API. + /// + [Export(typeof(IGraphQLClientFactory))] + [PartCreationPolicy(CreationPolicy.Shared)] + public class GraphQLClientFactory : IGraphQLClientFactory + { + readonly IKeychain keychain; + readonly IProgram program; + + /// + /// Initializes a new instance of the class. + /// + /// The to use. + /// The program details. + [ImportingConstructor] + public GraphQLClientFactory(IKeychain keychain, IProgram program) + { + this.keychain = keychain; + this.program = program; + } + + /// + public Task CreateConnection(HostAddress address) + { + var credentials = new GraphQLKeychainCredentialStore(keychain, address); + var header = new ProductHeaderValue(program.ProductHeader.Name, program.ProductHeader.Version); + return Task.FromResult(new Connection(header, credentials)); + } + } +} diff --git a/src/GitHub.Api/GraphQLKeychainCredentialStore.cs b/src/GitHub.Api/GraphQLKeychainCredentialStore.cs new file mode 100644 index 0000000000..0098d15983 --- /dev/null +++ b/src/GitHub.Api/GraphQLKeychainCredentialStore.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; +using GitHub.Extensions; +using GitHub.Primitives; +using Octokit.GraphQL; + +namespace GitHub.Api +{ + /// + /// An Octokit.GraphQL credential store that reads from an . + /// + public class GraphQLKeychainCredentialStore : ICredentialStore + { + readonly IKeychain keychain; + readonly HostAddress address; + + public GraphQLKeychainCredentialStore(IKeychain keychain, HostAddress address) + { + Guard.ArgumentNotNull(keychain, nameof(keychain)); + Guard.ArgumentNotNull(address, nameof(keychain)); + + this.keychain = keychain; + this.address = address; + } + + public async Task GetCredentials() + { + var userPass = await keychain.Load(address).ConfigureAwait(false); + return userPass?.Item2; + } + } +} diff --git a/src/GitHub.Api/IGraphQLClientFactory.cs b/src/GitHub.Api/IGraphQLClientFactory.cs new file mode 100644 index 0000000000..464fab0de8 --- /dev/null +++ b/src/GitHub.Api/IGraphQLClientFactory.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using GitHub.Primitives; + +namespace GitHub.Api +{ + /// + /// Creates GraphQL s for querying the + /// GitHub GraphQL API. + /// + public interface IGraphQLClientFactory + { + /// + /// Creates a new . + /// + /// The address of the server. + /// A task returning the created connection. + Task CreateConnection(HostAddress address); + } +} \ No newline at end of file diff --git a/src/GitHub.Api/packages.config b/src/GitHub.Api/packages.config index bfb877d0d6..d5d4862325 100644 --- a/src/GitHub.Api/packages.config +++ b/src/GitHub.Api/packages.config @@ -1,4 +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 8a6188b6bd..5ac1d26cdc 100644 --- a/src/GitHub.App/Factories/ModelServiceFactory.cs +++ b/src/GitHub.App/Factories/ModelServiceFactory.cs @@ -3,6 +3,7 @@ using System.ComponentModel.Composition; using System.Threading; using System.Threading.Tasks; +using GitHub.Api; using GitHub.Caches; using GitHub.Models; using GitHub.Services; @@ -15,6 +16,7 @@ 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(); @@ -23,10 +25,12 @@ 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; } @@ -43,6 +47,7 @@ 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 17331a586e..3e9bff0d12 100644 --- a/src/GitHub.App/GitHub.App.csproj +++ b/src/GitHub.App/GitHub.App.csproj @@ -132,15 +132,22 @@ False ..\..\packages\Microsoft.VisualStudio.Threading.14.1.131\lib\net45\Microsoft.VisualStudio.Threading.dll + + ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\Octokit.GraphQL.0.0.1\lib\netstandard1.1\Octokit.GraphQL.dll + True + + + ..\..\packages\Octokit.GraphQL.0.0.1\lib\netstandard1.1\Octokit.GraphQL.Core.dll + True + ..\..\packages\Microsoft.VisualStudio.Utilities.14.3.25407\lib\net45\Microsoft.VisualStudio.Utilities.dll True - - False - ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - False - @@ -199,6 +206,7 @@ + diff --git a/src/GitHub.App/Models/Account.cs b/src/GitHub.App/Models/Account.cs index e7a61c3146..b2b07462aa 100644 --- a/src/GitHub.App/Models/Account.cs +++ b/src/GitHub.App/Models/Account.cs @@ -23,6 +23,7 @@ public Account( bool isEnterprise, int ownedPrivateRepositoryCount, long privateRepositoryInPlanCount, + string avatarUrl, IObservable bitmapSource) { Guard.ArgumentNotEmptyString(login, nameof(login)); @@ -34,6 +35,7 @@ public Account( PrivateReposInPlan = privateRepositoryInPlanCount; IsOnFreePlan = privateRepositoryInPlanCount == 0; HasMaximumPrivateRepositories = OwnedPrivateRepos >= PrivateReposInPlan; + AvatarUrl = avatarUrl; this.bitmapSource = bitmapSource; bitmapSourceSubscription = bitmapSource @@ -54,6 +56,7 @@ public Account(Octokit.Account account) OwnedPrivateRepos = account.OwnedPrivateRepos; IsOnFreePlan = PrivateReposInPlan == 0; HasMaximumPrivateRepositories = OwnedPrivateRepos >= PrivateReposInPlan; + AvatarUrl = account.AvatarUrl; } public Account(Octokit.Account account, IObservable bitmapSource) @@ -77,6 +80,8 @@ public Account(Octokit.Account account, IObservable bitmapSource) public long PrivateReposInPlan { get; private set; } + public string AvatarUrl { get; private set; } + public BitmapSource Avatar { get { return avatar; } @@ -115,7 +120,7 @@ public override bool Equals(object obj) public override int GetHashCode() { - return (Login?.GetHashCode() ?? 0) ^ IsUser .GetHashCode() ^ IsEnterprise.GetHashCode(); + return (Login?.GetHashCode() ?? 0) ^ IsUser.GetHashCode() ^ IsEnterprise.GetHashCode(); } bool IEquatable.Equals(IAccount other) diff --git a/src/GitHub.App/Models/IssueCommentModel.cs b/src/GitHub.App/Models/IssueCommentModel.cs index f4e4ddfe5e..5031aa2bd4 100644 --- a/src/GitHub.App/Models/IssueCommentModel.cs +++ b/src/GitHub.App/Models/IssueCommentModel.cs @@ -4,8 +4,9 @@ namespace GitHub.Models { public class IssueCommentModel : ICommentModel { - public string Body { get; set; } 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/PullRequestModel.cs b/src/GitHub.App/Models/PullRequestModel.cs index 6ae87f3012..8e9d6439d7 100644 --- a/src/GitHub.App/Models/PullRequestModel.cs +++ b/src/GitHub.App/Models/PullRequestModel.cs @@ -162,11 +162,23 @@ public string Body public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; } public IAccount Author { get; set; } - public IReadOnlyCollection ChangedFiles { get; set; } = new IPullRequestFileModel[0]; - public IReadOnlyCollection Comments { get; set; } = new ICommentModel[0]; + public IReadOnlyList ChangedFiles { get; set; } = new IPullRequestFileModel[0]; + public IReadOnlyList Comments { get; set; } = new ICommentModel[0]; - IReadOnlyCollection reviewComments = new IPullRequestReviewCommentModel[0]; - public IReadOnlyCollection ReviewComments + 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 diff --git a/src/GitHub.App/Models/PullRequestReviewCommentModel.cs b/src/GitHub.App/Models/PullRequestReviewCommentModel.cs index 1a073f84c0..7ae45548e6 100644 --- a/src/GitHub.App/Models/PullRequestReviewCommentModel.cs +++ b/src/GitHub.App/Models/PullRequestReviewCommentModel.cs @@ -5,6 +5,8 @@ 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; } @@ -14,5 +16,6 @@ public class PullRequestReviewCommentModel : IPullRequestReviewCommentModel 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 new file mode 100644 index 0000000000..ff7ad40d13 --- /dev/null +++ b/src/GitHub.App/Models/PullRequestReviewModel.cs @@ -0,0 +1,14 @@ +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; } + } +} diff --git a/src/GitHub.App/SampleData/AccountDesigner.cs b/src/GitHub.App/SampleData/AccountDesigner.cs index 8d555e2cdb..68770f8336 100644 --- a/src/GitHub.App/SampleData/AccountDesigner.cs +++ b/src/GitHub.App/SampleData/AccountDesigner.cs @@ -32,6 +32,7 @@ public BitmapSource Avatar public string Login { get; set; } public int OwnedPrivateRepos { get; set; } public long PrivateReposInPlan { get; set; } + public string AvatarUrl { get; set; } public override string ToString() { diff --git a/src/GitHub.App/Services/AvatarProvider.cs b/src/GitHub.App/Services/AvatarProvider.cs index 5ed2428434..1a99e815f6 100644 --- a/src/GitHub.App/Services/AvatarProvider.cs +++ b/src/GitHub.App/Services/AvatarProvider.cs @@ -70,6 +70,21 @@ public IObservable GetAvatar(IAvatarContainer apiAccount) .Catch(_ => Observable.Return(DefaultAvatar(apiAccount))); } + public IObservable GetAvatar(string url) + { + if (url == null) + { + return Observable.Return(DefaultUserBitmapImage); + } + + Uri avatarUrl; + Uri.TryCreate(url, UriKind.Absolute, out avatarUrl); + Log.Assert(avatarUrl != null, "Cannot have a null avatar url"); + + return imageCache.GetImage(avatarUrl) + .Catch(_ => Observable.Return(DefaultUserBitmapImage)); + } + public IObservable InvalidateAvatar(IAvatarContainer apiAccount) { return String.IsNullOrWhiteSpace(apiAccount?.Login) diff --git a/src/GitHub.App/Services/ModelService.cs b/src/GitHub.App/Services/ModelService.cs index e50eae69ec..15f1321021 100644 --- a/src/GitHub.App/Services/ModelService.cs +++ b/src/GitHub.App/Services/ModelService.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reactive; using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; using System.Threading.Tasks; using Akavache; using GitHub.Api; @@ -17,7 +18,9 @@ using GitHub.Models; using GitHub.Primitives; using Octokit; +using Octokit.GraphQL; using Serilog; +using static Octokit.GraphQL.Variable; namespace GitHub.Services { @@ -33,13 +36,16 @@ 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; } @@ -203,19 +209,22 @@ public IObservable GetPullRequest(string owner, string name, ApiClient.GetPullRequest(owner, name, number), ApiClient.GetPullRequestFiles(owner, name, number).ToList(), ApiClient.GetIssueComments(owner, name, number).ToList(), - ApiClient.GetPullRequestReviewComments(owner, name, number).ToList(), - (pr, files, comments, reviewComments) => new + 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.ReviewComments)), + (IReadOnlyList)x.Reviews, + (IReadOnlyList)x.ReviewComments)), TimeSpan.Zero, TimeSpan.FromDays(7)) .Select(Create); @@ -366,6 +375,158 @@ 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), + 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() @@ -394,9 +555,22 @@ IAccount Create(AccountCacheItem accountCacheItem) accountCacheItem.IsEnterprise, accountCacheItem.OwnedPrivateRepositoriesCount, accountCacheItem.PrivateRepositoriesInPlanCount, + accountCacheItem.AvatarUrl, avatarProvider.GetAvatar(accountCacheItem)); } + IAccount Create(string login, string avatarUrl) + { + return new Models.Account( + login, + true, + false, + 0, + 0, + avatarUrl, + avatarProvider.GetAvatar(avatarUrl)); + } + IRemoteRepositoryModel Create(RepositoryCacheItem item) { return new RemoteRepositoryModel( @@ -440,10 +614,22 @@ IPullRequestModel Create(PullRequestCacheItem prCacheItem) 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, + }).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, @@ -453,6 +639,7 @@ IPullRequestModel Create(PullRequestCacheItem prCacheItem) User = Create(x.User), Body = x.Body, CreatedAt = x.CreatedAt, + IsPending = x.IsPending, }).ToList(), CommentCount = prCacheItem.CommentCount, CommitCount = prCacheItem.CommitCount, @@ -478,6 +665,11 @@ public void Dispose() GC.SuppressFinalize(this); } + static GitHub.Models.PullRequestReviewState FromGraphQL(Octokit.GraphQL.Model.PullRequestReviewState s) + { + return (GitHub.Models.PullRequestReviewState)s; + } + public class GitIgnoreCacheItem : CacheItem { public static GitIgnoreCacheItem Create(string ignore) @@ -539,22 +731,28 @@ public class PullRequestCacheItem : CacheItem { public static PullRequestCacheItem Create(PullRequest pr) { - return new PullRequestCacheItem(pr, new PullRequestFile[0], new IssueComment[0], new PullRequestReviewComment[0]); + 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 reviewComments) + IReadOnlyList reviews, + IReadOnlyList reviewComments) { - return new PullRequestCacheItem(pr, files, comments, reviewComments); + return new PullRequestCacheItem(pr, files, comments, reviews, reviewComments); } public PullRequestCacheItem() {} public PullRequestCacheItem(PullRequest pr) - : this(pr, new PullRequestFile[0], new IssueComment[0], new PullRequestReviewComment[0]) + : this(pr, new PullRequestFile[0], new IssueComment[0], new IPullRequestReviewModel[0], new IPullRequestReviewCommentModel[0]) { } @@ -562,7 +760,8 @@ public PullRequestCacheItem( PullRequest pr, IReadOnlyList files, IReadOnlyList comments, - IReadOnlyList reviewComments) + IReadOnlyList reviews, + IReadOnlyList reviewComments) { Title = pr.Title; Number = pr.Number; @@ -580,7 +779,7 @@ public PullRequestCacheItem( Sha = pr.Head.Sha, RepositoryCloneUrl = pr.Head.Repository?.CloneUrl }; - CommentCount = pr.Comments + pr.ReviewComments; + CommentCount = pr.Comments; CommitCount = pr.Commits; Author = new AccountCacheItem(pr.User); Assignee = pr.Assignee != null ? new AccountCacheItem(pr.Assignee) : null; @@ -589,6 +788,7 @@ public PullRequestCacheItem( 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; @@ -610,6 +810,7 @@ public PullRequestCacheItem( 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. @@ -674,27 +875,63 @@ public IssueCommentCacheItem(IssueComment comment) 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; + } + + 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 class PullRequestReviewCommentCacheItem { public PullRequestReviewCommentCacheItem() { } - public PullRequestReviewCommentCacheItem(PullRequestReviewComment comment) + 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(comment.User); + 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; } @@ -704,6 +941,7 @@ public PullRequestReviewCommentCacheItem(PullRequestReviewComment comment) public AccountCacheItem User { get; set; } public string Body { get; set; } public DateTimeOffset CreatedAt { get; set; } + public bool IsPending { get; set; } } public class GitReferenceCacheItem diff --git a/src/GitHub.App/ViewModels/GitHubPane/PullRequestListViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/PullRequestListViewModel.cs index 88f79ffb9d..bed4cb897f 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/PullRequestListViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/PullRequestListViewModel.cs @@ -309,7 +309,7 @@ public IAccount SelectedAssignee set { this.RaiseAndSetIfChanged(ref selectedAssignee, value); } } - IAccount emptyUser = new Account("[None]", false, false, 0, 0, Observable.Empty()); + IAccount emptyUser = new Account("[None]", false, false, 0, 0, string.Empty, Observable.Empty()); public IAccount EmptyUser { get { return emptyUser; } diff --git a/src/GitHub.App/packages.config b/src/GitHub.App/packages.config index 03a098bbfb..604d5310a1 100644 --- a/src/GitHub.App/packages.config +++ b/src/GitHub.App/packages.config @@ -20,7 +20,8 @@ - + + diff --git a/src/GitHub.Exports.Reactive/Services/IAvatarProvider.cs b/src/GitHub.Exports.Reactive/Services/IAvatarProvider.cs index ccf656280a..1006256d96 100644 --- a/src/GitHub.Exports.Reactive/Services/IAvatarProvider.cs +++ b/src/GitHub.Exports.Reactive/Services/IAvatarProvider.cs @@ -10,6 +10,7 @@ public interface IAvatarProvider : IDisposable BitmapImage DefaultUserBitmapImage { get; } BitmapImage DefaultOrgBitmapImage { get; } IObservable GetAvatar(IAvatarContainer account); + IObservable GetAvatar(string avatarUri); IObservable InvalidateAvatar(IAvatarContainer account); } } diff --git a/src/GitHub.Exports/GitHub.Exports.csproj b/src/GitHub.Exports/GitHub.Exports.csproj index 43bc622b3b..2ac5424a96 100644 --- a/src/GitHub.Exports/GitHub.Exports.csproj +++ b/src/GitHub.Exports/GitHub.Exports.csproj @@ -167,6 +167,7 @@ + diff --git a/src/GitHub.Exports/Models/IAccount.cs b/src/GitHub.Exports/Models/IAccount.cs index 313d677944..617d770b91 100644 --- a/src/GitHub.Exports/Models/IAccount.cs +++ b/src/GitHub.Exports/Models/IAccount.cs @@ -14,6 +14,7 @@ public interface IAccount : ICopyable, string Login { get; } int OwnedPrivateRepos { get; } long PrivateReposInPlan { get; } - BitmapSource Avatar { get; } + string AvatarUrl { get; } + BitmapSource Avatar { get; } } } diff --git a/src/GitHub.Exports/Models/ICommentModel.cs b/src/GitHub.Exports/Models/ICommentModel.cs index 82fc036c30..121c7862fd 100644 --- a/src/GitHub.Exports/Models/ICommentModel.cs +++ b/src/GitHub.Exports/Models/ICommentModel.cs @@ -12,6 +12,11 @@ public interface ICommentModel /// int Id { get; } + /// + /// Gets the GraphQL ID of the comment. + /// + string NodeId { get; } + /// /// Gets the author of the comment. /// diff --git a/src/GitHub.Exports/Models/IPullRequestModel.cs b/src/GitHub.Exports/Models/IPullRequestModel.cs index f185c2ef44..40996c78dc 100644 --- a/src/GitHub.Exports/Models/IPullRequestModel.cs +++ b/src/GitHub.Exports/Models/IPullRequestModel.cs @@ -31,8 +31,9 @@ public interface IPullRequestModel : ICopyable, DateTimeOffset UpdatedAt { get; } IAccount Author { get; } IAccount Assignee { get; } - IReadOnlyCollection ChangedFiles { get; } - IReadOnlyCollection Comments { get; } - IReadOnlyCollection ReviewComments { get; set; } + 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 index b695b116d9..f6d376bb13 100644 --- a/src/GitHub.Exports/Models/IPullRequestReviewCommentModel.cs +++ b/src/GitHub.Exports/Models/IPullRequestReviewCommentModel.cs @@ -7,6 +7,11 @@ namespace GitHub.Models /// 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. /// @@ -38,5 +43,10 @@ public interface IPullRequestReviewCommentModel : ICommentModel /// 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; } } } diff --git a/src/GitHub.Exports/Models/IPullRequestReviewModel.cs b/src/GitHub.Exports/Models/IPullRequestReviewModel.cs new file mode 100644 index 0000000000..d59d401e1e --- /dev/null +++ b/src/GitHub.Exports/Models/IPullRequestReviewModel.cs @@ -0,0 +1,71 @@ +using System; + +namespace GitHub.Models +{ + /// + /// The possible states of a pull request review. + /// + public enum PullRequestReviewState + { + /// + /// A review that has not yet been submitted. + /// + Pending, + + /// + /// An informational review. + /// + Commented, + + /// + /// A review allowing the pull request to merge. + /// + Approved, + + /// + /// A review blocking the pull request from merging. + /// + ChangesRequested, + + /// + /// A review that has been dismissed. + /// + Dismissed, + } + + /// + /// Represents a review of a pull request. + /// + public interface IPullRequestReviewModel + { + /// + /// Gets the ID of the review. + /// + long Id { get; } + + /// + /// Gets the GraphQL ID for the review. + /// + string NodeId { get; set; } + + /// + /// Gets the author of the review. + /// + IAccount User { get; } + + /// + /// Gets the body of the review. + /// + string Body { get; } + + /// + /// Gets the state of the review. + /// + PullRequestReviewState State { get; } + + /// + /// Gets the SHA of the commit that the review was submitted on. + /// + string CommitId { get; } + } +} diff --git a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj index 14a5784b4f..acf8349a34 100644 --- a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj +++ b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj @@ -235,8 +235,16 @@ ..\..\packages\Microsoft.VisualStudio.Validation.14.1.111\lib\net45\Microsoft.VisualStudio.Validation.dll True - - ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\Octokit.GraphQL.0.0.1\lib\netstandard1.1\Octokit.GraphQL.dll + True + + + ..\..\packages\Octokit.GraphQL.0.0.1\lib\netstandard1.1\Octokit.GraphQL.Core.dll True diff --git a/src/GitHub.VisualStudio/Properties/AssemblyInfo.cs b/src/GitHub.VisualStudio/Properties/AssemblyInfo.cs index 0b3fff38d4..c0a4cbbb10 100644 --- a/src/GitHub.VisualStudio/Properties/AssemblyInfo.cs +++ b/src/GitHub.VisualStudio/Properties/AssemblyInfo.cs @@ -20,6 +20,8 @@ OldVersionLowerBound = "0.0.0.0", OldVersionUpperBound = AssemblyVersionInformation.Version)] [assembly: ProvideCodeBase(AssemblyName = "Octokit", CodeBase = @"$PackageFolder$\Octokit.dll")] +[assembly: ProvideCodeBase(AssemblyName = "Octokit.GraphQL", CodeBase = @"$PackageFolder$\Octokit.GraphQL.dll")] +[assembly: ProvideCodeBase(AssemblyName = "Octokit.GraphQL.Core", CodeBase = @"$PackageFolder$\Octokit.GraphQL.Core.dll")] [assembly: ProvideCodeBase(AssemblyName = "LibGit2Sharp", CodeBase = @"$PackageFolder$\LibGit2Sharp.dll")] [assembly: ProvideCodeBase(AssemblyName = "Splat", CodeBase = @"$PackageFolder$\Splat.dll")] [assembly: ProvideCodeBase(AssemblyName = "Rothko", CodeBase = @"$PackageFolder$\Rothko.dll")] @@ -28,6 +30,7 @@ [assembly: ProvideCodeBase(AssemblyName = "Serilog.Sinks.File", CodeBase = @"$PackageFolder$\Serilog.Sinks.File.dll")] [assembly: ProvideCodeBase(AssemblyName = "Markdig", CodeBase = @"$PackageFolder$\Markdig.dll")] [assembly: ProvideCodeBase(AssemblyName = "Markdig.Wpf", CodeBase = @"$PackageFolder$\Markdig.Wpf.dll")] +[assembly: ProvideCodeBase(AssemblyName = "Newtonsoft.Json", CodeBase = @"$PackageFolder$\Newtonsoft.Json.dll")] [assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.VisualStudio.Views")] [assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.VisualStudio.Views.Dialog")] diff --git a/src/GitHub.VisualStudio/packages.config b/src/GitHub.VisualStudio/packages.config index df355004a5..c968000021 100644 --- a/src/GitHub.VisualStudio/packages.config +++ b/src/GitHub.VisualStudio/packages.config @@ -34,7 +34,8 @@ - + + diff --git a/test/UnitTests/GitHub.App/Factories/ModelServiceFactoryTests.cs b/test/UnitTests/GitHub.App/Factories/ModelServiceFactoryTests.cs index a42a65c864..a47f5aa797 100644 --- a/test/UnitTests/GitHub.App/Factories/ModelServiceFactoryTests.cs +++ b/test/UnitTests/GitHub.App/Factories/ModelServiceFactoryTests.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using GitHub.Api; using GitHub.Caches; using GitHub.Extensions; using GitHub.Factories; @@ -63,12 +64,14 @@ static ModelServiceFactory CreateTarget( IHostCacheFactory hostCacheFactory = null) { var apiClientFactory = Substitute.For(); + var graphQLClientFactory = Substitute.For(); var avatarProvider = Substitute.For(); hostCacheFactory = hostCacheFactory ?? Substitute.For(); return new ModelServiceFactory( apiClientFactory, + graphQLClientFactory, hostCacheFactory, avatarProvider); } diff --git a/test/UnitTests/GitHub.App/Models/AccountModelTests.cs b/test/UnitTests/GitHub.App/Models/AccountModelTests.cs index 7f1f931843..6b31baa3bb 100644 --- a/test/UnitTests/GitHub.App/Models/AccountModelTests.cs +++ b/test/UnitTests/GitHub.App/Models/AccountModelTests.cs @@ -30,7 +30,7 @@ public void CopyFromDoesNotLoseAvatar() const string login = "foo"; const int initialOwnedPrivateRepositoryCount = 1; - var initialAccount = new Account(login, true, false, initialOwnedPrivateRepositoryCount, 0, initialBitmapImageSubject); + var initialAccount = new Account(login, true, false, initialOwnedPrivateRepositoryCount, 0, null, initialBitmapImageSubject); //Creating the test collection var col = new TrackingCollection(Observable.Empty(), OrderedComparer.OrderByDescending(x => x.Login).Compare); @@ -78,7 +78,7 @@ public void CopyFromDoesNotLoseAvatar() //Creating an account update const int updatedOwnedPrivateRepositoryCount = 2; var updatedBitmapImageSubject = new Subject(); - var updatedAccount = new Account(login, true, false, updatedOwnedPrivateRepositoryCount, 0, updatedBitmapImageSubject); + var updatedAccount = new Account(login, true, false, updatedOwnedPrivateRepositoryCount, 0, null, updatedBitmapImageSubject); //Updating the account in the collection col.AddItem(updatedAccount); @@ -119,13 +119,26 @@ public static bool BitmapSourcesAreEqual(BitmapSource image1, BitmapSource image public static byte[] BitmapSourceToBytes(BitmapSource image) { - var encoder = new BmpBitmapEncoder(); - encoder.Frames.Add(BitmapFrame.Create(image)); - using (MemoryStream ms = new MemoryStream()) + byte[] data = new byte[] { }; + if (image != null) { - encoder.Save(ms); - return ms.ToArray(); + try + { + var encoder = new BmpBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(image)); + using (MemoryStream ms = new MemoryStream()) + { + encoder.Save(ms); + data = ms.ToArray(); + } + return data; + } + catch (Exception ex) + { + } } + + return data; } } } diff --git a/test/UnitTests/GitHub.App/Models/ModelServiceTests.cs b/test/UnitTests/GitHub.App/Models/ModelServiceTests.cs index ed938565c3..b0192b04b8 100644 --- a/test/UnitTests/GitHub.App/Models/ModelServiceTests.cs +++ b/test/UnitTests/GitHub.App/Models/ModelServiceTests.cs @@ -29,10 +29,9 @@ public class TheGetCurrentUserMethod : TestBaseClass [Test] public async Task RetrievesCurrentUser() { - var apiClient = Substitute.For(); var cache = new InMemoryBlobCache(); await cache.InsertObject("user", new AccountCacheItem(CreateOctokitUser("octocat"))); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(hostCache: cache); var user = await modelService.GetCurrentUser(); @@ -45,9 +44,8 @@ public class TheInsertUserMethod : TestBaseClass [Test] public async Task AddsUserToCache() { - var apiClient = Substitute.For(); var cache = new InMemoryBlobCache(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(hostCache: cache); var user = await modelService.InsertUser(new AccountCacheItem(CreateOctokitUser("octocat"))); @@ -65,7 +63,7 @@ public async Task CanRetrieveAndCacheGitIgnores() var apiClient = Substitute.For(); apiClient.GetGitIgnoreTemplates().Returns(data.ToObservable()); var cache = new InMemoryBlobCache(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient, hostCache: cache); var fetched = await modelService.GetGitIgnoreTemplates().ToList(); @@ -97,7 +95,7 @@ public async Task CanRetrieveAndCacheLicenses() var apiClient = Substitute.For(); apiClient.GetLicenses().Returns(data.ToObservable()); var cache = new InMemoryBlobCache(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient, hostCache: cache); var fetched = await modelService.GetLicenses().ToList(); @@ -120,8 +118,7 @@ public async Task ReturnsEmptyIfLicenseApiNotFound() var apiClient = Substitute.For(); apiClient.GetLicenses() .Returns(Observable.Throw(new NotFoundException("Not Found", HttpStatusCode.NotFound))); - var cache = new InMemoryBlobCache(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient); var fetched = await modelService.GetLicenses().ToList(); @@ -135,7 +132,7 @@ public async Task ReturnsEmptyIfCacheReadFails() var cache = Substitute.For(); cache.Get(Args.String) .Returns(Observable.Throw(new InvalidOperationException("Unknown"))); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient, hostCache: cache); var fetched = await modelService.GetLicenses().ToList(); @@ -157,7 +154,7 @@ public async Task CanRetrieveAndCacheUserAndAccounts() apiClient.GetUser().Returns(Observable.Return(CreateOctokitUser("snoopy"))); apiClient.GetOrganizations().Returns(orgs.ToObservable()); var cache = new InMemoryBlobCache(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient, hostCache: cache); await modelService.InsertUser(new AccountCacheItem { Login = "snoopy" }); var fetched = await modelService.GetAccounts(); @@ -185,7 +182,7 @@ public async Task CanRetrieveUserFromCacheAndAccountsFromApi() var apiClient = Substitute.For(); apiClient.GetOrganizations().Returns(orgs.ToObservable()); var cache = new InMemoryBlobCache(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient, hostCache: cache); await modelService.InsertUser(new AccountCacheItem(CreateOctokitUser("octocat"))); var fetched = await modelService.GetAccounts(); @@ -214,8 +211,7 @@ public async Task OnlyRetrievesOneUserEvenIfCacheOrApiReturnsMoreThanOne() var apiClient = Substitute.For(); apiClient.GetUser().Returns(users.ToObservable()); apiClient.GetOrganizations().Returns(Observable.Empty()); - var cache = new InMemoryBlobCache(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient); var fetched = await modelService.GetAccounts(); @@ -263,7 +259,7 @@ public async Task CanRetrieveAndCacheRepositoriesForUserAndOrganizations() apiClient.GetRepositoriesForOrganization("github").Returns(githubRepos.ToObservable()); apiClient.GetRepositoriesForOrganization("octokit").Returns(octokitRepos.ToObservable()); var cache = new InMemoryBlobCache(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient, hostCache: cache); await modelService.InsertUser(new AccountCacheItem { Login = "opus" }); var fetched = await modelService.GetRepositories().ToList(); @@ -318,7 +314,7 @@ public async Task CanRetrieveAndCacheRepositoriesForUserAndOrganizations() public async Task WhenNotLoggedInReturnsEmptyCollection() { var apiClient = Substitute.For(); - var modelService = new ModelService(apiClient, new InMemoryBlobCache(), Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient); var repos = await modelService.GetRepositories(); @@ -329,7 +325,7 @@ public async Task WhenNotLoggedInReturnsEmptyCollection() public async Task WhenLoggedInDoesNotBlowUpOnUnexpectedNetworkProblems() { var apiClient = Substitute.For(); - var modelService = new ModelService(apiClient, new InMemoryBlobCache(), Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient); apiClient.GetOrganizations() .Returns(Observable.Throw(new NotFoundException("Not Found", HttpStatusCode.NotFound))); @@ -346,7 +342,7 @@ public async Task InvalidatesTheCache() { var apiClient = Substitute.For(); var cache = new InMemoryBlobCache(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient, hostCache: cache); var user = await modelService.InsertUser(new AccountCacheItem(CreateOctokitUser("octocat"))); //Assert.Single((await cache.GetAllObjects())); @@ -367,7 +363,7 @@ public async Task VaccumsTheCache() received = true; return Observable.Return(Unit.Default); }); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient, hostCache: cache); await modelService.InvalidateAll(); Assert.True(received); @@ -387,7 +383,7 @@ public async Task NonExpiredIndexReturnsCache() var cache = new InMemoryBlobCache(); var apiClient = Substitute.For(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient, hostCache: cache); var user = CreateOctokitUser(username); apiClient.GetUser().Returns(Observable.Return(user)); apiClient.GetOrganizations().Returns(Observable.Empty()); @@ -438,7 +434,7 @@ public async Task ExpiredIndexReturnsLive() var cache = new InMemoryBlobCache(); var apiClient = Substitute.For(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient, hostCache: cache); var user = CreateOctokitUser(username); apiClient.GetUser().Returns(Observable.Return(user)); apiClient.GetOrganizations().Returns(Observable.Empty()); @@ -506,7 +502,7 @@ public async Task ExpiredIndexClearsItems() var cache = new InMemoryBlobCache(); var apiClient = Substitute.For(); - var modelService = new ModelService(apiClient, cache, Substitute.For()); + var modelService = CreateTarget(apiClient: apiClient, hostCache: cache); var user = CreateOctokitUser(username); apiClient.GetUser().Returns(Observable.Return(user)); apiClient.GetOrganizations().Returns(Observable.Empty()); @@ -574,4 +570,17 @@ public async Task ExpiredIndexClearsItems() );*/ } } + + static ModelService CreateTarget( + IApiClient apiClient = null, + Octokit.GraphQL.IConnection graphql = null, + IBlobCache hostCache = null, + IAvatarProvider avatarProvider = null) + { + return new ModelService( + apiClient ?? Substitute.For(), + graphql ?? Substitute.For(), + hostCache ?? new InMemoryBlobCache(), + Substitute.For()); + } } diff --git a/test/UnitTests/GitHub.App/ViewModels/GitHubPane/PullRequestListViewModelTests.cs b/test/UnitTests/GitHub.App/ViewModels/GitHubPane/PullRequestListViewModelTests.cs index 4d169be19a..3bdf322d62 100644 --- a/test/UnitTests/GitHub.App/ViewModels/GitHubPane/PullRequestListViewModelTests.cs +++ b/test/UnitTests/GitHub.App/ViewModels/GitHubPane/PullRequestListViewModelTests.cs @@ -131,9 +131,9 @@ IModelServiceFactory CreateModelServiceFactory() var pullRequest = new PullRequestModel( 1, "PR1", - new Account("foo", true, false, 1, 0, bitmapSource), + new Account("foo", true, false, 1, 0, null, bitmapSource), DateTimeOffset.MinValue); - pullRequest.Assignee = new Account("foo", true, false, 1, 0, bitmapSource); + pullRequest.Assignee = new Account("foo", true, false, 1, 0, null, bitmapSource); var pullRequestCollection = Substitute.For>(); pullRequestCollection[0].Returns(pullRequest); diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index 164350fce0..b9bd0c647c 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -166,6 +166,10 @@ ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.TextManager.Interop.8.0.dll True + + ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + True + ..\..\packages\NSubstitute.2.0.3\lib\net45\NSubstitute.dll True @@ -173,6 +177,14 @@ ..\..\packages\NUnit.3.9.0\lib\net45\nunit.framework.dll + + ..\..\packages\Octokit.GraphQL.0.0.1\lib\netstandard1.1\Octokit.GraphQL.dll + True + + + ..\..\packages\Octokit.GraphQL.0.0.1\lib\netstandard1.1\Octokit.GraphQL.Core.dll + True + @@ -377,6 +389,9 @@ Designer + + PreserveNewest + diff --git a/test/UnitTests/UnitTests.dll.config b/test/UnitTests/UnitTests.dll.config new file mode 100644 index 0000000000..5fbc6df21b --- /dev/null +++ b/test/UnitTests/UnitTests.dll.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/test/UnitTests/packages.config b/test/UnitTests/packages.config index f6e22cda65..723198e5a5 100644 --- a/test/UnitTests/packages.config +++ b/test/UnitTests/packages.config @@ -26,10 +26,12 @@ + +