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 @@
+
+