diff --git a/README.md b/README.md
index 4ffa2e1416..258111cdc2 100644
--- a/README.md
+++ b/README.md
@@ -98,3 +98,4 @@ Visit the [Contributor Guidelines](CONTRIBUTING.md) for details on how to contri
Copyright 2015 - 2017 GitHub, Inc.
Licensed under the [MIT License](LICENSE.md)
+
diff --git a/appveyor.yml b/appveyor.yml
index 63fa013b88..cd32d5c109 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: '2.4.4.{build}'
+version: '2.4.99.{build}'
skip_tags: true
install:
- ps: |
diff --git a/lib/.gitignore b/lib/.gitignore
new file mode 100644
index 0000000000..296234c026
--- /dev/null
+++ b/lib/.gitignore
@@ -0,0 +1 @@
+!*.nupkg
diff --git a/lib/Octokit.GraphQL.0.0.2-alpha.nupkg b/lib/Octokit.GraphQL.0.0.2-alpha.nupkg
new file mode 100644
index 0000000000..88686b7729
Binary files /dev/null and b/lib/Octokit.GraphQL.0.0.2-alpha.nupkg differ
diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj
index a825cfc079..fc35dd136b 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.2-alpha\lib\netstandard1.1\Octokit.GraphQL.dll
+ True
+
+
+ ..\..\packages\Octokit.GraphQL.0.0.2-alpha\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..cd91295935
--- /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, address.GraphQLUri, 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..6356b0e89c 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/Api/ApiClient.cs b/src/GitHub.App/Api/ApiClient.cs
index 8aa92a19a6..978bfe3354 100644
--- a/src/GitHub.App/Api/ApiClient.cs
+++ b/src/GitHub.App/Api/ApiClient.cs
@@ -48,6 +48,27 @@ public IObservable CreateRepository(NewRepository repository, string
return (isUser ? client.Create(repository) : client.Create(login, repository));
}
+ public IObservable PostPullRequestReview(
+ string owner,
+ string name,
+ int number,
+ string commitId,
+ string body,
+ PullRequestReviewEvent e)
+ {
+ Guard.ArgumentNotEmptyString(owner, nameof(owner));
+ Guard.ArgumentNotEmptyString(name, nameof(name));
+
+ var review = new PullRequestReviewCreate
+ {
+ Body = body,
+ CommitId = commitId,
+ Event = e,
+ };
+
+ return gitHubClient.PullRequest.Review.Create(owner, name, number, review);
+ }
+
public IObservable CreatePullRequestReviewComment(
string owner,
string name,
@@ -88,6 +109,11 @@ public IObservable GetUser()
return gitHubClient.User.Current();
}
+ public IObservable GetUser(string login)
+ {
+ return gitHubClient.User.Get(login);
+ }
+
public IObservable GetOrganizations()
{
// Organization.GetAllForCurrent doesn't return all of the information we need (we
@@ -123,30 +149,10 @@ public IObservable GetLicenses()
public HostAddress HostAddress { get; }
- static string GetSha256Hash(string input)
- {
- Guard.ArgumentNotEmptyString(input, nameof(input));
-
- try
- {
- using (var sha256 = SHA256.Create())
- {
- var bytes = Encoding.UTF8.GetBytes(input);
- var hash = sha256.ComputeHash(bytes);
-
- return string.Join("", hash.Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
- }
- }
- catch (Exception e)
- {
- log.Error(e, "IMPOSSIBLE! Generating Sha256 hash caused an exception");
- return null;
- }
- }
-
static string GetFingerprint()
{
- return GetSha256Hash(ProductName + ":" + GetMachineIdentifier());
+ var fingerprint = ProductName + ":" + GetMachineIdentifier();
+ return fingerprint.GetSha256Hash();
}
static string GetMachineNameSafe()
@@ -269,11 +275,7 @@ public IObservable GetBranches(string owner, string repo)
Guard.ArgumentNotEmptyString(owner, nameof(owner));
Guard.ArgumentNotEmptyString(repo, nameof(repo));
-#pragma warning disable 618
- // GetAllBranches is obsolete, but don't want to introduce the change to fix the
- // warning in the PR, so disabling for now.
- return gitHubClient.Repository.GetAllBranches(owner, repo);
-#pragma warning restore
+ return gitHubClient.Repository.Branch.GetAll(owner, repo);
}
public IObservable GetRepository(string owner, string repo)
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 815b12f40b..8f3a92c09d 100644
--- a/src/GitHub.App/GitHub.App.csproj
+++ b/src/GitHub.App/GitHub.App.csproj
@@ -61,8 +61,16 @@
..\..\packages\Microsoft.VisualStudio.ComponentModelHost.14.0.25424\lib\net45\Microsoft.VisualStudio.ComponentModelHost.dll
True
+
+ ..\..\packages\Microsoft.VisualStudio.CoreUtility.14.3.25407\lib\net45\Microsoft.VisualStudio.CoreUtility.dll
+ True
+
+
+ ..\..\packages\Microsoft.VisualStudio.Editor.14.3.25407\lib\net45\Microsoft.VisualStudio.Editor.dll
+ True
+
- ..\..\packages\Microsoft.VisualStudio.OLE.Interop.7.10.6070\lib\Microsoft.VisualStudio.OLE.Interop.dll
+ ..\..\packages\Microsoft.VisualStudio.OLE.Interop.7.10.6071\lib\Microsoft.VisualStudio.OLE.Interop.dll
True
@@ -74,25 +82,75 @@
True
- ..\..\packages\Microsoft.VisualStudio.Shell.Interop.7.10.6071\lib\Microsoft.VisualStudio.Shell.Interop.dll
+ ..\..\packages\Microsoft.VisualStudio.Shell.Interop.7.10.6072\lib\net11\Microsoft.VisualStudio.Shell.Interop.dll
+ True
+
+
+ True
+ ..\..\packages\Microsoft.VisualStudio.Shell.Interop.10.0.10.0.30320\lib\net20\Microsoft.VisualStudio.Shell.Interop.10.0.dll
+ True
+
+
+ True
+ ..\..\packages\Microsoft.VisualStudio.Shell.Interop.11.0.11.0.61031\lib\net20\Microsoft.VisualStudio.Shell.Interop.11.0.dll
+ True
+
+
+ True
+ ..\..\packages\Microsoft.VisualStudio.Shell.Interop.12.0.12.0.30111\lib\net20\Microsoft.VisualStudio.Shell.Interop.12.0.dll
+ True
+
+
+ ..\..\packages\Microsoft.VisualStudio.Shell.Interop.8.0.8.0.50728\lib\net11\Microsoft.VisualStudio.Shell.Interop.8.0.dll
+ True
+
+
+ ..\..\packages\Microsoft.VisualStudio.Text.Data.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.Data.dll
+ True
+
+
+ ..\..\packages\Microsoft.VisualStudio.Text.Logic.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.Logic.dll
+ True
+
+
+ ..\..\packages\Microsoft.VisualStudio.Text.UI.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.UI.dll
+ True
+
+
+ ..\..\packages\Microsoft.VisualStudio.Text.UI.Wpf.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.UI.Wpf.dll
True
- ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.7.10.6070\lib\Microsoft.VisualStudio.TextManager.Interop.dll
+ ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.7.10.6071\lib\net11\Microsoft.VisualStudio.TextManager.Interop.dll
True
- ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.TextManager.Interop.8.0.dll
+ ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.8.0.8.0.50728\lib\net11\Microsoft.VisualStudio.TextManager.Interop.8.0.dll
True
False
..\..\packages\Microsoft.VisualStudio.Threading.14.1.131\lib\net45\Microsoft.VisualStudio.Threading.dll
-
- False
- ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll
- False
+
+ ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll
+ True
+
+
+ ..\..\packages\Microsoft.VisualStudio.Utilities.14.3.25407\lib\net45\Microsoft.VisualStudio.Utilities.dll
+ True
+
+
+ ..\..\packages\Microsoft.VisualStudio.Utilities.14.3.25407\lib\net45\Microsoft.VisualStudio.Utilities.dll
+ True
+
+
+ ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.dll
+ True
+
+
+ ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll
+ True
@@ -152,11 +210,18 @@
+
+
+
+
+
+
+
@@ -172,12 +237,18 @@
+
+
+
+
+
+
diff --git a/src/GitHub.App/Models/Account.cs b/src/GitHub.App/Models/Account.cs
index e7a61c3146..050871f6fa 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,13 +80,15 @@ 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; }
set { avatar = value; this.RaisePropertyChanged(); }
}
-#region Equality things
+ #region Equality things
public void CopyFrom(IAccount other)
{
if (!Equals(other))
@@ -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..aab24ab076
--- /dev/null
+++ b/src/GitHub.App/Models/PullRequestReviewModel.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace GitHub.Models
+{
+ public class PullRequestReviewModel : IPullRequestReviewModel
+ {
+ public long Id { get; set; }
+ public string NodeId { get; set; }
+ public IAccount User { get; set; }
+ public string Body { get; set; }
+ public PullRequestReviewState State { get; set; }
+ public string CommitId { get; set; }
+ public DateTimeOffset? SubmittedAt { get; set; }
+ }
+}
diff --git a/src/GitHub.App/Resources.Designer.cs b/src/GitHub.App/Resources.Designer.cs
index d658cc788d..f9f2856d6a 100644
--- a/src/GitHub.App/Resources.Designer.cs
+++ b/src/GitHub.App/Resources.Designer.cs
@@ -69,6 +69,15 @@ internal static string AddedFileStatus {
}
}
+ ///
+ /// Looks up a localized string similar to Approved.
+ ///
+ internal static string Approved {
+ get {
+ return ResourceManager.GetString("Approved", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Select a containing folder for your new repository..
///
@@ -78,6 +87,15 @@ internal static string BrowseForDirectory {
}
}
+ ///
+ /// Looks up a localized string similar to Changes Requested.
+ ///
+ internal static string ChangesRequested {
+ get {
+ return ResourceManager.GetString("ChangesRequested", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Clone a {0} Repository.
///
@@ -87,6 +105,15 @@ internal static string CloneTitle {
}
}
+ ///
+ /// Looks up a localized string similar to Commented.
+ ///
+ internal static string Commented {
+ get {
+ return ResourceManager.GetString("Commented", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Could not connect to github.com.
///
@@ -180,6 +207,15 @@ internal static string Fork {
}
}
+ ///
+ /// Looks up a localized string similar to InProgress.
+ ///
+ internal static string InProgress {
+ get {
+ return ResourceManager.GetString("InProgress", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to [invalid].
///
@@ -234,6 +270,33 @@ internal static string MustPullBeforePush {
}
}
+ ///
+ /// Looks up a localized string similar to Checkout PR branch before navigating to Editor.
+ ///
+ internal static string NavigateToEditorNotCheckedOutInfoMessage {
+ get {
+ return ResourceManager.GetString("NavigateToEditorNotCheckedOutInfoMessage", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Press Enter to navigate to Editor (PR branch must be checked out).
+ ///
+ internal static string NavigateToEditorNotCheckedOutStatusMessage {
+ get {
+ return ResourceManager.GetString("NavigateToEditorNotCheckedOutStatusMessage", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Press Enter to navigate to Editor.
+ ///
+ internal static string NavigateToEditorStatusMessage {
+ get {
+ return ResourceManager.GetString("NavigateToEditorStatusMessage", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to No commits to pull.
///
diff --git a/src/GitHub.App/Resources.resx b/src/GitHub.App/Resources.resx
index 7fa2be0265..aa055d9027 100644
--- a/src/GitHub.App/Resources.resx
+++ b/src/GitHub.App/Resources.resx
@@ -294,4 +294,25 @@
Please install Git for Windows from:
https://git-scm.com/download/win
+
+ Approved
+
+
+ Changes Requested
+
+
+ Commented
+
+
+ InProgress
+
+
+ Press Enter to navigate to Editor
+
+
+ Checkout PR branch before navigating to Editor
+
+
+ Press Enter to navigate to Editor (PR branch must be checked out)
+
\ No newline at end of file
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/SampleData/PullRequestDetailViewModelDesigner.cs b/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs
index 64126dc3bd..a4bd06b8a0 100644
--- a/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs
+++ b/src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs
@@ -31,8 +31,6 @@ public class PullRequestUpdateStateDesigner : IPullRequestUpdateState
[ExcludeFromCodeCoverage]
public class PullRequestDetailViewModelDesigner : PanePageViewModelBase, IPullRequestDetailViewModel
{
- private List changedFilesTree;
-
public PullRequestDetailViewModelDesigner()
{
var repoPath = @"C:\Repo";
@@ -69,8 +67,35 @@ public PullRequestDetailViewModelDesigner()
modelsDir.Files.Add(oldBranchModel);
gitHubDir.Directories.Add(modelsDir);
- changedFilesTree = new List();
- changedFilesTree.Add(gitHubDir);
+ Reviews = new[]
+ {
+ new PullRequestReviewSummaryViewModel
+ {
+ Id = 2,
+ User = new AccountDesigner { Login = "grokys", IsUser = true },
+ State = PullRequestReviewState.Pending,
+ FileCommentCount = 0,
+ },
+ new PullRequestReviewSummaryViewModel
+ {
+ Id = 1,
+ User = new AccountDesigner { Login = "jcansdale", IsUser = true },
+ State = PullRequestReviewState.Approved,
+ FileCommentCount = 5,
+ },
+ new PullRequestReviewSummaryViewModel
+ {
+ Id = 2,
+ User = new AccountDesigner { Login = "shana", IsUser = true },
+ State = PullRequestReviewState.ChangesRequested,
+ FileCommentCount = 5,
+ },
+ new PullRequestReviewSummaryViewModel
+ {
+ },
+ };
+
+ Files = new PullRequestFilesViewModelDesigner();
}
public IPullRequestModel Model { get; }
@@ -84,7 +109,8 @@ public PullRequestDetailViewModelDesigner()
public bool IsCheckedOut { get; }
public bool IsFromFork { get; }
public string Body { get; }
- public IReadOnlyList ChangedFilesTree => changedFilesTree;
+ public IReadOnlyList Reviews { get; }
+ public IPullRequestFilesViewModel Files { get; set; }
public IPullRequestCheckoutState CheckoutState { get; set; }
public IPullRequestUpdateState UpdateState { get; set; }
public string OperationError { get; set; }
@@ -94,20 +120,11 @@ public PullRequestDetailViewModelDesigner()
public ReactiveCommand Checkout { get; }
public ReactiveCommand Pull { get; }
public ReactiveCommand Push { get; }
- public ReactiveCommand SyncSubmodules { get; }
public ReactiveCommand
+
+ ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll
+ True
+
+
+ ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.dll
+ True
+
+
+ ..\..\packages\Octokit.GraphQL.0.0.2-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll
+ True
+
@@ -423,10 +420,6 @@
Designer
true
-
- Designer
- MSBuild:Compile
-
Designer
MSBuild:Compile
@@ -435,10 +428,6 @@
Designer
MSBuild:Compile
-
- Designer
- MSBuild:Compile
-
MSBuild:Compile
Designer
diff --git a/src/GitHub.InlineReviews/InlineReviewsPackage.cs b/src/GitHub.InlineReviews/InlineReviewsPackage.cs
index 1bb4310743..736139be3a 100644
--- a/src/GitHub.InlineReviews/InlineReviewsPackage.cs
+++ b/src/GitHub.InlineReviews/InlineReviewsPackage.cs
@@ -3,7 +3,6 @@
using System.Runtime.InteropServices;
using System.Threading;
using GitHub.Commands;
-using GitHub.InlineReviews.Views;
using GitHub.Services.Vssdk.Commands;
using GitHub.VisualStudio;
using Microsoft.VisualStudio.ComponentModelHost;
@@ -17,7 +16,6 @@ namespace GitHub.InlineReviews
[Guid(Guids.InlineReviewsPackageId)]
[ProvideAutoLoad(Guids.UIContext_Git, PackageAutoLoadFlags.BackgroundLoad)]
[ProvideMenuResource("Menus.ctmenu", 1)]
- [ProvideToolWindow(typeof(PullRequestCommentsPane), DocumentLikeTool = true)]
public class InlineReviewsPackage : AsyncPackage
{
protected override async Task InitializeAsync(
diff --git a/src/GitHub.InlineReviews/Models/InlineCommentThreadModel.cs b/src/GitHub.InlineReviews/Models/InlineCommentThreadModel.cs
index 4a6fac84e4..4619d3c545 100644
--- a/src/GitHub.InlineReviews/Models/InlineCommentThreadModel.cs
+++ b/src/GitHub.InlineReviews/Models/InlineCommentThreadModel.cs
@@ -19,7 +19,7 @@ class InlineCommentThreadModel : ReactiveObject, IInlineCommentThreadModel
/// Initializes a new instance of the class.
///
/// The relative path to the file that the thread is on.
- /// The SHA of the commit that the thread was left con.
+ /// The SHA of the commit that the thread appears on.
///
/// The 1-based line number in the original diff that the thread was left on.
///
@@ -28,20 +28,18 @@ class InlineCommentThreadModel : ReactiveObject, IInlineCommentThreadModel
///
public InlineCommentThreadModel(
string relativePath,
- string originalCommitSha,
- int originalPosition,
+ string commitSha,
IList diffMatch,
IEnumerable comments)
{
Guard.ArgumentNotNull(relativePath, nameof(relativePath));
- Guard.ArgumentNotNull(originalCommitSha, nameof(originalCommitSha));
+ Guard.ArgumentNotNull(commitSha, nameof(commitSha));
Guard.ArgumentNotNull(diffMatch, nameof(diffMatch));
Comments = comments.ToList();
DiffMatch = diffMatch;
DiffLineType = diffMatch[0].Type;
- OriginalCommitSha = originalCommitSha;
- OriginalPosition = originalPosition;
+ CommitSha = commitSha;
RelativePath = relativePath;
}
@@ -69,10 +67,7 @@ public int LineNumber
}
///
- public string OriginalCommitSha { get; }
-
- ///
- public int OriginalPosition { get; }
+ public string CommitSha { get; }
///
public string RelativePath { get; }
diff --git a/src/GitHub.InlineReviews/Models/PullRequestSessionFile.cs b/src/GitHub.InlineReviews/Models/PullRequestSessionFile.cs
index d625d931f6..a38b0d22c7 100644
--- a/src/GitHub.InlineReviews/Models/PullRequestSessionFile.cs
+++ b/src/GitHub.InlineReviews/Models/PullRequestSessionFile.cs
@@ -18,7 +18,7 @@ namespace GitHub.InlineReviews.Models
///
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
- Justification = "linesChanged is sharred and shouldn't be disposed")]
+ Justification = "linesChanged is shared and shouldn't be disposed")]
public class PullRequestSessionFile : ReactiveObject, IPullRequestSessionFile
{
readonly Subject>> linesChanged = new Subject>>();
@@ -32,9 +32,14 @@ public class PullRequestSessionFile : ReactiveObject, IPullRequestSessionFile
///
/// The relative path to the file in the repository.
///
- public PullRequestSessionFile(string relativePath)
+ ///
+ /// The commit to pin the file to, or "HEAD" to follow the pull request head.
+ ///
+ public PullRequestSessionFile(string relativePath, string commitSha = "HEAD")
{
RelativePath = relativePath;
+ this.commitSha = commitSha;
+ IsTrackingHead = commitSha == "HEAD";
}
///
@@ -54,9 +59,24 @@ public IReadOnlyList Diff
public string CommitSha
{
get { return commitSha; }
- internal set { this.RaiseAndSetIfChanged(ref commitSha, value); }
+ internal set
+ {
+ if (value != commitSha)
+ {
+ if (!IsTrackingHead)
+ {
+ throw new GitHubLogicException(
+ "Cannot change the CommitSha of a PullRequestSessionFile that is not tracking HEAD.");
+ }
+
+ this.RaiseAndSetIfChanged(ref commitSha, value);
+ }
+ }
}
+ ///
+ public bool IsTrackingHead { get; }
+
///
public IReadOnlyList InlineCommentThreads
{
@@ -69,7 +89,9 @@ public IReadOnlyList InlineCommentThreads
.Where(x => x.Item1 >= 0)
.Distinct()
.ToList();
+ this.RaisePropertyChanging();
inlineCommentThreads = value;
+ this.RaisePropertyChanged();
NotifyLinesChanged(lines);
}
}
diff --git a/src/GitHub.InlineReviews/SampleData/CommentViewModelDesigner.cs b/src/GitHub.InlineReviews/SampleData/CommentViewModelDesigner.cs
index 2a158c45ae..d6cf93a107 100644
--- a/src/GitHub.InlineReviews/SampleData/CommentViewModelDesigner.cs
+++ b/src/GitHub.InlineReviews/SampleData/CommentViewModelDesigner.cs
@@ -18,10 +18,12 @@ public CommentViewModelDesigner()
}
public int Id { get; set; }
+ public string NodeId { get; set; }
public string Body { get; set; }
public string ErrorMessage { get; set; }
public CommentEditState EditState { get; set; }
public bool IsReadOnly { get; set; }
+ public bool IsSubmitting { get; set; }
public ICommentThreadViewModel Thread { get; }
public DateTimeOffset UpdatedAt => DateTime.Now.Subtract(TimeSpan.FromDays(3));
public IAccount User { get; set; }
diff --git a/src/GitHub.InlineReviews/SampleData/DiffCommentThreadViewModelDesigner.cs b/src/GitHub.InlineReviews/SampleData/DiffCommentThreadViewModelDesigner.cs
deleted file mode 100644
index ebf8dc18df..0000000000
--- a/src/GitHub.InlineReviews/SampleData/DiffCommentThreadViewModelDesigner.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-using GitHub.InlineReviews.ViewModels;
-
-namespace GitHub.InlineReviews.SampleData
-{
- [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")]
- class DiffCommentThreadViewModelDesigner : IDiffCommentThreadViewModel
- {
- public string DiffHunk { get; set; }
- public int LineNumber { get; set; }
- public string Path { get; set; }
- public ICommentThreadViewModel Comments { get; set; }
- }
-}
diff --git a/src/GitHub.InlineReviews/SampleData/PullRequestCommentsViewModelDesigner.cs b/src/GitHub.InlineReviews/SampleData/PullRequestCommentsViewModelDesigner.cs
deleted file mode 100644
index bf1799d22e..0000000000
--- a/src/GitHub.InlineReviews/SampleData/PullRequestCommentsViewModelDesigner.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-using GitHub.InlineReviews.ViewModels;
-using GitHub.Models;
-using ReactiveUI;
-
-namespace GitHub.InlineReviews.SampleData
-{
- [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")]
- class PullRequestCommentsViewModelDesigner : IPullRequestCommentsViewModel
- {
- public IRepositoryModel Repository { get; set; }
- public int Number { get; set; }
- public string Title { get; set; }
- public ICommentThreadViewModel Conversation { get; set; }
- public IReactiveList FileComments { get; }
- = new ReactiveList();
- }
-}
diff --git a/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs
index 5f82830fd9..8a12f9f7ac 100644
--- a/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs
+++ b/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs
@@ -4,6 +4,7 @@
using System.Threading.Tasks;
using GitHub.Models;
using Microsoft.VisualStudio.Text;
+using Octokit;
namespace GitHub.InlineReviews.Services
{
@@ -48,13 +49,15 @@ Task> Diff(
/// The pull request session.
/// The relative path to the file.
/// The diff.
+ /// The SHA of the HEAD.
///
/// A collection of objects with updated line numbers.
///
IReadOnlyList BuildCommentThreads(
IPullRequestModel pullRequest,
string relativePath,
- IReadOnlyList diff);
+ IReadOnlyList diff,
+ string headSha);
///
/// Updates a set of comment thread models for a file based on a new diff.
@@ -140,6 +143,18 @@ Task ExtractFileFromGit(
///
Task GetPullRequestMergeBase(ILocalRepositoryModel repository, IPullRequestModel pullRequest);
+ ///
+ /// Gets the GraphQL ID for a pull request.
+ ///
+ /// The local repository.
+ /// The owner of the remote fork.
+ /// The pull request number.
+ ///
+ Task GetGraphQLPullRequestId(
+ ILocalRepositoryModel localRepository,
+ string repositoryOwner,
+ int number);
+
///
/// Creates a rebuild signal subject for a .
///
@@ -155,7 +170,92 @@ Task ExtractFileFromGit(
ISubject CreateRebuildSignal();
///
- /// Posts a new PR review comment.
+ /// Creates a new pending review on the server.
+ ///
+ /// The local repository.
+ /// The user posting the review.
+ /// The GraphQL ID of the pull request.
+ ///
+ Task CreatePendingReview(
+ ILocalRepositoryModel localRepository,
+ IAccount user,
+ string pullRequestId);
+
+ ///
+ /// Cancels a pending review on the server.
+ ///
+ /// The GraphQL ID of the review.
+ Task CancelPendingReview(
+ ILocalRepositoryModel localRepository,
+ string reviewId);
+
+ ///
+ /// Posts PR review with no comments.
+ ///
+ /// The local repository.
+ /// The owner of the repository fork to post to.
+ /// The user posting the review.
+ /// The pull request number.
+ /// The SHA of the commit being reviewed.
+ /// The review body.
+ /// The review event.
+ Task PostReview(
+ ILocalRepositoryModel localRepository,
+ string remoteRepositoryOwner,
+ IAccount user,
+ int number,
+ string commitId,
+ string body,
+ PullRequestReviewEvent e);
+
+ ///
+ /// Submits a pending PR review.
+ ///
+ /// The local repository.
+ /// The user posting the review.
+ /// The GraphQL ID of the pending review.
+ /// The review body.
+ /// The review event.
+ Task SubmitPendingReview(
+ ILocalRepositoryModel localRepository,
+ IAccount user,
+ string pendingReviewId,
+ string body,
+ PullRequestReviewEvent e);
+
+ ///
+ /// Posts a new pending PR review comment.
+ ///
+ /// The local repository.
+ /// The user posting the comment.
+ /// The GraphQL ID of the pending review.
+ /// The comment body.
+ /// THe SHA of the commit to comment on.
+ /// The relative path of the file to comment on.
+ /// The line index in the diff to comment on.
+ /// A model representing the posted comment.
+ ///
+ /// The method posts a new pull request comment to a pending review started by
+ /// .
+ ///
+ Task PostPendingReviewComment(
+ ILocalRepositoryModel localRepository,
+ IAccount user,
+ string pendingReviewId,
+ string body,
+ string commitId,
+ string path,
+ int position);
+
+ Task PostPendingReviewCommentReply(
+ ILocalRepositoryModel localRepository,
+ IAccount user,
+ string pendingReviewId,
+ string body,
+ string inReplyTo);
+
+ ///
+ /// Posts a new standalone PR review comment.
///
/// The local repository.
/// The owner of the repository fork to post to.
@@ -166,7 +266,11 @@ Task ExtractFileFromGit(
/// The relative path of the file to comment on.
/// The line index in the diff to comment on.
/// A model representing the posted comment.
- Task PostReviewComment(
+ ///
+ /// The method posts a new standalone pull request comment that is not attached to a pending
+ /// pull request review.
+ ///
+ Task PostStandaloneReviewComment(
ILocalRepositoryModel localRepository,
string remoteRepositoryOwner,
IAccount user,
@@ -186,7 +290,7 @@ Task PostReviewComment(
/// The comment body.
/// The comment ID to reply to.
/// A model representing the posted comment.
- Task PostReviewComment(
+ Task PostStandaloneReviewCommentRepy(
ILocalRepositoryModel localRepository,
string remoteRepositoryOwner,
IAccount user,
diff --git a/src/GitHub.InlineReviews/Services/PullRequestSession.cs b/src/GitHub.InlineReviews/Services/PullRequestSession.cs
index 29680ffe15..335f4b21d9 100644
--- a/src/GitHub.InlineReviews/Services/PullRequestSession.cs
+++ b/src/GitHub.InlineReviews/Services/PullRequestSession.cs
@@ -10,6 +10,7 @@
using ReactiveUI;
using System.Threading;
using System.Reactive.Subjects;
+using static System.FormattableString;
namespace GitHub.InlineReviews.Services
{
@@ -32,7 +33,10 @@ public class PullRequestSession : ReactiveObject, IPullRequestSession
string mergeBase;
IReadOnlyList files;
IPullRequestModel pullRequest;
+ string pullRequestNodeId;
Subject pullRequestChanged = new Subject();
+ bool hasPendingReview;
+ string pendingReviewNodeId { get; set; }
public PullRequestSession(
IPullRequestSessionService service,
@@ -53,6 +57,7 @@ public PullRequestSession(
User = user;
LocalRepository = localRepository;
RepositoryOwner = repositoryOwner;
+ UpdatePendingReview();
}
///
@@ -67,21 +72,23 @@ public async Task> GetAllFiles()
}
///
- public async Task GetFile(string relativePath)
+ public async Task GetFile(
+ string relativePath,
+ string commitSha = "HEAD")
{
await getFilesLock.WaitAsync();
try
{
PullRequestSessionFile file;
+ var normalizedPath = relativePath.Replace("\\", "/");
+ var key = normalizedPath + '@' + commitSha;
- relativePath = relativePath.Replace("\\", "/");
-
- if (!fileIndex.TryGetValue(relativePath, out file))
+ if (!fileIndex.TryGetValue(key, out file))
{
- file = new PullRequestSessionFile(relativePath);
+ file = new PullRequestSessionFile(normalizedPath, commitSha);
await UpdateFile(file);
- fileIndex.Add(relativePath, file);
+ fileIndex.Add(key, file);
}
return file;
@@ -120,35 +127,143 @@ public string GetRelativePath(string path)
}
///
- public async Task PostReviewComment(string body, string commitId, string path, int position)
+ public async Task PostReviewComment(
+ string body,
+ string commitId,
+ string path,
+ IReadOnlyList diff,
+ int position)
{
- var model = await service.PostReviewComment(
- LocalRepository,
- RepositoryOwner,
- User,
- PullRequest.Number,
- body,
- commitId,
- path,
- position);
+ IPullRequestReviewCommentModel model;
+
+ if (!HasPendingReview)
+ {
+ model = await service.PostStandaloneReviewComment(
+ LocalRepository,
+ RepositoryOwner,
+ User,
+ PullRequest.Number,
+ body,
+ commitId,
+ path,
+ position);
+ }
+ else
+ {
+ model = await service.PostPendingReviewComment(
+ LocalRepository,
+ User,
+ pendingReviewNodeId,
+ body,
+ commitId,
+ path,
+ position);
+ }
+
+ await AddComment(model);
+ return model;
+ }
+
+ ///
+ public async Task PostReviewComment(
+ string body,
+ int inReplyTo,
+ string inReplyToNodeId)
+ {
+ IPullRequestReviewCommentModel model;
+
+ if (!HasPendingReview)
+ {
+ model = await service.PostStandaloneReviewCommentRepy(
+ LocalRepository,
+ RepositoryOwner,
+ User,
+ PullRequest.Number,
+ body,
+ inReplyTo);
+ }
+ else
+ {
+ model = await service.PostPendingReviewCommentReply(
+ LocalRepository,
+ User,
+ pendingReviewNodeId,
+ body,
+ inReplyToNodeId);
+ }
+
await AddComment(model);
return model;
}
///
- public async Task PostReviewComment(string body, int inReplyTo)
+ public async Task StartReview()
{
- var model = await service.PostReviewComment(
+ if (HasPendingReview)
+ {
+ throw new InvalidOperationException("A pending review is already underway.");
+ }
+
+ var model = await service.CreatePendingReview(
LocalRepository,
- RepositoryOwner,
User,
- PullRequest.Number,
- body,
- inReplyTo);
- await AddComment(model);
+ await GetPullRequestNodeId());
+
+ await AddReview(model);
return model;
}
+ ///
+ public async Task CancelReview()
+ {
+ if (!HasPendingReview)
+ {
+ throw new InvalidOperationException("There is no pending review to cancel.");
+ }
+
+ await service.CancelPendingReview(LocalRepository, pendingReviewNodeId);
+
+ PullRequest.Reviews = PullRequest.Reviews
+ .Where(x => x.NodeId != pendingReviewNodeId)
+ .ToList();
+ PullRequest.ReviewComments = PullRequest.ReviewComments
+ .Where(x => x.PullRequestReviewId != PendingReviewId)
+ .ToList();
+
+ await Update(PullRequest);
+ }
+
+ ///
+ public async Task PostReview(string body, Octokit.PullRequestReviewEvent e)
+ {
+ IPullRequestReviewModel model;
+
+ if (pendingReviewNodeId == null)
+ {
+ model = await service.PostReview(
+ LocalRepository,
+ RepositoryOwner,
+ User,
+ PullRequest.Number,
+ PullRequest.Head.Sha,
+ body,
+ e);
+ }
+ else
+ {
+ model = await service.SubmitPendingReview(
+ LocalRepository,
+ User,
+ pendingReviewNodeId,
+ body,
+ e);
+ }
+
+ await AddReview(model);
+ return model;
+ }
+
+ ///
public async Task Update(IPullRequestModel pullRequestModel)
{
PullRequest = pullRequestModel;
@@ -159,6 +274,7 @@ public async Task Update(IPullRequestModel pullRequestModel)
await UpdateFile(file);
}
+ UpdatePendingReview();
pullRequestChanged.OnNext(pullRequestModel);
}
@@ -170,13 +286,53 @@ async Task AddComment(IPullRequestReviewCommentModel comment)
await Update(PullRequest);
}
+ async Task AddReview(IPullRequestReviewModel review)
+ {
+ PullRequest.Reviews = PullRequest.Reviews
+ .Where(x => x.NodeId != review.NodeId)
+ .Concat(new[] { review })
+ .ToList();
+
+ if (review.State != PullRequestReviewState.Pending)
+ {
+ foreach (var comment in PullRequest.ReviewComments)
+ {
+ if (comment.PullRequestReviewId == review.Id)
+ {
+ comment.IsPending = false;
+ }
+ }
+ }
+
+ await Update(PullRequest);
+ }
+
async Task UpdateFile(PullRequestSessionFile file)
{
var mergeBaseSha = await GetMergeBase();
file.BaseSha = PullRequest.Base.Sha;
- file.CommitSha = PullRequest.Head.Sha;
+ file.CommitSha = file.IsTrackingHead ? PullRequest.Head.Sha : file.CommitSha;
file.Diff = await service.Diff(LocalRepository, mergeBaseSha, file.CommitSha, file.RelativePath);
- file.InlineCommentThreads = service.BuildCommentThreads(PullRequest, file.RelativePath, file.Diff);
+ file.InlineCommentThreads = service.BuildCommentThreads(PullRequest, file.RelativePath, file.Diff, file.CommitSha);
+ }
+
+ void UpdatePendingReview()
+ {
+ var pendingReview = PullRequest.Reviews
+ .FirstOrDefault(x => x.State == PullRequestReviewState.Pending && x.User.Login == User.Login);
+
+ if (pendingReview != null)
+ {
+ HasPendingReview = true;
+ pendingReviewNodeId = pendingReview.NodeId;
+ PendingReviewId = pendingReview.Id;
+ }
+ else
+ {
+ HasPendingReview = false;
+ pendingReviewNodeId = null;
+ PendingReviewId = 0;
+ }
}
async Task> CreateAllFiles()
@@ -192,22 +348,32 @@ async Task> CreateAllFiles()
return result;
}
- async Task CalculateContentCommitSha(IPullRequestSessionFile file, byte[] content)
+ string GetFullPath(string relativePath)
{
- if (IsCheckedOut)
- {
- return await service.IsUnmodifiedAndPushed(LocalRepository, file.RelativePath, content) ?
- await service.GetTipSha(LocalRepository) : null;
- }
- else
+ return Path.Combine(LocalRepository.LocalPath, relativePath);
+ }
+
+ async Task GetPullRequestNodeId()
+ {
+ if (pullRequestNodeId == null)
{
- return PullRequest.Head.Sha;
+ pullRequestNodeId = await service.GetGraphQLPullRequestId(
+ LocalRepository,
+ RepositoryOwner,
+ PullRequest.Number);
}
+
+ return pullRequestNodeId;
}
- string GetFullPath(string relativePath)
+ static string BuildDiffHunk(IReadOnlyList diff, int position)
{
- return Path.Combine(LocalRepository.LocalPath, relativePath);
+ var lines = diff.SelectMany(x => x.Lines).Reverse();
+ var context = lines.SkipWhile(x => x.DiffLineNumber != position).Take(5).Reverse().ToList();
+ var oldLineNumber = context.Select(x => x.OldLineNumber).Where(x => x != -1).FirstOrDefault();
+ var newLineNumber = context.Select(x => x.NewLineNumber).Where(x => x != -1).FirstOrDefault();
+ var header = Invariant($"@@ -{oldLineNumber},5 +{newLineNumber},5 @@");
+ return header + '\n' + string.Join("\n", context);
}
///
@@ -247,6 +413,16 @@ private set
///
public string RepositoryOwner { get; }
+ ///
+ public bool HasPendingReview
+ {
+ get { return hasPendingReview; }
+ private set { this.RaiseAndSetIfChanged(ref hasPendingReview, value); }
+ }
+
+ ///
+ public long PendingReviewId { get; private set; }
+
IEnumerable FilePaths
{
get { return PullRequest.ChangedFiles.Select(x => x.FileName); }
diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs
index 13d2577956..50e5b0dbba 100644
--- a/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs
+++ b/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs
@@ -301,7 +301,8 @@ async Task UpdateLiveFile(PullRequestSessionLiveFile file, bool rebuildThreads)
file.InlineCommentThreads = sessionService.BuildCommentThreads(
session.PullRequest,
file.RelativePath,
- file.Diff);
+ file.Diff,
+ session.PullRequest.Head.Sha);
}
else
{
diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs
index 8165159d01..ad54647b22 100644
--- a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs
+++ b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs
@@ -7,6 +7,7 @@
using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
+using GitHub.Api;
using GitHub.Factories;
using GitHub.InlineReviews.Models;
using GitHub.Models;
@@ -16,8 +17,14 @@
using LibGit2Sharp;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Projection;
+using Octokit.GraphQL;
+using Octokit.GraphQL.Model;
using ReactiveUI;
using Serilog;
+using PullRequestReviewEvent = Octokit.PullRequestReviewEvent;
+
+// GraphQL DatabaseId field are marked as deprecated, but we need them for interop with REST.
+#pragma warning disable CS0618
namespace GitHub.InlineReviews.Services
{
@@ -33,6 +40,7 @@ public class PullRequestSessionService : IPullRequestSessionService
readonly IGitClient gitClient;
readonly IDiffService diffService;
readonly IApiClientFactory apiClientFactory;
+ readonly IGraphQLClientFactory graphqlFactory;
readonly IUsageTracker usageTracker;
readonly IDictionary, string> mergeBaseCache;
@@ -43,12 +51,14 @@ public PullRequestSessionService(
IGitClient gitClient,
IDiffService diffService,
IApiClientFactory apiClientFactory,
+ IGraphQLClientFactory graphqlFactory,
IUsageTracker usageTracker)
{
this.gitService = gitService;
this.gitClient = gitClient;
this.diffService = diffService;
this.apiClientFactory = apiClientFactory;
+ this.graphqlFactory = graphqlFactory;
this.usageTracker = usageTracker;
mergeBaseCache = new Dictionary, string>();
@@ -76,7 +86,8 @@ public virtual async Task> Diff(ILocalRepositoryModel r
public IReadOnlyList BuildCommentThreads(
IPullRequestModel pullRequest,
string relativePath,
- IReadOnlyList diff)
+ IReadOnlyList diff,
+ string headSha)
{
relativePath = relativePath.Replace("\\", "/");
@@ -101,8 +112,7 @@ public IReadOnlyList BuildCommentThreads(
var thread = new InlineCommentThreadModel(
relativePath,
- comments.Key.Item1,
- comments.Key.Item2,
+ headSha,
diffLines,
comments);
threads.Add(thread);
@@ -249,6 +259,22 @@ public async Task ReadFileAsync(string path)
return null;
}
+ public async Task GetGraphQLPullRequestId(
+ ILocalRepositoryModel localRepository,
+ string repositoryOwner,
+ int number)
+ {
+ var address = HostAddress.Create(localRepository.CloneUrl.Host);
+ var graphql = await graphqlFactory.CreateConnection(address);
+
+ var query = new Query()
+ .Repository(repositoryOwner, localRepository.Name)
+ .PullRequest(number)
+ .Select(x => x.Id);
+
+ return await graphql.Run(query);
+ }
+
///
public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel repository, IPullRequestModel pullRequest)
{
@@ -293,7 +319,213 @@ public virtual ISubject CreateRebuildSignal()
}
///
- public async Task PostReviewComment(
+ public async Task CreatePendingReview(
+ ILocalRepositoryModel localRepository,
+ IAccount user,
+ string pullRequestId)
+ {
+ var address = HostAddress.Create(localRepository.CloneUrl.Host);
+ var graphql = await graphqlFactory.CreateConnection(address);
+
+ var review = new AddPullRequestReviewInput
+ {
+ PullRequestId = pullRequestId,
+ };
+
+ var addReview = new Mutation()
+ .AddPullRequestReview(review)
+ .Select(x => new PullRequestReviewModel
+ {
+ Id = x.PullRequestReview.DatabaseId.Value,
+ Body = x.PullRequestReview.Body,
+ CommitId = x.PullRequestReview.Commit.Oid,
+ NodeId = x.PullRequestReview.Id,
+ State = FromGraphQL(x.PullRequestReview.State),
+ User = user,
+ });
+
+ var result = await graphql.Run(addReview);
+ await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentStartReview);
+ return result;
+ }
+
+ ///
+ public async Task CancelPendingReview(
+ ILocalRepositoryModel localRepository,
+ string reviewId)
+ {
+ var address = HostAddress.Create(localRepository.CloneUrl.Host);
+ var graphql = await graphqlFactory.CreateConnection(address);
+
+ var delete = new DeletePullRequestReviewInput
+ {
+ PullRequestReviewId = reviewId,
+ };
+
+ var deleteReview = new Mutation()
+ .DeletePullRequestReview(delete)
+ .Select(x => x.ClientMutationId);
+
+ await graphql.Run(deleteReview);
+ }
+
+ ///
+ public async Task PostReview(
+ ILocalRepositoryModel localRepository,
+ string remoteRepositoryOwner,
+ IAccount user,
+ int number,
+ string commitId,
+ string body,
+ PullRequestReviewEvent e)
+ {
+ var address = HostAddress.Create(localRepository.CloneUrl.Host);
+ var apiClient = await apiClientFactory.Create(address);
+
+ var result = await apiClient.PostPullRequestReview(
+ remoteRepositoryOwner,
+ localRepository.Name,
+ number,
+ commitId,
+ body,
+ e);
+
+ await usageTracker.IncrementCounter(x => x.NumberOfPRReviewPosts);
+
+ return new PullRequestReviewModel
+ {
+ Id = result.Id,
+ Body = result.Body,
+ CommitId = result.CommitId,
+ State = (GitHub.Models.PullRequestReviewState)result.State.Value,
+ User = user,
+ };
+ }
+
+ public async Task SubmitPendingReview(
+ ILocalRepositoryModel localRepository,
+ IAccount user,
+ string pendingReviewId,
+ string body,
+ PullRequestReviewEvent e)
+ {
+ var address = HostAddress.Create(localRepository.CloneUrl.Host);
+ var graphql = await graphqlFactory.CreateConnection(address);
+
+ var submit = new SubmitPullRequestReviewInput
+ {
+ Body = body,
+ Event = ToGraphQl(e),
+ PullRequestReviewId = pendingReviewId,
+ };
+
+ var mutation = new Mutation()
+ .SubmitPullRequestReview(submit)
+ .Select(x => new PullRequestReviewModel
+ {
+ Body = body,
+ CommitId = x.PullRequestReview.Commit.Oid,
+ Id = x.PullRequestReview.DatabaseId.Value,
+ NodeId = x.PullRequestReview.Id,
+ State = (GitHub.Models.PullRequestReviewState)x.PullRequestReview.State,
+ User = user,
+ });
+
+ var result = await graphql.Run(mutation);
+ await usageTracker.IncrementCounter(x => x.NumberOfPRReviewPosts);
+ return result;
+ }
+
+ ///
+ public async Task PostPendingReviewComment(
+ ILocalRepositoryModel localRepository,
+ IAccount user,
+ string pendingReviewId,
+ string body,
+ string commitId,
+ string path,
+ int position)
+ {
+ var address = HostAddress.Create(localRepository.CloneUrl.Host);
+ var graphql = await graphqlFactory.CreateConnection(address);
+
+ var comment = new AddPullRequestReviewCommentInput
+ {
+ Body = body,
+ CommitOID = commitId,
+ Path = path,
+ Position = position,
+ PullRequestReviewId = pendingReviewId,
+ };
+
+ var addComment = new Mutation()
+ .AddPullRequestReviewComment(comment)
+ .Select(x => new PullRequestReviewCommentModel
+ {
+ Id = x.Comment.DatabaseId.Value,
+ NodeId = x.Comment.Id,
+ Body = x.Comment.Body,
+ CommitId = x.Comment.Commit.Oid,
+ Path = x.Comment.Path,
+ Position = x.Comment.Position,
+ CreatedAt = x.Comment.CreatedAt.Value,
+ DiffHunk = x.Comment.DiffHunk,
+ OriginalPosition = x.Comment.OriginalPosition,
+ OriginalCommitId = x.Comment.OriginalCommit.Oid,
+ PullRequestReviewId = x.Comment.PullRequestReview.DatabaseId.Value,
+ User = user,
+ IsPending = true,
+ });
+
+ var result = await graphql.Run(addComment);
+ await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost);
+ return result;
+ }
+
+ ///
+ public async Task PostPendingReviewCommentReply(
+ ILocalRepositoryModel localRepository,
+ IAccount user,
+ string pendingReviewId,
+ string body,
+ string inReplyTo)
+ {
+ var address = HostAddress.Create(localRepository.CloneUrl.Host);
+ var graphql = await graphqlFactory.CreateConnection(address);
+
+ var comment = new AddPullRequestReviewCommentInput
+ {
+ Body = body,
+ InReplyTo = inReplyTo,
+ PullRequestReviewId = pendingReviewId,
+ };
+
+ var addComment = new Mutation()
+ .AddPullRequestReviewComment(comment)
+ .Select(x => new PullRequestReviewCommentModel
+ {
+ Id = x.Comment.DatabaseId.Value,
+ NodeId = x.Comment.Id,
+ Body = x.Comment.Body,
+ CommitId = x.Comment.Commit.Oid,
+ Path = x.Comment.Path,
+ Position = x.Comment.Position,
+ CreatedAt = x.Comment.CreatedAt.Value,
+ DiffHunk = x.Comment.DiffHunk,
+ OriginalPosition = x.Comment.OriginalPosition,
+ OriginalCommitId = x.Comment.OriginalCommit.Oid,
+ PullRequestReviewId = x.Comment.PullRequestReview.DatabaseId.Value,
+ User = user,
+ IsPending = true,
+ });
+
+ var result = await graphql.Run(addComment);
+ await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost);
+ return result;
+ }
+
+ ///
+ public async Task PostStandaloneReviewComment(
ILocalRepositoryModel localRepository,
string remoteRepositoryOwner,
IAccount user,
@@ -333,7 +565,7 @@ public async Task PostReviewComment(
}
///
- public async Task PostReviewComment(
+ public async Task PostStandaloneReviewCommentRepy(
ILocalRepositoryModel localRepository,
string remoteRepositoryOwner,
IAccount user,
@@ -386,5 +618,26 @@ Task GetRepository(ILocalRepositoryModel repository)
{
return Task.Factory.StartNew(() => gitService.GetRepository(repository.LocalPath));
}
+
+
+ static GitHub.Models.PullRequestReviewState FromGraphQL(Octokit.GraphQL.Model.PullRequestReviewState s)
+ {
+ return (GitHub.Models.PullRequestReviewState)s;
+ }
+
+ static Octokit.GraphQL.Model.PullRequestReviewEvent ToGraphQl(Octokit.PullRequestReviewEvent e)
+ {
+ switch (e)
+ {
+ case Octokit.PullRequestReviewEvent.Approve:
+ return Octokit.GraphQL.Model.PullRequestReviewEvent.Approve;
+ case Octokit.PullRequestReviewEvent.Comment:
+ return Octokit.GraphQL.Model.PullRequestReviewEvent.Comment;
+ case Octokit.PullRequestReviewEvent.RequestChanges:
+ return Octokit.GraphQL.Model.PullRequestReviewEvent.RequestChanges;
+ default:
+ throw new NotSupportedException();
+ }
+ }
}
}
diff --git a/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs b/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs
index 41b93e82c1..4ea56eb4de 100644
--- a/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs
+++ b/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs
@@ -136,9 +136,10 @@ async Task Initialize()
if (bufferInfo != null)
{
+ var commitSha = bufferInfo.Side == DiffSide.Left ? "HEAD" : bufferInfo.CommitSha;
session = bufferInfo.Session;
relativePath = bufferInfo.RelativePath;
- file = await session.GetFile(relativePath);
+ file = await session.GetFile(relativePath, commitSha);
fileSubscription = file.LinesChanged.Subscribe(LinesChanged);
side = bufferInfo.Side ?? DiffSide.Right;
NotifyTagsChanged();
diff --git a/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs
index af5982e418..be3c623afb 100644
--- a/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs
+++ b/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs
@@ -1,5 +1,4 @@
using System;
-using System.ComponentModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
@@ -7,8 +6,6 @@
using GitHub.Extensions;
using GitHub.Logging;
using GitHub.Models;
-using GitHub.UI;
-using Octokit;
using ReactiveUI;
using Serilog;
@@ -23,6 +20,7 @@ public class CommentViewModel : ReactiveObject, ICommentViewModel
string body;
string errorMessage;
bool isReadOnly;
+ bool isSubmitting;
CommentEditState state;
DateTimeOffset updatedAt;
string undoBody;
@@ -33,14 +31,16 @@ public class CommentViewModel : ReactiveObject, ICommentViewModel
/// The thread that the comment is a part of.
/// The current user.
/// The ID of the comment.
+ /// The GraphQL ID of the comment.
/// The comment body.
/// The comment edit state.
/// The author of the comment.
/// The modified date of the comment.
- public CommentViewModel(
+ protected CommentViewModel(
ICommentThreadViewModel thread,
IAccount currentUser,
int commentId,
+ string commentNodeId,
string body,
CommentEditState state,
IAccount user,
@@ -54,6 +54,7 @@ public CommentViewModel(
Thread = thread;
CurrentUser = currentUser;
Id = commentId;
+ NodeId = commentNodeId;
Body = body;
EditState = state;
User = user;
@@ -89,35 +90,15 @@ public CommentViewModel(
/// The thread that the comment is a part of.
/// The current user.
/// The comment model.
- public CommentViewModel(
+ protected CommentViewModel(
ICommentThreadViewModel thread,
IAccount currentUser,
ICommentModel model)
- : this(thread, currentUser, model.Id, model.Body, CommentEditState.None, model.User, model.CreatedAt)
+ : this(thread, currentUser, model.Id, model.NodeId, model.Body, CommentEditState.None, model.User, model.CreatedAt)
{
}
- ///
- /// Creates a placeholder comment which can be used to add a new comment to a thread.
- ///
- /// The comment thread.
- /// The current user.
- /// THe placeholder comment.
- public static CommentViewModel CreatePlaceholder(
- ICommentThreadViewModel thread,
- IAccount currentUser)
- {
- return new CommentViewModel(
- thread,
- currentUser,
- 0,
- string.Empty,
- CommentEditState.Placeholder,
- currentUser,
- DateTimeOffset.MinValue);
- }
-
- void AddErrorHandler(ReactiveCommand command)
+ protected void AddErrorHandler(ReactiveCommand command)
{
command.ThrownExceptions.Subscribe(x => ErrorMessage = x.Message);
}
@@ -147,31 +128,32 @@ async Task DoCommitEdit(object unused)
try
{
ErrorMessage = null;
- Id = (await Thread.PostComment.ExecuteAsyncTask(Body)).Id;
+ IsSubmitting = true;
+
+ var model = await Thread.PostComment.ExecuteAsyncTask(Body);
+ Id = model.Id;
+ NodeId = model.NodeId;
EditState = CommentEditState.None;
UpdatedAt = DateTimeOffset.Now;
}
catch (Exception e)
{
var message = e.Message;
-
- if (e is ApiValidationException)
- {
- // HACK: If the user has pending review comments on the server then we can't
- // post new comments. The correct way to test for this would be to make a
- // request to /repos/:owner/:repo/pulls/:number/reviews and check for comments
- // with a PENDING state. For the moment however we'll just display a message.
- message += ". Do you have pending review comments?";
- }
-
ErrorMessage = message;
- log.Error(e, "Error posting inline comment");
+ log.Error(e, "Error posting comment");
+ }
+ finally
+ {
+ IsSubmitting = false;
}
}
///
public int Id { get; private set; }
+ ///
+ public string NodeId { get; private set; }
+
///
public string Body
{
@@ -200,6 +182,13 @@ public bool IsReadOnly
set { this.RaiseAndSetIfChanged(ref isReadOnly, value); }
}
+ ///
+ public bool IsSubmitting
+ {
+ get { return isSubmitting; }
+ protected set { this.RaiseAndSetIfChanged(ref isSubmitting, value); }
+ }
+
///
public DateTimeOffset UpdatedAt
{
@@ -207,14 +196,10 @@ public DateTimeOffset UpdatedAt
private set { this.RaiseAndSetIfChanged(ref updatedAt, value); }
}
- ///
- /// Gets the current user.
- ///
+ ///
public IAccount CurrentUser { get; }
- ///
- /// Gets the thread that the comment is a part of.
- ///
+ ///
public ICommentThreadViewModel Thread { get; }
///
diff --git a/src/GitHub.InlineReviews/ViewModels/DiffCommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/DiffCommentThreadViewModel.cs
deleted file mode 100644
index e30b0d0a8a..0000000000
--- a/src/GitHub.InlineReviews/ViewModels/DiffCommentThreadViewModel.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System;
-using ReactiveUI;
-
-namespace GitHub.InlineReviews.ViewModels
-{
- class DiffCommentThreadViewModel : ReactiveObject, IDiffCommentThreadViewModel
- {
- public DiffCommentThreadViewModel(
- string diffHunk,
- int lineNumber,
- string path,
- InlineCommentThreadViewModel comments)
- {
- DiffHunk = diffHunk;
- LineNumber = lineNumber;
- Path = path;
- Comments = comments;
- }
-
- public string DiffHunk { get; }
- public int LineNumber { get; }
- public string Path { get; }
- public ICommentThreadViewModel Comments { get; }
- }
-}
diff --git a/src/GitHub.InlineReviews/ViewModels/ICommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/ICommentViewModel.cs
index fdfab31eff..05080e2052 100644
--- a/src/GitHub.InlineReviews/ViewModels/ICommentViewModel.cs
+++ b/src/GitHub.InlineReviews/ViewModels/ICommentViewModel.cs
@@ -13,6 +13,9 @@ public enum CommentEditState
Placeholder,
}
+ ///
+ /// View model for an issue or pull request comment.
+ ///
public interface ICommentViewModel : IViewModel
{
///
@@ -20,6 +23,11 @@ public interface ICommentViewModel : IViewModel
///
int Id { get; }
+ ///
+ /// Gets the GraphQL ID of the comment.
+ ///
+ string NodeId { get; }
+
///
/// Gets or sets the body of the comment.
///
@@ -40,6 +48,12 @@ public interface ICommentViewModel : IViewModel
///
bool IsReadOnly { get; set; }
+ ///
+ /// Gets a value indicating whether the comment is currently in the process of being
+ /// submitted.
+ ///
+ bool IsSubmitting { get; }
+
///
/// Gets the modified date of the comment.
///
diff --git a/src/GitHub.InlineReviews/ViewModels/IDiffCommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/IDiffCommentThreadViewModel.cs
deleted file mode 100644
index 808f10d5e0..0000000000
--- a/src/GitHub.InlineReviews/ViewModels/IDiffCommentThreadViewModel.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace GitHub.InlineReviews.ViewModels
-{
- interface IDiffCommentThreadViewModel
- {
- string DiffHunk { get; }
- int LineNumber { get; }
- string Path { get; }
- ICommentThreadViewModel Comments { get; }
- }
-}
\ No newline at end of file
diff --git a/src/GitHub.InlineReviews/ViewModels/IInlineCommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/IInlineCommentViewModel.cs
deleted file mode 100644
index 036cb5eefd..0000000000
--- a/src/GitHub.InlineReviews/ViewModels/IInlineCommentViewModel.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-
-namespace GitHub.InlineReviews.ViewModels
-{
- ///
- /// View model for an inline comment (aka Pull Request Review Comment).
- ///
- interface IInlineCommentViewModel : ICommentViewModel
- {
- ///
- /// Gets the SHA of the commit that the comment was left on.
- ///
- string CommitSha { get; }
-
- ///
- /// Gets the line on the diff between PR.Base and that
- /// the comment was left on.
- ///
- int DiffLine { get; }
- }
-}
diff --git a/src/GitHub.InlineReviews/ViewModels/IPullRequestCommentsViewModel.cs b/src/GitHub.InlineReviews/ViewModels/IPullRequestCommentsViewModel.cs
deleted file mode 100644
index 441b4fc93d..0000000000
--- a/src/GitHub.InlineReviews/ViewModels/IPullRequestCommentsViewModel.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System.Collections.ObjectModel;
-using GitHub.Models;
-using ReactiveUI;
-
-namespace GitHub.InlineReviews.ViewModels
-{
- interface IPullRequestCommentsViewModel
- {
- IRepositoryModel Repository { get; }
- int Number { get; }
- string Title { get; }
- ICommentThreadViewModel Conversation { get; }
- IReactiveList FileComments { get; }
- }
-}
\ No newline at end of file
diff --git a/src/GitHub.InlineReviews/ViewModels/IPullRequestReviewCommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/IPullRequestReviewCommentViewModel.cs
new file mode 100644
index 0000000000..63bb86d96d
--- /dev/null
+++ b/src/GitHub.InlineReviews/ViewModels/IPullRequestReviewCommentViewModel.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Reactive;
+using ReactiveUI;
+
+namespace GitHub.InlineReviews.ViewModels
+{
+ ///
+ /// View model for a pull request review comment.
+ ///
+ public interface IPullRequestReviewCommentViewModel : ICommentViewModel
+ {
+ ///
+ /// Gets a value indicating whether the user can start a new review with this comment.
+ ///
+ bool CanStartReview { get; }
+
+ ///
+ /// Gets the caption for the "Commit" button.
+ ///
+ ///
+ /// This will be "Add a single comment" when not in review mode and "Add review comment"
+ /// when in review mode.
+ ///
+ string CommitCaption { get; }
+
+ ///
+ /// Gets a value indicating whether this comment is part of a pending pull request review.
+ ///
+ bool IsPending { get; }
+
+ ///
+ /// Gets a command which will commit a new comment and start a review.
+ ///
+ ReactiveCommand StartReview { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs
index 25a560f668..3c018872f0 100644
--- a/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs
+++ b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs
@@ -114,9 +114,10 @@ public async Task Initialize()
if (info != null)
{
+ var commitSha = info.Side == DiffSide.Left ? "HEAD" : info.CommitSha;
relativePath = info.RelativePath;
side = info.Side ?? DiffSide.Right;
- file = await info.Session.GetFile(relativePath);
+ file = await info.Session.GetFile(relativePath, commitSha);
session = info.Session;
await UpdateThread();
}
@@ -153,7 +154,7 @@ async void LinesChanged(IReadOnlyList> lines)
async Task UpdateThread()
{
- var placeholderBody = await GetPlaceholderBodyToPreserve();
+ var placeholderBody = GetPlaceholderBodyToPreserve();
Thread = null;
threadSubscription?.Dispose();
@@ -207,14 +208,13 @@ async Task SessionChanged(IPullRequestSession pullRequestSession)
}
}
- async Task GetPlaceholderBodyToPreserve()
+ string GetPlaceholderBodyToPreserve()
{
var lastComment = Thread?.Comments.LastOrDefault();
if (lastComment?.EditState == CommentEditState.Editing)
{
- var executing = await lastComment.CommitEdit.IsExecuting.FirstAsync();
- if (!executing) return lastComment.Body;
+ if (!lastComment.IsSubmitting) return lastComment.Body;
}
return null;
diff --git a/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs
index 113b7de294..0634156491 100644
--- a/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs
+++ b/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs
@@ -37,10 +37,10 @@ public InlineCommentThreadViewModel(
foreach (var comment in comments)
{
- Comments.Add(new CommentViewModel(this, CurrentUser, comment));
+ Comments.Add(new PullRequestReviewCommentViewModel(session, this, CurrentUser, comment));
}
- Comments.Add(CommentViewModel.CreatePlaceholder(this, CurrentUser));
+ Comments.Add(PullRequestReviewCommentViewModel.CreatePlaceholder(session, this, CurrentUser));
}
///
@@ -65,7 +65,8 @@ async Task DoPostComment(object parameter)
var body = (string)parameter;
var replyId = Comments[0].Id;
- return await Session.PostReviewComment(body, replyId);
+ var nodeId = Comments[0].NodeId;
+ return await Session.PostReviewComment(body, replyId, nodeId);
}
}
}
diff --git a/src/GitHub.InlineReviews/ViewModels/InlineCommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/InlineCommentViewModel.cs
deleted file mode 100644
index c7e1e65c75..0000000000
--- a/src/GitHub.InlineReviews/ViewModels/InlineCommentViewModel.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using System;
-using GitHub.Extensions;
-using GitHub.Models;
-
-namespace GitHub.InlineReviews.ViewModels
-{
- ///
- /// View model for an inline comment (aka Pull Request Review Comment).
- ///
- public class InlineCommentViewModel : CommentViewModel, IInlineCommentViewModel
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The thread that the comment is a part of.
- /// The current user.
- /// The ID of the comment.
- /// The comment body.
- /// The comment edit state.
- /// The author of the comment.
- /// The modified date of the comment.
- public InlineCommentViewModel(
- ICommentThreadViewModel thread,
- IAccount currentUser,
- int commentId,
- string body,
- CommentEditState state,
- IAccount user,
- DateTimeOffset updatedAt,
- string commitSha,
- int diffLine)
- : base(thread, currentUser, commentId, body, state, user, updatedAt)
- {
- Guard.ArgumentNotNull(commitSha, nameof(commitSha));
-
- CommitSha = commitSha;
- DiffLine = diffLine;
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The thread that the comment is a part of.
- /// The current user.
- /// The comment model.
- public InlineCommentViewModel(
- ICommentThreadViewModel thread,
- IAccount currentUser,
- IPullRequestReviewCommentModel model)
- : base(thread, currentUser, model)
- {
- CommitSha = model.OriginalCommitId;
- DiffLine = model.OriginalPosition.Value;
- }
-
- ///
- public string CommitSha { get; }
-
- ///
- public int DiffLine { get; }
- }
-}
diff --git a/src/GitHub.InlineReviews/ViewModels/IssueCommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/IssueCommentThreadViewModel.cs
deleted file mode 100644
index 31f8123ae7..0000000000
--- a/src/GitHub.InlineReviews/ViewModels/IssueCommentThreadViewModel.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using GitHub.Models;
-using ReactiveUI;
-
-namespace GitHub.InlineReviews.ViewModels
-{
- class IssueCommentThreadViewModel : CommentThreadViewModel
- {
- public IssueCommentThreadViewModel(
- IRepositoryModel repository,
- int number,
- IAccount currentUser)
- : base(currentUser)
- {
- Repository = repository;
- Number = number;
- }
-
- ///
- public override Uri GetCommentUrl(int id)
- {
- throw new NotImplementedException();
- }
-
- public IRepositoryModel Repository { get; }
- public int Number { get; }
- }
-}
diff --git a/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs
index 9b625b004c..599b58eb34 100644
--- a/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs
+++ b/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using GitHub.Extensions;
@@ -44,7 +45,7 @@ public NewInlineCommentThreadViewModel(
this.WhenAnyValue(x => x.NeedsPush, x => !x),
DoPostComment);
- var placeholder = CommentViewModel.CreatePlaceholder(this, CurrentUser);
+ var placeholder = PullRequestReviewCommentViewModel.CreatePlaceholder(session, this, CurrentUser);
placeholder.BeginEdit.Execute(null);
this.WhenAnyValue(x => x.NeedsPush).Subscribe(x => placeholder.IsReadOnly = x);
Comments.Add(placeholder);
@@ -110,6 +111,7 @@ async Task DoPostComment(object parameter)
body,
File.CommitSha,
File.RelativePath.Replace("\\", "/"),
+ File.Diff,
diffPosition.DiffLineNumber);
return model;
diff --git a/src/GitHub.InlineReviews/ViewModels/PullRequestCommentsViewModel.cs b/src/GitHub.InlineReviews/ViewModels/PullRequestCommentsViewModel.cs
deleted file mode 100644
index 9babec2f20..0000000000
--- a/src/GitHub.InlineReviews/ViewModels/PullRequestCommentsViewModel.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using GitHub.Api;
-using GitHub.Models;
-using GitHub.Services;
-using ReactiveUI;
-
-namespace GitHub.InlineReviews.ViewModels
-{
- class PullRequestCommentsViewModel : ReactiveObject, IPullRequestCommentsViewModel, IDisposable
- {
- readonly IPullRequestSession session;
-
- public PullRequestCommentsViewModel(
- IPullRequestSession session)
- {
- this.session = session;
-
- Repository = session.LocalRepository;
- Number = session.PullRequest.Number;
- Title = session.PullRequest.Title;
-
- Conversation = new IssueCommentThreadViewModel(Repository, Number, session.User);
-
- foreach (var comment in session.PullRequest.Comments)
- {
- Conversation.Comments.Add(new CommentViewModel(
- Conversation,
- session.User,
- comment));
- }
- }
-
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- bool disposed = false;
-
- protected virtual void Dispose(bool disposing)
- {
- if (!disposed)
- {
- disposed = true;
-
- if (disposing)
- {
- (Conversation as IDisposable)?.Dispose();
- }
- }
- }
-
- public IRepositoryModel Repository { get; }
- public int Number { get; }
- public string Title { get; }
- public ICommentThreadViewModel Conversation { get; }
- public IReactiveList FileComments { get; }
- = new ReactiveList();
-
- public async Task Initialize()
- {
- var files = await session.GetAllFiles();
-
- foreach (var file in files)
- {
- foreach (var thread in file.InlineCommentThreads)
- {
- var threadViewModel = new InlineCommentThreadViewModel(
- session,
- thread.Comments);
-
- FileComments.Add(new DiffCommentThreadViewModel(
- ToString(thread.DiffMatch),
- thread.LineNumber,
- file.RelativePath,
- threadViewModel));
- }
- }
- }
-
- private string ToString(IList diffMatch)
- {
- var b = new StringBuilder();
-
- for (var i = diffMatch.Count - 1; i >= 0; --i)
- {
- b.AppendLine(diffMatch[i].Content);
- }
-
- return b.ToString();
- }
- }
-}
diff --git a/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs
new file mode 100644
index 0000000000..3f0182aed8
--- /dev/null
+++ b/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using GitHub.Extensions;
+using GitHub.Logging;
+using GitHub.Models;
+using GitHub.Services;
+using GitHub.VisualStudio.UI;
+using ReactiveUI;
+using Serilog;
+
+namespace GitHub.InlineReviews.ViewModels
+{
+ ///
+ /// View model for a pull request review comment.
+ ///
+ public class PullRequestReviewCommentViewModel : CommentViewModel, IPullRequestReviewCommentViewModel
+ {
+ readonly IPullRequestSession session;
+ ObservableAsPropertyHelper canStartReview;
+ ObservableAsPropertyHelper commitCaption;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The pull request session.
+ /// The thread that the comment is a part of.
+ /// The current user.
+ /// The REST ID of the comment.
+ /// The GraphQL ID of the comment.
+ /// The comment body.
+ /// The comment edit state.
+ /// The author of the comment.
+ /// The modified date of the comment.
+ /// Whether this is a pending comment.
+ public PullRequestReviewCommentViewModel(
+ IPullRequestSession session,
+ ICommentThreadViewModel thread,
+ IAccount currentUser,
+ int commentId,
+ string commentNodeId,
+ string body,
+ CommentEditState state,
+ IAccount user,
+ DateTimeOffset updatedAt,
+ bool isPending)
+ : base(thread, currentUser, commentId, commentNodeId, body, state, user, updatedAt)
+ {
+ Guard.ArgumentNotNull(session, nameof(session));
+
+ this.session = session;
+ IsPending = isPending;
+
+ canStartReview = session.WhenAnyValue(x => x.HasPendingReview, x => !x)
+ .ToProperty(this, x => x.CanStartReview);
+ commitCaption = session.WhenAnyValue(
+ x => x.HasPendingReview,
+ x => x ? Resources.AddReviewComment : Resources.AddSingleComment)
+ .ToProperty(this, x => x.CommitCaption);
+
+ StartReview = ReactiveCommand.CreateAsyncTask(
+ CommitEdit.CanExecuteObservable,
+ DoStartReview);
+ AddErrorHandler(StartReview);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The pull request session.
+ /// The thread that the comment is a part of.
+ /// The current user.
+ /// The comment model.
+ public PullRequestReviewCommentViewModel(
+ IPullRequestSession session,
+ ICommentThreadViewModel thread,
+ IAccount currentUser,
+ IPullRequestReviewCommentModel model)
+ : this(session, thread, currentUser, model.Id, model.NodeId, model.Body, CommentEditState.None, model.User, model.CreatedAt, model.IsPending)
+ {
+ }
+
+ ///
+ /// Creates a placeholder comment which can be used to add a new comment to a thread.
+ ///
+ /// The comment thread.
+ /// The current user.
+ /// THe placeholder comment.
+ public static CommentViewModel CreatePlaceholder(
+ IPullRequestSession session,
+ ICommentThreadViewModel thread,
+ IAccount currentUser)
+ {
+ return new PullRequestReviewCommentViewModel(
+ session,
+ thread,
+ currentUser,
+ 0,
+ null,
+ string.Empty,
+ CommentEditState.Placeholder,
+ currentUser,
+ DateTimeOffset.MinValue,
+ false);
+ }
+
+ ///
+ public bool CanStartReview => canStartReview.Value;
+
+ ///
+ public string CommitCaption => commitCaption.Value;
+
+ ///
+ public bool IsPending { get; }
+
+ ///
+ public ReactiveCommand StartReview { get; }
+
+ async Task DoStartReview(object unused)
+ {
+ IsSubmitting = true;
+
+ try
+ {
+ await session.StartReview();
+ await CommitEdit.ExecuteAsync(null);
+ }
+ finally
+ {
+ IsSubmitting = false;
+ }
+ }
+ }
+}
diff --git a/src/GitHub.InlineReviews/Views/CommentView.xaml b/src/GitHub.InlineReviews/Views/CommentView.xaml
index 94fb253a8a..0d7499806f 100644
--- a/src/GitHub.InlineReviews/Views/CommentView.xaml
+++ b/src/GitHub.InlineReviews/Views/CommentView.xaml
@@ -60,6 +60,14 @@
Command="{Binding OpenOnGitHub}"
Foreground="{DynamicResource GitHubVsToolWindowText}"
Opacity="0.75" />
+
+ Pending
+
@@ -168,7 +176,12 @@
-
+
+
diff --git a/src/GitHub.InlineReviews/Views/DiffCommentThreadView.xaml b/src/GitHub.InlineReviews/Views/DiffCommentThreadView.xaml
deleted file mode 100644
index c77a0030ed..0000000000
--- a/src/GitHub.InlineReviews/Views/DiffCommentThreadView.xaml
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
- .Subscribe(a => UpdateFilter(SelectedState, SelectedAssignee, a));
-
-+ this.WhenAny(x => x.SelectedSortOrder, x => x.Value)
-+ .Where(x => pullRequests != null)
-
-
-
-
-
-
- @StanleyGoldman Ooops, I missed a x != null check here, and it's breaking the tests. Do you want to add it or shall I?
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- :
-
-
-
-
-
-
-
-
-
diff --git a/src/GitHub.InlineReviews/Views/DiffView.cs b/src/GitHub.InlineReviews/Views/DiffView.cs
deleted file mode 100644
index f0233bf970..0000000000
--- a/src/GitHub.InlineReviews/Views/DiffView.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using System;
-using System.IO;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Media;
-
-namespace GitHub.InlineReviews.Views
-{
- public class DiffView : StackPanel
- {
- static readonly Brush AddedBrush = new SolidColorBrush(Color.FromRgb(0xD7, 0xE3, 0xBC));
- static readonly Brush DeletedBrush = new SolidColorBrush(Color.FromRgb(0xFF, 0x99, 0x99));
-
- public static readonly DependencyProperty DiffProperty =
- DependencyProperty.Register(
- nameof(Diff),
- typeof(string),
- typeof(DiffView),
- new PropertyMetadata(DiffChanged));
-
- public string Diff
- {
- get { return (string)GetValue(DiffProperty); }
- set { SetValue(DiffProperty, value); }
- }
-
- void UpdateContents()
- {
- Children.Clear();
-
- if (Diff != null)
- {
- using (var reader = new StringReader(Diff))
- {
- string line;
-
- while ((line = reader.ReadLine()) != null)
- {
- var textBlock = new TextBlock();
- textBlock.Text = line;
-
- if (line.Length > 0)
- {
- switch (line[0])
- {
- case '+':
- textBlock.Background = AddedBrush;
- break;
- case '-':
- textBlock.Background = DeletedBrush;
- break;
- }
- }
-
- Children.Add(textBlock);
- }
- }
- }
- }
-
- static void DiffChanged(object sender, DependencyPropertyChangedEventArgs e)
- {
- ((DiffView)sender).UpdateContents();
- }
- }
-}
diff --git a/src/GitHub.InlineReviews/Views/PullRequestCommentsPane.cs b/src/GitHub.InlineReviews/Views/PullRequestCommentsPane.cs
deleted file mode 100644
index 3bc70a5a5f..0000000000
--- a/src/GitHub.InlineReviews/Views/PullRequestCommentsPane.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// Copyright (c) Company. All rights reserved.
-//
-//------------------------------------------------------------------------------
-
-using System.Runtime.InteropServices;
-using GitHub.Api;
-using GitHub.Extensions;
-using GitHub.InlineReviews.ViewModels;
-using GitHub.Services;
-using Microsoft.VisualStudio.Shell;
-using Task = System.Threading.Tasks.Task;
-
-namespace GitHub.InlineReviews.Views
-{
- [Guid("aa280a78-f2fa-49cd-b2f9-21426b40501f")]
- public class PullRequestCommentsPane : ToolWindowPane
- {
- readonly PullRequestCommentsView view;
- IPullRequestSession session;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public PullRequestCommentsPane() : base(null)
- {
- this.Caption = "Pull Request Comments";
- this.Content = view = new PullRequestCommentsView();
- }
-
- public async Task Initialize(
- IPullRequestSession pullRequestSession,
- IApiClient apiClient)
- {
- Guard.ArgumentNotNull(pullRequestSession, nameof(pullRequestSession));
- Guard.ArgumentNotNull(apiClient, nameof(apiClient));
-
- if (this.session != null)
- return;
-
- this.session = pullRequestSession;
-
- var viewModel = new PullRequestCommentsViewModel(pullRequestSession);
- await viewModel.Initialize();
- view.DataContext = viewModel;
- }
- }
-}
diff --git a/src/GitHub.InlineReviews/Views/PullRequestCommentsView.xaml b/src/GitHub.InlineReviews/Views/PullRequestCommentsView.xaml
deleted file mode 100644
index 025200f53a..0000000000
--- a/src/GitHub.InlineReviews/Views/PullRequestCommentsView.xaml
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- Thanks @StanleyGoldman! Might also be nice to also be able to sort by PR number?
-
-
-
-
-
-
-
-
- .Subscribe(a => UpdateFilter(SelectedState, SelectedAssignee, a));
-
-+ this.WhenAny(x => x.SelectedSortOrder, x => x.Value)
-+ .Where(x => pullRequests != null)
-
-
-
-
-
-
- @StanleyGoldman Ooops, I missed a x != null check here, and it's breaking the tests. Do you want to add it or shall I?
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/GitHub.InlineReviews/Views/PullRequestCommentsView.xaml.cs b/src/GitHub.InlineReviews/Views/PullRequestCommentsView.xaml.cs
deleted file mode 100644
index b95d9e7d5e..0000000000
--- a/src/GitHub.InlineReviews/Views/PullRequestCommentsView.xaml.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Windows.Controls;
-using GitHub.VisualStudio.UI.Helpers;
-
-namespace GitHub.InlineReviews
-{
- public partial class PullRequestCommentsView : UserControl
- {
- public PullRequestCommentsView()
- {
- this.InitializeComponent();
- PreviewMouseWheel += ScrollViewerUtilities.FixMouseWheelScroll;
- }
- }
-}
\ No newline at end of file
diff --git a/src/GitHub.InlineReviews/packages.config b/src/GitHub.InlineReviews/packages.config
index 52a4e8fd39..9c2010990b 100644
--- a/src/GitHub.InlineReviews/packages.config
+++ b/src/GitHub.InlineReviews/packages.config
@@ -33,6 +33,8 @@
+
+
diff --git a/src/GitHub.UI/Assets/Controls.xaml b/src/GitHub.UI/Assets/Controls.xaml
index 47977c775a..78925a17ec 100644
--- a/src/GitHub.UI/Assets/Controls.xaml
+++ b/src/GitHub.UI/Assets/Controls.xaml
@@ -419,7 +419,130 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/GitHub.UI/Controls/DropDownButton.cs b/src/GitHub.UI/Controls/DropDownButton.cs
new file mode 100644
index 0000000000..7e06388725
--- /dev/null
+++ b/src/GitHub.UI/Controls/DropDownButton.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+
+namespace GitHub.UI
+{
+ public class DropDownButton : ContentControl
+ {
+ public static readonly DependencyProperty AutoCloseOnClickProperty =
+ DependencyProperty.Register("AutoCloseOnClick", typeof(bool), typeof(DropDownButton));
+ public static readonly DependencyProperty DropDownContentProperty =
+ DependencyProperty.Register(nameof(DropDownContent), typeof(object), typeof(DropDownButton));
+ public static readonly DependencyProperty IsOpenProperty =
+ Popup.IsOpenProperty.AddOwner(typeof(DropDownButton));
+
+ Button button;
+ Popup popup;
+
+ static DropDownButton()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(
+ typeof(DropDownButton),
+ new FrameworkPropertyMetadata(typeof(DropDownButton)));
+ }
+
+ public bool AutoCloseOnClick
+ {
+ get { return (bool)GetValue(AutoCloseOnClickProperty); }
+ set { SetValue(AutoCloseOnClickProperty, value); }
+ }
+
+ public object DropDownContent
+ {
+ get { return GetValue(DropDownContentProperty); }
+ set { SetValue(DropDownContentProperty, value); }
+ }
+
+ public bool IsOpen
+ {
+ get { return (bool)GetValue(IsOpenProperty); }
+ set { SetValue(IsOpenProperty, value); }
+ }
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+ button = (Button)Template.FindName("PART_Button", this);
+ popup = (Popup)Template.FindName("PART_Popup", this);
+ button.Click += ButtonClick;
+ popup.Opened += PopupOpened;
+ popup.Closed += PopupClosed;
+ popup.AddHandler(MouseUpEvent, new RoutedEventHandler(PopupMouseUp), true);
+ }
+
+ void ButtonClick(object sender, RoutedEventArgs e)
+ {
+ IsOpen = true;
+ }
+
+ private void PopupOpened(object sender, EventArgs e)
+ {
+ IsHitTestVisible = false;
+ }
+
+ private void PopupClosed(object sender, EventArgs e)
+ {
+ IsOpen = false;
+ IsHitTestVisible = true;
+ }
+
+ private void PopupMouseUp(object sender, RoutedEventArgs e)
+ {
+ if (AutoCloseOnClick)
+ {
+ IsOpen = false;
+ }
+ }
+ }
+}
diff --git a/src/GitHub.UI/Controls/DropDownButton.xaml b/src/GitHub.UI/Controls/DropDownButton.xaml
new file mode 100644
index 0000000000..995237b5c1
--- /dev/null
+++ b/src/GitHub.UI/Controls/DropDownButton.xaml
@@ -0,0 +1,24 @@
+
+
+
\ No newline at end of file
diff --git a/src/GitHub.UI/Controls/TrimmedPathTextBlock.cs b/src/GitHub.UI/Controls/TrimmedPathTextBlock.cs
new file mode 100644
index 0000000000..d272f9fd77
--- /dev/null
+++ b/src/GitHub.UI/Controls/TrimmedPathTextBlock.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace GitHub.UI
+{
+ ///
+ /// TextBlock that displays a path and intelligently trims with ellipsis when the path doesn't
+ /// fit in the allocated size.
+ ///
+ ///
+ /// When displaying a path that is too long for its allocated space, we need to trim the path
+ /// with ellipses intelligently instead of simply trimming the end (as this is the filename
+ /// which is the most important part!). This control trims a path in the following manner with
+ /// decreasing allocated space:
+ ///
+ /// - VisualStudio\src\GitHub.UI\Controls\TrimmedPathTextBlock.cs
+ /// - VisualStudio\...\GitHub.UI\Controls\TrimmedPathTextBlock.cs
+ /// - VisualStudio\...\...\Controls\TrimmedPathTextBlock.cs
+ /// - VisualStudio\...\...\...\TrimmedPathTextBlock.cs
+ /// - ...\...\...\...\TrimmedPathTextBlock.cs
+ ///
+ public class TrimmedPathTextBlock : FrameworkElement
+ {
+ public static readonly DependencyProperty FontFamilyProperty =
+ TextBlock.FontFamilyProperty.AddOwner(typeof(TrimmedPathTextBlock));
+ public static readonly DependencyProperty FontSizeProperty =
+ TextBlock.FontSizeProperty.AddOwner(typeof(TrimmedPathTextBlock));
+ public static readonly DependencyProperty FontStretchProperty =
+ TextBlock.FontStretchProperty.AddOwner(typeof(TrimmedPathTextBlock));
+ public static readonly DependencyProperty FontStyleProperty =
+ TextBlock.FontStyleProperty.AddOwner(typeof(TrimmedPathTextBlock));
+ public static readonly DependencyProperty FontWeightProperty =
+ TextBlock.FontWeightProperty.AddOwner(typeof(TrimmedPathTextBlock));
+ public static readonly DependencyProperty ForegroundProperty =
+ TextBlock.ForegroundProperty.AddOwner(typeof(TrimmedPathTextBlock));
+ public static readonly DependencyProperty TextProperty =
+ TextBlock.TextProperty.AddOwner(
+ typeof(TrimmedPathTextBlock),
+ new FrameworkPropertyMetadata(
+ null,
+ FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
+ TextChanged));
+
+ FormattedText formattedText;
+ FormattedText renderText;
+
+ public FontFamily FontFamily
+ {
+ get { return (FontFamily)GetValue(FontFamilyProperty); }
+ set { SetValue(FontFamilyProperty, value); }
+ }
+
+ public double FontSize
+ {
+ get { return (double)GetValue(FontSizeProperty); }
+ set { SetValue(FontSizeProperty, value); }
+ }
+
+ public FontStretch FontStretch
+ {
+ get { return (FontStretch)GetValue(FontStretchProperty); }
+ set { SetValue(FontStretchProperty, value); }
+ }
+
+ public FontStyle FontStyle
+ {
+ get { return (FontStyle)GetValue(FontStyleProperty); }
+ set { SetValue(FontStyleProperty, value); }
+ }
+
+ public FontWeight FontWeight
+ {
+ get { return (FontWeight)GetValue(FontWeightProperty); }
+ set { SetValue(FontWeightProperty, value); }
+ }
+
+ public Brush Foreground
+ {
+ get { return (Brush)GetValue(ForegroundProperty); }
+ set { SetValue(ForegroundProperty, value); }
+ }
+
+ public string Text
+ {
+ get { return (string)GetValue(TextProperty); }
+ set { SetValue(TextProperty, value); }
+ }
+
+ protected FormattedText FormattedText
+ {
+ get
+ {
+ if (formattedText == null && Text != null)
+ {
+ formattedText = CreateFormattedText(Text);
+ }
+
+ return formattedText;
+ }
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var parts = Text
+ .Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar })
+ .ToList();
+ var nextPart = Math.Min(1, parts.Count - 1);
+
+ while (true)
+ {
+ renderText = CreateFormattedText(string.Join(Path.DirectorySeparatorChar.ToString(), parts));
+
+ if (renderText.Width <= availableSize.Width || nextPart == -1)
+ break;
+
+ parts[nextPart] = "\u2026";
+
+ if (nextPart == 0)
+ nextPart = -1;
+ else if (nextPart == parts.Count - 2)
+ nextPart = 0;
+ else
+ nextPart++;
+ };
+
+ return new Size(renderText.Width, renderText.Height);
+ }
+
+ protected override void OnRender(DrawingContext drawingContext)
+ {
+ drawingContext.DrawText(renderText, new Point());
+ }
+
+ FormattedText CreateFormattedText(string text)
+ {
+ return new FormattedText(
+ text,
+ CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight,
+ new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
+ FontSize,
+ Foreground);
+ }
+
+ static void TextChanged(object sender, DependencyPropertyChangedEventArgs e)
+ {
+ var textBlock = sender as TrimmedPathTextBlock;
+
+ if (textBlock != null)
+ {
+ textBlock.formattedText = null;
+ textBlock.renderText = null;
+ }
+ }
+ }
+}
diff --git a/src/GitHub.UI/Converters/NotEqualsToVisibilityConverter.cs b/src/GitHub.UI/Converters/NotEqualsToVisibilityConverter.cs
new file mode 100644
index 0000000000..42d186302b
--- /dev/null
+++ b/src/GitHub.UI/Converters/NotEqualsToVisibilityConverter.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+using System.Windows.Markup;
+
+namespace GitHub.UI
+{
+ public class NotEqualsToVisibilityConverter : MarkupExtension, IValueConverter
+ {
+ readonly string collapsedValue;
+
+ public NotEqualsToVisibilityConverter(string collapsedValue)
+ {
+ this.collapsedValue = collapsedValue;
+ }
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value?.ToString() != collapsedValue ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override object ProvideValue(IServiceProvider serviceProvider) => this;
+ }
+}
diff --git a/src/GitHub.UI/Converters/TrimNewlinesConverter.cs b/src/GitHub.UI/Converters/TrimNewlinesConverter.cs
new file mode 100644
index 0000000000..d7dee09fd7
--- /dev/null
+++ b/src/GitHub.UI/Converters/TrimNewlinesConverter.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+namespace GitHub.UI
+{
+ ///
+ /// An that trims newlines and tabs from a string and replaces them
+ /// with spaces.
+ ///
+ public class TrimNewlinesConverter : ValueConverterMarkupExtension
+ {
+ public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var text = value as string;
+ if (String.IsNullOrEmpty(text)) return null;
+ return Regex.Replace(text, @"\t|\n|\r", " ");
+ }
+ }
+}
diff --git a/src/GitHub.UI/GitHub.UI.csproj b/src/GitHub.UI/GitHub.UI.csproj
index ed3bbb281e..b5e6c4b839 100644
--- a/src/GitHub.UI/GitHub.UI.csproj
+++ b/src/GitHub.UI/GitHub.UI.csproj
@@ -79,6 +79,7 @@
+
True
True
@@ -88,15 +89,18 @@
Spinner.xaml
+
+
+
@@ -217,6 +221,10 @@
MSBuild:Compile
Designer
+
+ MSBuild:Compile
+ Designer
+
MSBuild:Compile
@@ -239,6 +247,10 @@
MSBuild:Compile
+
+ MSBuild:Compile
+ Designer
+
diff --git a/src/GitHub.UI/Themes/Generic.xaml b/src/GitHub.UI/Themes/Generic.xaml
new file mode 100644
index 0000000000..41baf6c6ff
--- /dev/null
+++ b/src/GitHub.UI/Themes/Generic.xaml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/GitHub.VisualStudio.UI/Resources.Designer.cs b/src/GitHub.VisualStudio.UI/Resources.Designer.cs
index 419c0cdf9d..b829867d16 100644
--- a/src/GitHub.VisualStudio.UI/Resources.Designer.cs
+++ b/src/GitHub.VisualStudio.UI/Resources.Designer.cs
@@ -60,6 +60,33 @@ internal Resources() {
}
}
+ ///
+ /// Looks up a localized string similar to Add review comment.
+ ///
+ public static string AddReviewComment {
+ get {
+ return ResourceManager.GetString("AddReviewComment", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Add a single comment.
+ ///
+ public static string AddSingleComment {
+ get {
+ return ResourceManager.GetString("AddSingleComment", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Add your review.
+ ///
+ public static string AddYourReview {
+ get {
+ return ResourceManager.GetString("AddYourReview", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Invalid authentication code.
///
@@ -150,6 +177,15 @@ public static string CompareFileAsDefaultAction {
}
}
+ ///
+ /// Looks up a localized string similar to Continue your review.
+ ///
+ public static string ContinueYourReview {
+ get {
+ return ResourceManager.GetString("ContinueYourReview", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Could not connect to github.com.
///
@@ -771,6 +807,15 @@ public static string resendCodeButtonToolTip {
}
}
+ ///
+ /// Looks up a localized string similar to Reviewers.
+ ///
+ public static string Reviewers {
+ get {
+ return ResourceManager.GetString("Reviewers", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Sign in....
///
diff --git a/src/GitHub.VisualStudio.UI/Resources.resx b/src/GitHub.VisualStudio.UI/Resources.resx
index 27da1f9985..c54e0086db 100644
--- a/src/GitHub.VisualStudio.UI/Resources.resx
+++ b/src/GitHub.VisualStudio.UI/Resources.resx
@@ -401,6 +401,21 @@
Token
+
+ Continue your review
+
+
+ Add your review
+
+
+ Reviewers
+
+
+ Add review comment
+
+
+ Add a single comment
+
Debugging
diff --git a/src/GitHub.VisualStudio.UI/Styles/GitHubActionLink.xaml b/src/GitHub.VisualStudio.UI/Styles/GitHubActionLink.xaml
index b4a2cd459d..60218fc5ff 100644
--- a/src/GitHub.VisualStudio.UI/Styles/GitHubActionLink.xaml
+++ b/src/GitHub.VisualStudio.UI/Styles/GitHubActionLink.xaml
@@ -13,6 +13,15 @@
+
+
+
@@ -40,8 +49,7 @@
-
-
+
diff --git a/src/GitHub.VisualStudio.UI/Styles/GitHubTabControl.xaml b/src/GitHub.VisualStudio.UI/Styles/GitHubTabControl.xaml
index b041d7bc99..5b9b6c0dc3 100644
--- a/src/GitHub.VisualStudio.UI/Styles/GitHubTabControl.xaml
+++ b/src/GitHub.VisualStudio.UI/Styles/GitHubTabControl.xaml
@@ -1,7 +1,7 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- View conversation on GitHub
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ HeaderText="{Binding Files.ChangedFilesCount, StringFormat={x:Static prop:Resources.ChangesCountFormat}}"
+ Margin="0 8 10 0">
+
-
+
\ No newline at end of file
diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml.cs b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml.cs
index 038160144a..542a62b614 100644
--- a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml.cs
+++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml.cs
@@ -1,32 +1,17 @@
using System;
using System.ComponentModel.Composition;
using System.Globalization;
-using System.Linq;
using System.Reactive.Linq;
using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Documents;
using System.Windows.Input;
-using System.Windows.Media;
-using GitHub.Commands;
using GitHub.Exports;
using GitHub.Extensions;
-using GitHub.Models;
using GitHub.Services;
using GitHub.UI;
using GitHub.UI.Helpers;
using GitHub.ViewModels.GitHubPane;
using GitHub.VisualStudio.UI.Helpers;
-using Microsoft.VisualStudio;
-using Microsoft.VisualStudio.Editor;
-using Microsoft.VisualStudio.Shell;
-using Microsoft.VisualStudio.Shell.Interop;
-using Microsoft.VisualStudio.Text;
-using Microsoft.VisualStudio.Text.Editor;
-using Microsoft.VisualStudio.Text.Projection;
-using Microsoft.VisualStudio.TextManager.Interop;
using ReactiveUI;
-using Task = System.Threading.Tasks.Task;
namespace GitHub.VisualStudio.Views.GitHubPane
{
@@ -47,38 +32,12 @@ public PullRequestDetailView()
this.WhenActivated(d =>
{
d(ViewModel.OpenOnGitHub.Subscribe(_ => DoOpenOnGitHub()));
- d(ViewModel.DiffFile.Subscribe(x => DoDiffFile((IPullRequestFileNode)x, false).Forget()));
- d(ViewModel.ViewFile.Subscribe(x => DoOpenFile((IPullRequestFileNode)x, false).Forget()));
- d(ViewModel.DiffFileWithWorkingDirectory.Subscribe(x => DoDiffFile((IPullRequestFileNode)x, true).Forget()));
- d(ViewModel.OpenFileInWorkingDirectory.Subscribe(x => DoOpenFile((IPullRequestFileNode)x, true).Forget()));
});
-
- bodyGrid.RequestBringIntoView += BodyFocusHack;
}
- [Import]
- ITeamExplorerServiceHolder TeamExplorerServiceHolder { get; set; }
-
[Import]
IVisualStudioBrowser VisualStudioBrowser { get; set; }
- [Import]
- IEditorOptionsFactoryService EditorOptionsFactoryService { get; set; }
-
- [Import]
- IUsageTracker UsageTracker { get; set; }
-
- [Import]
- IPullRequestEditorService NavigationService { get; set; }
-
- [Import]
- IVsEditorAdaptersFactoryService EditorAdaptersFactoryService { get; set; }
-
- protected override void OnVisualParentChanged(DependencyObject oldParent)
- {
- base.OnVisualParentChanged(oldParent);
- }
-
void DoOpenOnGitHub()
{
var browser = VisualStudioBrowser;
@@ -93,315 +52,6 @@ static Uri ToPullRequestUrl(string host, string owner, string repositoryName, in
return new Uri(url);
}
- async Task DoOpenFile(IPullRequestFileNode file, bool workingDirectory)
- {
- try
- {
- var fullPath = ViewModel.GetLocalFilePath(file);
- var fileName = workingDirectory ? fullPath : await ViewModel.ExtractFile(file, true);
-
- using (workingDirectory ? null : OpenInProvisionalTab())
- {
- var window = GitHub.VisualStudio.Services.Dte.ItemOperations.OpenFile(fileName);
- window.Document.ReadOnly = !workingDirectory;
-
- var buffer = GetBufferAt(fileName);
-
- if (!workingDirectory)
- {
- AddBufferTag(buffer, ViewModel.Session, fullPath, null);
-
- var textView = NavigationService.FindActiveView();
- EnableNavigateToEditor(textView, file);
- }
- }
-
- if (workingDirectory)
- await UsageTracker.IncrementCounter(x => x.NumberOfPRDetailsOpenFileInSolution);
- else
- await UsageTracker.IncrementCounter(x => x.NumberOfPRDetailsViewFile);
- }
- catch (Exception e)
- {
- ShowErrorInStatusBar("Error opening file", e);
- }
- }
-
- async Task DoNavigateToEditor(IPullRequestFileNode file)
- {
- try
- {
- if (!ViewModel.IsCheckedOut)
- {
- ShowInfoMessage("Checkout PR branch before opening file in solution.");
- return;
- }
-
- var fullPath = ViewModel.GetLocalFilePath(file);
-
- var activeView = NavigationService.FindActiveView();
- if (activeView == null)
- {
- ShowErrorInStatusBar("Couldn't find active view");
- return;
- }
-
- NavigationService.NavigateToEquivalentPosition(activeView, fullPath);
-
- await UsageTracker.IncrementCounter(x => x.NumberOfPRDetailsNavigateToEditor);
- }
- catch (Exception e)
- {
- ShowErrorInStatusBar("Error navigating to editor", e);
- }
- }
-
- static void ShowInfoMessage(string message)
- {
- ErrorHandler.ThrowOnFailure(VsShellUtilities.ShowMessageBox(
- Services.GitHubServiceProvider, message, null,
- OLEMSGICON.OLEMSGICON_INFO, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST));
- }
-
- async Task DoDiffFile(IPullRequestFileNode file, bool workingDirectory)
- {
- try
- {
- var rightPath = System.IO.Path.Combine(file.DirectoryPath, file.FileName);
- var leftPath = file.OldPath ?? rightPath;
- var rightFile = workingDirectory ? ViewModel.GetLocalFilePath(file) : await ViewModel.ExtractFile(file, true);
- var leftFile = await ViewModel.ExtractFile(file, false);
- var leftLabel = $"{leftPath};{ViewModel.TargetBranchDisplayName}";
- var rightLabel = workingDirectory ? rightPath : $"{rightPath};PR {ViewModel.Model.Number}";
- var caption = $"Diff - {file.FileName}";
- var options = __VSDIFFSERVICEOPTIONS.VSDIFFOPT_DetectBinaryFiles |
- __VSDIFFSERVICEOPTIONS.VSDIFFOPT_LeftFileIsTemporary;
-
- if (!workingDirectory)
- {
- options |= __VSDIFFSERVICEOPTIONS.VSDIFFOPT_RightFileIsTemporary;
- }
-
- IVsWindowFrame frame;
- using (OpenInProvisionalTab())
- {
- var tooltip = $"{leftLabel}\nvs.\n{rightLabel}";
-
- // Diff window will open in provisional (right hand) tab until document is touched.
- frame = GitHub.VisualStudio.Services.DifferenceService.OpenComparisonWindow2(
- leftFile,
- rightFile,
- caption,
- tooltip,
- leftLabel,
- rightLabel,
- string.Empty,
- string.Empty,
- (uint)options);
- }
-
- object docView;
- frame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView, out docView);
- var diffViewer = ((IVsDifferenceCodeWindow)docView).DifferenceViewer;
-
- var session = ViewModel.Session;
- AddBufferTag(diffViewer.LeftView.TextBuffer, session, leftPath, DiffSide.Left);
-
- if (!workingDirectory)
- {
- AddBufferTag(diffViewer.RightView.TextBuffer, session, rightPath, DiffSide.Right);
- EnableNavigateToEditor(diffViewer.LeftView, file);
- EnableNavigateToEditor(diffViewer.RightView, file);
- EnableNavigateToEditor(diffViewer.InlineView, file);
- }
-
- if (workingDirectory)
- await UsageTracker.IncrementCounter(x => x.NumberOfPRDetailsCompareWithSolution);
- else
- await UsageTracker.IncrementCounter(x => x.NumberOfPRDetailsViewChanges);
- }
- catch (Exception e)
- {
- ShowErrorInStatusBar("Error opening file", e);
- }
- }
-
- void AddBufferTag(ITextBuffer buffer, IPullRequestSession session, string path, DiffSide? side)
- {
- buffer.Properties.GetOrCreateSingletonProperty(
- typeof(PullRequestTextBufferInfo),
- () => new PullRequestTextBufferInfo(session, path, side));
-
- var projection = buffer as IProjectionBuffer;
-
- if (projection != null)
- {
- foreach (var source in projection.SourceBuffers)
- {
- AddBufferTag(source, session, path, side);
- }
- }
- }
-
- void EnableNavigateToEditor(IWpfTextView textView, IPullRequestFileNode file)
- {
- var view = EditorAdaptersFactoryService.GetViewAdapter(textView);
- EnableNavigateToEditor(view, file);
- }
-
- void EnableNavigateToEditor(IVsTextView textView, IPullRequestFileNode file)
- {
- var commandGroup = VSConstants.CMDSETID.StandardCommandSet2K_guid;
- var commandId = (int)VSConstants.VSStd2KCmdID.RETURN;
- new TextViewCommandDispatcher(textView, commandGroup, commandId).Exec += async (s, e) => await DoNavigateToEditor(file);
-
- var contextMenuCommandGroup = new Guid(Guids.guidContextMenuSetString);
- var goToCommandId = PkgCmdIDList.openFileInSolutionCommand;
- new TextViewCommandDispatcher(textView, contextMenuCommandGroup, goToCommandId).Exec += async (s, e) => await DoNavigateToEditor(file);
- }
-
- void ShowErrorInStatusBar(string message, Exception e = null)
- {
- var ns = GitHub.VisualStudio.Services.DefaultExportProvider.GetExportedValue();
- if (e != null)
- {
- message += ": " + e.Message;
- }
- ns?.ShowMessage(message);
- }
-
- private void FileListKeyUp(object sender, KeyEventArgs e)
- {
- if (e.Key == Key.Return)
- {
- var file = (e.OriginalSource as FrameworkElement)?.DataContext as IPullRequestFileNode;
- if (file != null)
- {
- DoDiffFile(file, false).Forget();
- }
- }
- }
-
- void FileListMouseDoubleClick(object sender, MouseButtonEventArgs e)
- {
- var file = (e.OriginalSource as FrameworkElement)?.DataContext as IPullRequestFileNode;
-
- if (file != null)
- {
- DoDiffFile(file, false).Forget();
- }
- }
-
- void FileListMouseRightButtonDown(object sender, MouseButtonEventArgs e)
- {
- var item = (e.OriginalSource as Visual)?.GetSelfAndVisualAncestors().OfType().FirstOrDefault();
-
- if (item != null)
- {
- // Select tree view item on right click.
- item.IsSelected = true;
- }
- }
-
- ITextBuffer GetBufferAt(string filePath)
- {
- var editorAdapterFactoryService = GitHub.VisualStudio.Services.ComponentModel.GetService();
- IVsUIHierarchy uiHierarchy;
- uint itemID;
- IVsWindowFrame windowFrame;
-
- if (VsShellUtilities.IsDocumentOpen(
- GitHub.VisualStudio.Services.GitHubServiceProvider,
- filePath,
- Guid.Empty,
- out uiHierarchy,
- out itemID,
- out windowFrame))
- {
- IVsTextView view = VsShellUtilities.GetTextView(windowFrame);
- IVsTextLines lines;
- if (view.GetBuffer(out lines) == 0)
- {
- var buffer = lines as IVsTextBuffer;
- if (buffer != null)
- return editorAdapterFactoryService.GetDataBuffer(buffer);
- }
- }
-
- return null;
- }
-
- void TreeView_ContextMenuOpening(object sender, ContextMenuEventArgs e)
- {
- ApplyContextMenuBinding(sender, e);
- }
-
- void ApplyContextMenuBinding(object sender, ContextMenuEventArgs e) where TItem : Control
- {
- var container = (Control)sender;
- var item = (e.OriginalSource as Visual)?.GetSelfAndVisualAncestors().OfType().FirstOrDefault();
-
- e.Handled = true;
-
- if (item != null)
- {
- var fileNode = item.DataContext as IPullRequestFileNode;
-
- if (fileNode != null)
- {
- container.ContextMenu.DataContext = this.DataContext;
-
- foreach (var menuItem in container.ContextMenu.Items.OfType
@@ -136,6 +137,10 @@
+
+ {b389adaf-62cc-486e-85b4-2d8b078df763}
+ GitHub.Api
+
{e4ed0537-d1d9-44b6-9212-3096d7c3f7a1}
GitHub.Exports.Reactive
diff --git a/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionManagerTests.cs b/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionManagerTests.cs
index 56771a4d49..275bd74f10 100644
--- a/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionManagerTests.cs
+++ b/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionManagerTests.cs
@@ -19,6 +19,7 @@
using NSubstitute;
using NUnit.Framework;
using System.ComponentModel;
+using GitHub.Api;
namespace GitHub.InlineReviews.UnitTests.Services
{
@@ -260,7 +261,8 @@ public async Task InlineCommentThreadsIsSet()
sessionService.BuildCommentThreads(
target.CurrentSession.PullRequest,
FilePath,
- Arg.Any>())
+ Arg.Any>(),
+ Arg.Any())
.Returns(threads);
var file = await target.GetLiveFile(FilePath, textView, textView.TextBuffer);
@@ -284,7 +286,8 @@ public async Task CreatesTrackingPointsForThreads()
sessionService.BuildCommentThreads(
target.CurrentSession.PullRequest,
FilePath,
- Arg.Any>())
+ Arg.Any>(),
+ Arg.Any())
.Returns(threads);
var file = (PullRequestSessionLiveFile)await target.GetLiveFile(FilePath, textView, textView.TextBuffer);
@@ -307,7 +310,8 @@ public async Task MovingToNoRepositoryShouldNullOutProperties()
sessionService.BuildCommentThreads(
target.CurrentSession.PullRequest,
FilePath,
- Arg.Any>())
+ Arg.Any>(),
+ Arg.Any())
.Returns(threads);
var file = (PullRequestSessionLiveFile)await target.GetLiveFile(FilePath, textView, textView.TextBuffer);
@@ -346,7 +350,8 @@ public async Task ModifyingBufferMarksThreadsAsStaleAndSignalsRebuild()
sessionService.BuildCommentThreads(
target.CurrentSession.PullRequest,
FilePath,
- Arg.Any>())
+ Arg.Any>(),
+ Arg.Any())
.Returns(threads);
var file = (PullRequestSessionLiveFile)await target.GetLiveFile(FilePath, textView, textView.TextBuffer);
@@ -635,7 +640,7 @@ public async Task UpdatingCurrentSessionPullRequestTriggersLinesChanged()
CreateInlineCommentThreadModel(expectedLineNumber),
};
- sessionService.BuildCommentThreads(null, null, null).ReturnsForAnyArgs(threads);
+ sessionService.BuildCommentThreads(null, null, null, null).ReturnsForAnyArgs(threads);
var target = CreateTarget(sessionService: sessionService);
var file = await target.GetLiveFile(FilePath, textView, textView.TextBuffer);
@@ -671,6 +676,7 @@ IPullRequestSessionService CreateRealSessionService(IDiffService diff)
Substitute.For(),
diff,
Substitute.For(),
+ Substitute.For(),
Substitute.For());
result.CreateRebuildSignal().Returns(new Subject());
result.GetPullRequestMergeBase(Arg.Any(), Arg.Any())
diff --git a/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionServiceTests.cs b/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionServiceTests.cs
index 355c7e74ea..53d60fd5c7 100644
--- a/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionServiceTests.cs
+++ b/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionServiceTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
+using GitHub.Api;
using GitHub.Factories;
using GitHub.InlineReviews.Services;
using GitHub.InlineReviews.UnitTests.TestDoubles;
@@ -46,7 +47,8 @@ Line 2
var result = target.BuildCommentThreads(
pullRequest,
FilePath,
- diff);
+ diff,
+ "HEAD_SHA");
var thread = result.Single();
Assert.That(2, Is.EqualTo(thread.LineNumber));
@@ -70,7 +72,8 @@ public async Task IgnoreCommentsWithNoDiffLineContext()
var result = target.BuildCommentThreads(
pullRequest,
FilePath,
- diff);
+ diff,
+ "HEAD_SHA");
Assert.That(result, Is.Empty);
}
@@ -105,7 +108,8 @@ Line 2
var result = target.BuildCommentThreads(
pullRequest,
FilePath,
- diff);
+ diff,
+ "HEAD_SHA");
var thread = result.Single();
Assert.That(4, Is.EqualTo(thread.LineNumber));
@@ -145,7 +149,8 @@ Line 2
var result = target.BuildCommentThreads(
pullRequest,
FilePath,
- diff);
+ diff,
+ "HEAD_SHA");
Assert.That(2, Is.EqualTo(result.Count));
Assert.That(-1, Is.EqualTo(result[1].LineNumber));
@@ -184,7 +189,8 @@ Line 2
var result = target.BuildCommentThreads(
pullRequest,
winFilePath,
- diff);
+ diff,
+ "HEAD_SHA");
var thread = result.First();
Assert.That(4, Is.EqualTo(thread.LineNumber));
@@ -226,7 +232,8 @@ Line 2
var threads = target.BuildCommentThreads(
pullRequest,
FilePath,
- diff);
+ diff,
+ "HEAD_SHA");
Assert.That(2, Is.EqualTo(threads[0].LineNumber));
@@ -271,7 +278,8 @@ Line 2
var threads = target.BuildCommentThreads(
pullRequest,
FilePath,
- diff);
+ diff,
+ "HEAD_SHA");
threads[0].IsStale = true;
var changedLines = target.UpdateCommentThreads(threads, diff);
@@ -289,6 +297,7 @@ static PullRequestSessionService CreateTarget(IDiffService diffService)
Substitute.For(),
diffService,
Substitute.For(),
+ Substitute.For(),
Substitute.For());
}
diff --git a/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionTests.cs b/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionTests.cs
index 82be2cf01e..56caf6cf76 100644
--- a/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionTests.cs
+++ b/test/GitHub.InlineReviews.UnitTests/Services/PullRequestSessionTests.cs
@@ -3,6 +3,7 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using GitHub.Api;
using GitHub.Extensions;
using GitHub.Factories;
using GitHub.InlineReviews.Services;
@@ -22,13 +23,156 @@ public class PullRequestSessionTests
const string RepoUrl = "https://foo.bar/owner/repo";
const string FilePath = "test.cs";
+ public class TheHasPendingReviewProperty
+ {
+ [Test]
+ public void IsFalseWithNoPendingReview()
+ {
+ var target = new PullRequestSession(
+ CreateRealSessionService(),
+ Substitute.For(),
+ CreatePullRequest(),
+ Substitute.For(),
+ "owner",
+ true);
+
+ Assert.That(target.HasPendingReview, Is.False);
+ }
+
+ [Test]
+ public void IsFalseWithPendingReviewForOtherUser()
+ {
+ var currentUser = CreateAccount("grokys");
+ var otherUser = CreateAccount("shana");
+ var pr = CreatePullRequest();
+ var review = CreatePullRequestReview(otherUser, PullRequestReviewState.Pending);
+ pr.Reviews.Returns(new[] { review });
+
+ var target = new PullRequestSession(
+ CreateRealSessionService(),
+ currentUser,
+ pr,
+ Substitute.For(),
+ "owner",
+ true);
+
+ Assert.That(target.HasPendingReview, Is.False);
+ }
+
+ [Test]
+ public void IsFalseWithNonPendingReviewForCurrentUser()
+ {
+ var currentUser = CreateAccount("grokys");
+ var pr = CreatePullRequest();
+ var review = CreatePullRequestReview(currentUser, PullRequestReviewState.Approved);
+ pr.Reviews.Returns(new[] { review });
+
+ var target = new PullRequestSession(
+ CreateRealSessionService(),
+ currentUser,
+ pr,
+ Substitute.For(),
+ "owner",
+ true);
+
+ Assert.That(target.HasPendingReview, Is.False);
+ }
+
+ [Test]
+ public void IsTrueWithPendingReviewForCurrentUser()
+ {
+ var currentUser = Substitute.For();
+ var pr = CreatePullRequest();
+ var review = CreatePullRequestReview(currentUser, PullRequestReviewState.Pending);
+ pr.Reviews.Returns(new[] { review });
+
+ var target = new PullRequestSession(
+ CreateRealSessionService(),
+ currentUser,
+ pr,
+ Substitute.For(),
+ "owner",
+ true);
+
+ Assert.That(target.HasPendingReview, Is.True);
+ }
+
+ [Test]
+ public async Task IsTrueWithUpdatedWithPendingReview()
+ {
+ var currentUser = Substitute.For();
+ var target = new PullRequestSession(
+ CreateRealSessionService(),
+ currentUser,
+ CreatePullRequest(),
+ Substitute.For(),
+ "owner",
+ true);
+
+ Assert.That(target.HasPendingReview, Is.False);
+
+ var pr = CreatePullRequest();
+ var review = CreatePullRequestReview(currentUser, PullRequestReviewState.Pending);
+ pr.Reviews.Returns(new[] { review });
+ await target.Update(pr);
+
+ Assert.That(target.HasPendingReview, Is.True);
+ }
+
+ [Test]
+ public async Task IsTrueWhenStartReviewCalled()
+ {
+ var currentUser = Substitute.For();
+ var service = Substitute.For();
+ var review = CreatePullRequestReview(currentUser, PullRequestReviewState.Pending);
+ service.CreatePendingReview(null, null, null).ReturnsForAnyArgs(review);
+
+ var target = new PullRequestSession(
+ service,
+ currentUser,
+ CreatePullRequest(),
+ CreateLocalRepository(),
+ "owner",
+ true);
+
+ Assert.That(target.HasPendingReview, Is.False);
+
+ await target.StartReview();
+
+ Assert.That(target.HasPendingReview, Is.True);
+ }
+
+ [Test]
+ public async Task IsFalseWhenReviewCancelled()
+ {
+ var currentUser = Substitute.For();
+ var pr = CreatePullRequest();
+ var review = CreatePullRequestReview(currentUser, PullRequestReviewState.Pending);
+ pr.Reviews.Returns(new[] { review });
+
+ var target = new PullRequestSession(
+ Substitute.For(),
+ currentUser,
+ pr,
+ Substitute.For(),
+ "owner",
+ true);
+
+ Assert.That(target.HasPendingReview, Is.True);
+
+ await target.CancelReview();
+
+ Assert.That(target.HasPendingReview, Is.False);
+ }
+ }
+
public class TheGetFileMethod
{
[Test]
public async Task BaseShaIsSet()
{
var target = new PullRequestSession(
- CreateSessionService(),
+ CreateRealSessionService(),
Substitute.For(),
CreatePullRequest(),
Substitute.For(),
@@ -40,10 +184,10 @@ public async Task BaseShaIsSet()
}
[Test]
- public async Task CommitShaIsSet()
+ public async Task HeadCommitShaIsSet()
{
var target = new PullRequestSession(
- CreateSessionService(),
+ CreateRealSessionService(),
Substitute.For(),
CreatePullRequest(),
Substitute.For(),
@@ -52,13 +196,30 @@ public async Task CommitShaIsSet()
var file = await target.GetFile(FilePath);
Assert.That("HEAD_SHA", Is.SameAs(file.CommitSha));
+ Assert.That(file.IsTrackingHead, Is.True);
+ }
+
+ [Test]
+ public async Task PinnedCommitShaIsSet()
+ {
+ var target = new PullRequestSession(
+ CreateRealSessionService(),
+ Substitute.For(),
+ CreatePullRequest(),
+ Substitute.For(),
+ "owner",
+ true);
+ var file = await target.GetFile(FilePath, "123");
+
+ Assert.That("123", Is.SameAs(file.CommitSha));
+ Assert.That(file.IsTrackingHead, Is.False);
}
[Test]
public async Task DiffShaIsSet()
{
var diff = new List();
- var sessionService = CreateSessionService();
+ var sessionService = CreateRealSessionService();
sessionService.Diff(
Arg.Any(),
@@ -99,7 +260,7 @@ Line 2
using (var diffService = new FakeDiffService())
{
var pullRequest = CreatePullRequest(comment);
- var service = CreateSessionService(diffService);
+ var service = CreateRealSessionService(diffService);
diffService.AddFile(FilePath, baseContents, "MERGE_BASE");
diffService.AddFile(FilePath, headContents, "HEAD_SHA");
@@ -117,22 +278,251 @@ Line 2
Assert.That(2, Is.EqualTo(thread.LineNumber));
}
}
+
+ [Test]
+ public async Task SameNonHeadCommitShasReturnSameFiles()
+ {
+ var target = new PullRequestSession(
+ CreateRealSessionService(),
+ Substitute.For(),
+ CreatePullRequest(),
+ Substitute.For(),
+ "owner",
+ true);
+ var file1 = await target.GetFile(FilePath, "123");
+ var file2 = await target.GetFile(FilePath, "123");
+
+ Assert.That(file1, Is.SameAs(file2));
+ }
+
+ [Test]
+ public async Task DifferentCommitShasReturnDifferentFiles()
+ {
+ var target = new PullRequestSession(
+ CreateRealSessionService(),
+ Substitute.For(),
+ CreatePullRequest(),
+ Substitute.For(),
+ "owner",
+ true);
+ var file1 = await target.GetFile(FilePath, "123");
+ var file2 = await target.GetFile(FilePath, "456");
+
+ Assert.That(file1, Is.Not.SameAs(file2));
+ }
}
- public class ThePostReviewCommentMethod
+ public class TheCancelReviewMethod
{
[Test]
- public async Task PostsToCorrectFork()
+ public void ThrowsWithNoPendingReview()
+ {
+ var target = new PullRequestSession(
+ CreateRealSessionService(),
+ Substitute.For(),
+ CreatePullRequest(),
+ Substitute.For(),
+ "owner",
+ true);
+
+ Assert.ThrowsAsync(async () => await target.CancelReview());
+ }
+
+ [Test]
+ public async Task CallsServiceWithNodeId()
{
var service = Substitute.For();
- var target = CreateTarget(service, "fork", "owner");
+ var target = CreateTargetWithPendingReview(service);
- await target.PostReviewComment("New Comment", "COMMIT_ID", "file.cs", 1);
+ await target.CancelReview();
- await service.Received(1).PostReviewComment(
+ await service.Received(1).CancelPendingReview(
Arg.Any(),
+ "nodeId1");
+ }
+
+ [Test]
+ public async Task RemovesReviewFromModel()
+ {
+ var service = Substitute.For();
+ var target = CreateTargetWithPendingReview(service);
+
+ await target.CancelReview();
+
+ Assert.IsEmpty(target.PullRequest.Reviews);
+ }
+
+ [Test]
+ public async Task RemovesCommentsFromModel()
+ {
+ var service = Substitute.For();
+ var target = CreateTargetWithPendingReview(service);
+
+ await target.CancelReview();
+
+ Assert.IsEmpty(target.PullRequest.ReviewComments);
+ }
+
+ public static PullRequestSession CreateTargetWithPendingReview(
+ IPullRequestSessionService service)
+ {
+ var currentUser = Substitute.For();
+ var pr = CreatePullRequest();
+ var review = CreatePullRequestReview(currentUser, PullRequestReviewState.Pending);
+ var comment = Substitute.For();
+
+ comment.PullRequestReviewId.Returns(1);
+ pr.Reviews.Returns(new[] { review });
+ pr.ReviewComments.Returns(new[] { comment });
+
+ return new PullRequestSession(
+ service,
+ currentUser,
+ pr,
+ Substitute.For(),
+ "owner",
+ true);
+ }
+ }
+
+ public class ThePostReviewMethod
+ {
+ [Test]
+ public async Task PostsToCorrectForkWithNoPendingReview()
+ {
+ var service = Substitute.For();
+ var target = CreateTarget(service, "fork", "owner", false);
+
+ await target.PostReview("New Review", Octokit.PullRequestReviewEvent.Approve);
+
+ await service.Received(1).PostReview(
+ target.LocalRepository,
"owner",
- Arg.Any(),
+ target.User,
+ PullRequestNumber,
+ "HEAD_SHA",
+ "New Review",
+ Octokit.PullRequestReviewEvent.Approve);
+ }
+
+ [Test]
+ public async Task PostsToCorrectForkWithPendingReview()
+ {
+ var service = Substitute.For();
+ var target = CreateTarget(service, "fork", "owner", true);
+
+ await target.PostReview("New Review", Octokit.PullRequestReviewEvent.RequestChanges);
+
+ await service.Received(1).SubmitPendingReview(
+ target.LocalRepository,
+ target.User,
+ "pendingReviewId",
+ "New Review",
+ Octokit.PullRequestReviewEvent.RequestChanges);
+ }
+
+ [Test]
+ public async Task AddsReviewToModel()
+ {
+ var service = Substitute.For();
+ var target = CreateTarget(service, "fork", "owner", false);
+
+ var model = await target.PostReview("New Review", Octokit.PullRequestReviewEvent.RequestChanges);
+
+ Assert.That(target.PullRequest.Reviews.Last(), Is.SameAs(model));
+ }
+
+ [Test]
+ public async Task ReplacesPendingReviewWithModel()
+ {
+ var service = Substitute.For();
+
+ var target = CreateTarget(service, "fork", "owner", true);
+
+ Assert.That(
+ target.PullRequest.Reviews.Where(x => x.State == PullRequestReviewState.Pending).Count(),
+ Is.EqualTo(1));
+
+ var submittedReview = CreatePullRequestReview(target.User, PullRequestReviewState.Approved);
+ submittedReview.NodeId.Returns("pendingReviewId");
+ service.SubmitPendingReview(null, null, null, null, Octokit.PullRequestReviewEvent.Approve)
+ .ReturnsForAnyArgs(submittedReview);
+
+ var model = await target.PostReview("New Review", Octokit.PullRequestReviewEvent.Approve);
+
+ Assert.That(
+ target.PullRequest.Reviews.Where(x => x.State == PullRequestReviewState.Pending).Count(),
+ Is.Zero);
+ }
+
+ [Test]
+ public async Task MarksAssociatedCommentsAsNonPending()
+ {
+ var service = Substitute.For();
+ var target = CreateTarget(service, "fork", "owner", true);
+
+ Assert.That(target.PullRequest.ReviewComments[0].IsPending, Is.True);
+
+ var submittedReview = CreatePullRequestReview(target.User, PullRequestReviewState.Approved);
+ submittedReview.NodeId.Returns("pendingReviewId");
+ service.SubmitPendingReview(null, null, null, null, Octokit.PullRequestReviewEvent.Approve)
+ .ReturnsForAnyArgs(submittedReview);
+ var model = await target.PostReview("New Review", Octokit.PullRequestReviewEvent.RequestChanges);
+
+ target.PullRequest.ReviewComments[0].Received(1).IsPending = false;
+ }
+
+ PullRequestSession CreateTarget(
+ IPullRequestSessionService service,
+ string localRepositoryOwner,
+ string remoteRepositoryOwner,
+ bool hasPendingReview)
+ {
+ var repository = Substitute.For();
+
+ repository.CloneUrl.Returns(new UriString($"https://github.com/{localRepositoryOwner}/reop"));
+ repository.Owner.Returns(localRepositoryOwner);
+ repository.Name.Returns("repo");
+
+ var pr = CreatePullRequest();
+ var user = Substitute.For();
+
+ if (hasPendingReview)
+ {
+ var reviewComment = Substitute.For();
+ reviewComment.PullRequestReviewId.Returns(1);
+ reviewComment.IsPending.Returns(true);
+ pr.ReviewComments.Returns(new[] { reviewComment });
+
+ var review = CreatePullRequestReview(user, PullRequestReviewState.Pending);
+ review.NodeId.Returns("pendingReviewId");
+ pr.Reviews.Returns(new[] { review });
+ }
+
+ return new PullRequestSession(
+ service,
+ user,
+ pr,
+ repository,
+ remoteRepositoryOwner,
+ true);
+ }
+ }
+
+ public class ThePostReviewCommentMethod
+ {
+ [Test]
+ public async Task PostsToCorrectForkWithNoPendingReview()
+ {
+ var service = Substitute.For();
+ var target = CreateTarget(service, "fork", "owner", false);
+
+ await target.PostReviewComment("New Comment", "COMMIT_ID", "file.cs", new DiffChunk[0], 1);
+
+ await service.Received(1).PostStandaloneReviewComment(
+ target.LocalRepository,
+ "owner",
+ target.User,
PullRequestNumber,
"New Comment",
"COMMIT_ID",
@@ -141,26 +531,61 @@ await service.Received(1).PostReviewComment(
}
[Test]
- public async Task PostsReplyToCorrectFork()
+ public async Task PostsReplyToCorrectForkWithNoPendingReview()
{
var service = Substitute.For();
- var target = CreateTarget(service, "fork", "owner");
+ var target = CreateTarget(service, "fork", "owner", false);
- await target.PostReviewComment("New Comment", 1);
+ await target.PostReviewComment("New Comment", 1, "node1");
- await service.Received(1).PostReviewComment(
- Arg.Any(),
+ await service.Received(1).PostStandaloneReviewCommentRepy(
+ target.LocalRepository,
"owner",
- Arg.Any(),
+ target.User,
PullRequestNumber,
"New Comment",
1);
}
+ [Test]
+ public async Task PostsToCorrectForkWithPendingReview()
+ {
+ var service = Substitute.For();
+ var target = CreateTarget(service, "fork", "owner", true);
+
+ await target.PostReviewComment("New Comment", "COMMIT_ID", "file.cs", new DiffChunk[0], 1);
+
+ await service.Received(1).PostPendingReviewComment(
+ target.LocalRepository,
+ target.User,
+ "pendingReviewId",
+ "New Comment",
+ "COMMIT_ID",
+ "file.cs",
+ 1);
+ }
+
+ [Test]
+ public async Task PostsReplyToCorrectForkWithPendingReview()
+ {
+ var service = Substitute.For();
+ var target = CreateTarget(service, "fork", "owner", true);
+
+ await target.PostReviewComment("New Comment", 1, "node1");
+
+ await service.Received(1).PostPendingReviewCommentReply(
+ target.LocalRepository,
+ target.User,
+ "pendingReviewId",
+ "New Comment",
+ "node1");
+ }
+
PullRequestSession CreateTarget(
IPullRequestSessionService service,
string localRepositoryOwner,
- string remoteRepositoryOwner)
+ string remoteRepositoryOwner,
+ bool hasPendingReview)
{
var repository = Substitute.For();
@@ -168,10 +593,20 @@ PullRequestSession CreateTarget(
repository.Owner.Returns(localRepositoryOwner);
repository.Name.Returns("repo");
+ var pr = CreatePullRequest();
+ var user = Substitute.For();
+
+ if (hasPendingReview)
+ {
+ var review = CreatePullRequestReview(user, PullRequestReviewState.Pending);
+ review.NodeId.Returns("pendingReviewId");
+ pr.Reviews.Returns(new[] { review });
+ }
+
return new PullRequestSession(
service,
- Substitute.For(),
- CreatePullRequest(),
+ user,
+ pr,
repository,
remoteRepositoryOwner,
true);
@@ -184,7 +619,7 @@ public class TheUpdateMethod
public async Task UpdatesThePullRequestModel()
{
var target = new PullRequestSession(
- CreateSessionService(),
+ CreateRealSessionService(),
Substitute.For(),
CreatePullRequest(),
Substitute.For(),
@@ -201,7 +636,7 @@ public async Task UpdatesThePullRequestModel()
}
[Test]
- public async Task AddsNewReviewCommentToThread()
+ public async Task AddsNewReviewCommentToThreadOnHeadFile()
{
var baseContents = @"Line 1
Line 2
@@ -211,7 +646,6 @@ Line 3
Line 2
Line 3 with comment
Line 4";
-
var comment1 = CreateComment(@"@@ -1,4 +1,4 @@
Line 1
Line 2
@@ -226,7 +660,7 @@ Line 2
using (var diffService = new FakeDiffService())
{
var pullRequest = CreatePullRequest(comment1);
- var service = CreateSessionService(diffService);
+ var service = CreateRealSessionService(diffService);
diffService.AddFile(FilePath, baseContents, "MERGE_BASE");
diffService.AddFile(FilePath, headContents, "HEAD_SHA");
@@ -239,14 +673,68 @@ Line 2
"owner",
true);
- var file = await target.GetFile(FilePath);
+ var file = await target.GetFile(FilePath, "HEAD");
- Assert.That(1, Is.EqualTo(file.InlineCommentThreads[0].Comments.Count));
+ Assert.That(file.InlineCommentThreads[0].Comments, Has.Count.EqualTo(1));
+ Assert.That(file.InlineCommentThreads[0].LineNumber, Is.EqualTo(2));
pullRequest = CreatePullRequest(comment1, comment2);
await target.Update(pullRequest);
- Assert.That(2, Is.EqualTo(file.InlineCommentThreads[0].Comments.Count));
+ Assert.That(file.InlineCommentThreads[0].Comments, Has.Count.EqualTo(2));
+ Assert.That(file.InlineCommentThreads[0].LineNumber, Is.EqualTo(2));
+ }
+ }
+
+ [Test]
+ public async Task AddsNewReviewCommentToThreadNonHeadFile()
+ {
+ var baseContents = @"Line 1
+Line 2
+Line 3
+Line 4";
+ var headContents = @"Line 1
+Line 2
+Line 3 with comment
+Line 4";
+
+ var comment1 = CreateComment(@"@@ -1,4 +1,4 @@
+ Line 1
+ Line 2
+-Line 3
++Line 3 with comment", "Comment1");
+ var comment2 = CreateComment(@"@@ -1,4 +1,4 @@
+ Line 1
+ Line 2
+-Line 3
++Line 3 with comment", "Comment2");
+
+ using (var diffService = new FakeDiffService())
+ {
+ var pullRequest = CreatePullRequest(comment1);
+ var service = CreateRealSessionService(diffService);
+
+ diffService.AddFile(FilePath, baseContents, "MERGE_BASE");
+ diffService.AddFile(FilePath, headContents, "123");
+
+ var target = new PullRequestSession(
+ service,
+ Substitute.For(),
+ pullRequest,
+ Substitute.For(),
+ "owner",
+ true);
+
+ var file = await target.GetFile(FilePath, "123");
+
+ Assert.That(file.InlineCommentThreads[0].Comments, Has.Count.EqualTo(1));
+ Assert.That(file.InlineCommentThreads[0].LineNumber, Is.EqualTo(2));
+
+ pullRequest = CreatePullRequest(comment1, comment2);
+ await target.Update(pullRequest);
+
+ Assert.That(file.InlineCommentThreads[0].Comments, Has.Count.EqualTo(2));
+ Assert.That(file.InlineCommentThreads[0].LineNumber, Is.EqualTo(2));
}
}
@@ -262,7 +750,7 @@ Line 2
using (var diffService = new FakeDiffService())
{
var pullRequest = CreatePullRequest(comment);
- var service = CreateSessionService(diffService);
+ var service = CreateRealSessionService(diffService);
var target = new PullRequestSession(
service,
@@ -284,6 +772,13 @@ Line 2
}
}
+ static IAccount CreateAccount(string login)
+ {
+ var result = Substitute.For();
+ result.Login.Returns(login);
+ return result;
+ }
+
static IPullRequestReviewCommentModel CreateComment(string diffHunk, string body = "Comment")
{
var result = Substitute.For();
@@ -322,6 +817,19 @@ static IPullRequestModel CreatePullRequest(params IPullRequestReviewCommentModel
return result;
}
+ static IPullRequestReviewModel CreatePullRequestReview(
+ IAccount author,
+ PullRequestReviewState state,
+ long id = 1)
+ {
+ var result = Substitute.For();
+ result.Id.Returns(id);
+ result.NodeId.Returns("nodeId" + id);
+ result.User.Returns(author);
+ result.State.Returns(state);
+ return result;
+ }
+
static IRepository CreateRepository()
{
var result = Substitute.For();
@@ -333,13 +841,21 @@ static IRepository CreateRepository()
return result;
}
- static IPullRequestSessionService CreateSessionService(IDiffService diffService = null)
+ static ILocalRepositoryModel CreateLocalRepository()
+ {
+ var result = Substitute.For();
+ result.CloneUrl.Returns(new UriString("https://github.com/owner/repo"));
+ return result;
+ }
+
+ static IPullRequestSessionService CreateRealSessionService(IDiffService diffService = null)
{
var result = Substitute.ForPartsOf(
Substitute.For(),
Substitute.For(),
diffService ?? Substitute.For(),
Substitute.For(),
+ Substitute.For(),
Substitute.For());
result.GetTipSha(Arg.Any()).Returns("BRANCH_TIP");
diff --git a/test/GitHub.InlineReviews.UnitTests/Tags/InlineCommentTaggerTests.cs b/test/GitHub.InlineReviews.UnitTests/Tags/InlineCommentTaggerTests.cs
index 617c9bf129..99d16f3528 100644
--- a/test/GitHub.InlineReviews.UnitTests/Tags/InlineCommentTaggerTests.cs
+++ b/test/GitHub.InlineReviews.UnitTests/Tags/InlineCommentTaggerTests.cs
@@ -140,6 +140,48 @@ public void ShouldRaiseTagsChangedOnFileLinesChanged()
Assert.True(raised);
}
+ [Test]
+ public void ShouldCallSessionGetFileWithCorrectCommitSha()
+ {
+ var sessionManager = CreateSessionManager(
+ CreateSessionFile(),
+ DiffSide.Right,
+ "123");
+ var session = sessionManager.CurrentSession;
+ var target = new InlineCommentTagger(
+ Substitute.For(),
+ Substitute.For(),
+ sessionManager);
+
+ // Line 11 has an add diff entry.
+ var span = CreateSpan(11);
+ var firstPass = target.GetTags(span);
+ var result = target.GetTags(span).ToList();
+
+ session.Received(1).GetFile("file.cs", "123");
+ }
+
+ [Test]
+ public void ShouldAlwaysCallSessionGetFileWithHeadCommitShaForLeftHandSide()
+ {
+ var sessionManager = CreateSessionManager(
+ CreateSessionFile(),
+ DiffSide.Left,
+ "123");
+ var session = sessionManager.CurrentSession;
+ var target = new InlineCommentTagger(
+ Substitute.For(),
+ Substitute.For(),
+ sessionManager);
+
+ // Line 11 has an add diff entry.
+ var span = CreateSpan(11);
+ var firstPass = target.GetTags(span);
+ var result = target.GetTags(span).ToList();
+
+ session.Received(1).GetFile("file.cs", "HEAD");
+ }
+
static IPullRequestSessionFile CreateSessionFile()
{
var diffChunk = new DiffChunk
@@ -182,13 +224,15 @@ static IPullRequestSessionManager CreateSessionManager(DiffSide side)
static IPullRequestSessionManager CreateSessionManager(
IPullRequestSessionFile file,
- DiffSide side)
+ DiffSide side,
+ string bufferInfoCommitSha = "HEAD")
{
var session = Substitute.For();
- session.GetFile("file.cs").Returns(file);
+ session.GetFile("file.cs", bufferInfoCommitSha).Returns(file);
- var info = new PullRequestTextBufferInfo(session, "file.cs", side);
+ var info = new PullRequestTextBufferInfo(session, "file.cs", bufferInfoCommitSha, side);
var result = Substitute.For();
+ result.CurrentSession.Returns(session);
result.GetTextBufferInfo(null).ReturnsForAnyArgs(info);
return result;
}
diff --git a/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs b/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs
index 1f9e259067..bf983d5f5d 100644
--- a/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs
+++ b/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentPeekViewModelTests.cs
@@ -67,6 +67,34 @@ public async Task ThreadIsCreatedForNewComment()
Assert.That(CommentEditState.Editing, Is.EqualTo(target.Thread.Comments[0].EditState));
}
+ [Test]
+ public async Task ShouldGetRelativePathFromTextBufferInfoIfPresent()
+ {
+ var session = CreateSession();
+ var bufferInfo = new PullRequestTextBufferInfo(session, RelativePath, "123", DiffSide.Right);
+ var sessionManager = CreateSessionManager(
+ relativePath: "ShouldNotUseThis",
+ session: session,
+ textBufferInfo: bufferInfo);
+
+ // There is an existing comment thread at line 10.
+ var target = new InlineCommentPeekViewModel(
+ CreatePeekService(lineNumber: 10),
+ CreatePeekSession(),
+ sessionManager,
+ Substitute.For(),
+ Substitute.For());
+
+ await target.Initialize();
+
+ // There should be an existing comment and a reply placeholder.
+ Assert.That(target.Thread, Is.InstanceOf(typeof(InlineCommentThreadViewModel)));
+ Assert.That(2, Is.EqualTo(target.Thread.Comments.Count));
+ Assert.That("Existing comment", Is.EqualTo(target.Thread.Comments[0].Body));
+ Assert.That(string.Empty, Is.EqualTo(target.Thread.Comments[1].Body));
+ Assert.That(CommentEditState.Placeholder, Is.EqualTo(target.Thread.Comments[1].EditState));
+ }
+
[Test]
public async Task SwitchesFromNewThreadToExistingThreadWhenCommentPosted()
{
@@ -85,7 +113,12 @@ public async Task SwitchesFromNewThreadToExistingThreadWhenCommentPosted()
target.Thread.Comments[0].Body = "New Comment";
sessionManager.CurrentSession
- .When(x => x.PostReviewComment(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()))
+ .When(x => x.PostReviewComment(
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any(),
+ Arg.Any>(),
+ Arg.Any()))
.Do(async x =>
{
// Simulate the thread being added to the session.
@@ -162,7 +195,7 @@ public async Task RetainsCommentBeingEditedWhenSessionRefreshed()
}
[Test]
- public async Task DoesntRetainSubmittedCommentInPlaceholderAfterPost()
+ public async Task CommittingEditDoesntRetainSubmittedCommentInPlaceholderAfterPost()
{
var sessionManager = CreateSessionManager();
var peekSession = CreatePeekSession();
@@ -177,7 +210,7 @@ public async Task DoesntRetainSubmittedCommentInPlaceholderAfterPost()
Assert.That(2, Is.EqualTo(target.Thread.Comments.Count));
- sessionManager.CurrentSession.PostReviewComment(null, 0)
+ sessionManager.CurrentSession.PostReviewComment(null, 0, null)
.ReturnsForAnyArgs(async x =>
{
var file = await sessionManager.GetLiveFile(
@@ -198,6 +231,43 @@ public async Task DoesntRetainSubmittedCommentInPlaceholderAfterPost()
Assert.That(string.Empty, Is.EqualTo(placeholder.Body));
}
+ [Test]
+ public async Task StartingReviewDoesntRetainSubmittedCommentInPlaceholderAfterPost()
+ {
+ var sessionManager = CreateSessionManager();
+ var peekSession = CreatePeekSession();
+ var target = new InlineCommentPeekViewModel(
+ CreatePeekService(lineNumber: 10),
+ peekSession,
+ sessionManager,
+ Substitute.For(),
+ Substitute.For());
+
+ await target.Initialize();
+
+ Assert.That(2, Is.EqualTo(target.Thread.Comments.Count));
+
+ sessionManager.CurrentSession.StartReview()
+ .ReturnsForAnyArgs(async x =>
+ {
+ var file = await sessionManager.GetLiveFile(
+ RelativePath,
+ peekSession.TextView,
+ peekSession.TextView.TextBuffer);
+ RaiseLinesChanged(file, Tuple.Create(10, DiffSide.Right));
+ return Substitute.For();
+ });
+
+ var placeholder = (IPullRequestReviewCommentViewModel)target.Thread.Comments.Last();
+ placeholder.BeginEdit.Execute(null);
+ placeholder.Body = "Comment being edited";
+ placeholder.StartReview.Execute(null);
+
+ placeholder = (IPullRequestReviewCommentViewModel)target.Thread.Comments.Last();
+ Assert.That(CommentEditState.Placeholder, Is.EqualTo(placeholder.EditState));
+ Assert.That(string.Empty, Is.EqualTo(placeholder.Body));
+ }
+
void AddCommentToExistingThread(IPullRequestSessionFile file)
{
var newThreads = file.InlineCommentThreads.ToList();
@@ -259,7 +329,18 @@ IPeekSession CreatePeekSession()
return result;
}
- IPullRequestSessionManager CreateSessionManager(string commitSha = "COMMIT")
+ IPullRequestSession CreateSession()
+ {
+ var result = Substitute.For();
+ result.LocalRepository.CloneUrl.Returns(new UriString("https://foo.bar"));
+ return result;
+ }
+
+ IPullRequestSessionManager CreateSessionManager(
+ string commitSha = "COMMIT",
+ string relativePath = RelativePath,
+ IPullRequestSession session = null,
+ PullRequestTextBufferInfo textBufferInfo = null)
{
var thread = CreateThread(10, "Existing comment");
@@ -286,13 +367,18 @@ IPullRequestSessionManager CreateSessionManager(string commitSha = "COMMIT")
file.InlineCommentThreads.Returns(new[] { thread });
file.LinesChanged.Returns(new Subject>>());
- var session = Substitute.For();
- session.LocalRepository.CloneUrl.Returns(new UriString("https://foo.bar"));
+ session = session ?? CreateSession();
+
+ if (textBufferInfo != null)
+ {
+ session.GetFile(textBufferInfo.RelativePath, textBufferInfo.CommitSha).Returns(file);
+ }
var result = Substitute.For();
result.CurrentSession.Returns(session);
- result.GetLiveFile(RelativePath, Arg.Any(), Arg.Any()).Returns(file);
- result.GetRelativePath(Arg.Any()).Returns(RelativePath);
+ result.GetLiveFile(relativePath, Arg.Any(), Arg.Any()).Returns(file);
+ result.GetRelativePath(Arg.Any()).Returns(relativePath);
+ result.GetTextBufferInfo(Arg.Any()).Returns(textBufferInfo);
return result;
}
diff --git a/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentThreadViewModelTests.cs b/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentThreadViewModelTests.cs
index bba17ce346..b5e17c7ad9 100644
--- a/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentThreadViewModelTests.cs
+++ b/test/GitHub.InlineReviews.UnitTests/ViewModels/InlineCommentThreadViewModelTests.cs
@@ -65,7 +65,7 @@ public void PostsCommentInReplyToCorrectComment()
target.Comments[2].Body = "New Comment";
target.Comments[2].CommitEdit.Execute(null);
- session.Received(1).PostReviewComment("New Comment", 1);
+ session.Received(1).PostReviewComment("New Comment", 1, "node1");
}
IApiClient CreateApiClient()
@@ -81,6 +81,7 @@ IPullRequestReviewCommentModel CreateComment(int id, string body)
var comment = Substitute.For();
comment.Body.Returns(body);
comment.Id.Returns(id);
+ comment.NodeId.Returns("node" + id);
return comment;
}
diff --git a/test/GitHub.InlineReviews.UnitTests/ViewModels/NewInlineCommentThreadViewModelTests.cs b/test/GitHub.InlineReviews.UnitTests/ViewModels/NewInlineCommentThreadViewModelTests.cs
index 6151bdc7d9..333b88bc98 100644
--- a/test/GitHub.InlineReviews.UnitTests/ViewModels/NewInlineCommentThreadViewModelTests.cs
+++ b/test/GitHub.InlineReviews.UnitTests/ViewModels/NewInlineCommentThreadViewModelTests.cs
@@ -8,6 +8,7 @@
using NSubstitute;
using Octokit;
using NUnit.Framework;
+using System.Collections.Generic;
namespace GitHub.InlineReviews.UnitTests.ViewModels
{
@@ -87,6 +88,7 @@ public void PostsCommentToCorrectAddedLine()
"New Comment",
"COMMIT_SHA",
"file.cs",
+ Arg.Any>(),
5);
}
@@ -116,6 +118,7 @@ public void AddsCommentToCorrectDeletedLine()
"New Comment",
"COMMIT_SHA",
"file.cs",
+ Arg.Any>(),
7);
}
diff --git a/test/GitHub.InlineReviews.UnitTests/ViewModels/PullRequestReviewCommentViewModelTests.cs b/test/GitHub.InlineReviews.UnitTests/ViewModels/PullRequestReviewCommentViewModelTests.cs
new file mode 100644
index 0000000000..f9becd8117
--- /dev/null
+++ b/test/GitHub.InlineReviews.UnitTests/ViewModels/PullRequestReviewCommentViewModelTests.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Reactive.Linq;
+using GitHub.InlineReviews.ViewModels;
+using GitHub.Models;
+using GitHub.Services;
+using NSubstitute;
+using NUnit.Framework;
+using ReactiveUI;
+
+namespace GitHub.InlineReviews.UnitTests.ViewModels
+{
+ public class PullRequestReviewCommentViewModelTests
+ {
+ public class TheCanStartReviewProperty
+ {
+ [Test]
+ public void IsFalseWhenSessionHasPendingReview()
+ {
+ var session = CreateSession(true);
+ var target = CreateTarget(session);
+
+ Assert.That(target.CanStartReview, Is.False);
+ }
+
+ [Test]
+ public void IsTrueWhenSessionHasNoPendingReview()
+ {
+ var session = CreateSession(false);
+ var target = CreateTarget(session);
+
+ Assert.That(target.CanStartReview, Is.True);
+ }
+ }
+
+ public class TheCommitCaptionProperty
+ {
+ [Test]
+ public void IsAddReviewCommentWhenSessionHasPendingReview()
+ {
+ var session = CreateSession(true);
+ var target = CreateTarget(session);
+
+ Assert.That(target.CommitCaption, Is.EqualTo("Add review comment"));
+ }
+
+ [Test]
+ public void IsAddSingleCommentWhenSessionHasNoPendingReview()
+ {
+ var session = CreateSession(false);
+ var target = CreateTarget(session);
+
+ Assert.That(target.CommitCaption, Is.EqualTo("Add a single comment"));
+ }
+ }
+
+ public class TheStartReviewCommand
+ {
+ [Test]
+ public void IsDisabledWhenSessionHasPendingReview()
+ {
+ var session = CreateSession(true);
+ var target = CreateTarget(session);
+
+ Assert.That(target.StartReview.CanExecute(null), Is.False);
+ }
+
+ [Test]
+ public void IsDisabledWhenSessionHasNoPendingReview()
+ {
+ var session = CreateSession(false);
+ var target = CreateTarget(session);
+
+ Assert.That(target.StartReview.CanExecute(null), Is.False);
+ }
+
+ [Test]
+ public void IsEnabledWhenSessionHasNoPendingReviewAndBodyNotEmpty()
+ {
+ var session = CreateSession(false);
+ var target = CreateTarget(session);
+
+ target.Body = "body";
+
+ Assert.That(target.StartReview.CanExecute(null), Is.True);
+ }
+
+ [Test]
+ public void CallsSessionStartReview()
+ {
+ var session = CreateSession(false);
+ var target = CreateTarget(session);
+
+ target.Body = "body";
+ target.StartReview.Execute(null);
+
+ session.Received(1).StartReview();
+ }
+ }
+
+ static PullRequestReviewCommentViewModel CreateTarget(
+ IPullRequestSession session = null,
+ ICommentThreadViewModel thread = null)
+ {
+ session = session ?? CreateSession();
+ thread = thread ?? CreateThread();
+
+ return new PullRequestReviewCommentViewModel(
+ session,
+ thread,
+ Substitute.For(),
+ Substitute.For());
+ }
+
+ static IPullRequestSession CreateSession(
+ bool hasPendingReview = false)
+ {
+ var result = Substitute.For();
+ result.HasPendingReview.Returns(hasPendingReview);
+ return result;
+ }
+
+ static ICommentThreadViewModel CreateThread(
+ bool canPost = true)
+ {
+ var result = Substitute.For();
+ result.PostComment.Returns(new ReactiveCommand(Observable.Return(canPost), _ => null));
+ return result;
+ }
+ }
+}
diff --git a/test/UnitTests/GitHub.Api/SimpleApiClientTests.cs b/test/UnitTests/GitHub.Api/SimpleApiClientTests.cs
index 980da768df..34679192a8 100644
--- a/test/UnitTests/GitHub.Api/SimpleApiClientTests.cs
+++ b/test/UnitTests/GitHub.Api/SimpleApiClientTests.cs
@@ -237,7 +237,7 @@ private static Repository CreateRepository(int id, bool hasWiki)
{
return new Repository("", "", "", "", "", "", "",
id, new User(), "", "", "", "", "", false, false, 0, 0, "",
- 0, null, DateTimeOffset.Now, DateTimeOffset.Now, new RepositoryPermissions(), null, null, false,
+ 0, null, DateTimeOffset.Now, DateTimeOffset.Now, new RepositoryPermissions(), null, null, null, false,
hasWiki, false, false, 0, 0, null, null, null);
}
}
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..844c2f3c95 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();
@@ -90,14 +88,14 @@ public async Task CanRetrieveAndCacheLicenses()
{
var data = new[]
{
- new LicenseMetadata("mit", "MIT", new Uri("https://github.com/")),
- new LicenseMetadata("apache", "Apache", new Uri("https://github.com/"))
+ new LicenseMetadata("mit", "MIT", "foo", "https://github.com/", false),
+ new LicenseMetadata("apache", "Apache", "foo", "https://github.com/", false)
};
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/Services/PullRequestServiceTests.cs b/test/UnitTests/GitHub.App/Services/PullRequestServiceTests.cs
index 63dc45e80c..35678eafa7 100644
--- a/test/UnitTests/GitHub.App/Services/PullRequestServiceTests.cs
+++ b/test/UnitTests/GitHub.App/Services/PullRequestServiceTests.cs
@@ -579,203 +579,87 @@ static ILocalRepositoryModel CreateLocalRepositoryModel(Repository repo)
static Signature Author => new Signature("foo", "foo@bar.com", DateTimeOffset.Now);
- public class TheExtractFileMethod
+ public class TheExtractToTempFileMethod
{
[Test]
- public async Task ExtractHead()
+ public async Task ExtractsExistingFile()
{
- var baseFileContent = "baseFileContent";
- var headFileContent = "headFileContent";
- var fileName = "fileName";
- var baseSha = "baseSha";
- var headSha = "headSha";
- var head = true;
-
- var file = await ExtractFile(baseSha, baseFileContent, headSha, headFileContent, baseSha, baseFileContent,
- fileName, head, Encoding.UTF8);
-
- Assert.That(headFileContent, Is.EqualTo(File.ReadAllText(file)));
- }
+ var gitClient = MockGitClient();
+ var target = CreateTarget(gitClient);
+ var repository = Substitute.For();
+ var fileContent = "file content";
+ var pr = CreatePullRequest();
- [Test]
- public async Task ExtractBase_MergeBaseAvailable_UseMergeBaseSha()
- {
- var baseFileContent = "baseFileContent";
- var headFileContent = "headFileContent";
- var mergeBaseFileContent = "mergeBaseFileContent";
- var fileName = "fileName";
- var baseSha = "baseSha";
- var headSha = "headSha";
- var mergeBaseSha = "mergeBaseSha";
- var head = false;
-
- var file = await ExtractFile(baseSha, baseFileContent, headSha, headFileContent, mergeBaseSha, mergeBaseFileContent,
- fileName, head, Encoding.UTF8);
-
- Assert.That(mergeBaseFileContent, Is.EqualTo(File.ReadAllText(file)));
- }
+ gitClient.ExtractFile(Arg.Any(), "123", "filename").Returns(GetFileTask(fileContent));
+ var file = await target.ExtractToTempFile(repository, pr, "filename", "123", Encoding.UTF8);
- [Test]
- public void MergeBaseNotAvailable_ThrowsNotFoundException()
- {
- var baseFileContent = "baseFileContent";
- var headFileContent = "headFileContent";
- var mergeBaseFileContent = null as string;
- var fileName = "fileName";
- var baseSha = "baseSha";
- var headSha = "headSha";
- var mergeBaseSha = null as string;
- var head = false;
- var mergeBaseException = new NotFoundException();
-
- var ex = Assert.ThrowsAsync(() => ExtractFile(baseSha, baseFileContent, headSha, headFileContent, mergeBaseSha, mergeBaseFileContent,
- fileName, head, Encoding.UTF8, mergeBaseException: mergeBaseException));
+ try
+ {
+ Assert.That(File.ReadAllText(file), Is.EqualTo(fileContent));
+ }
+ finally
+ {
+ File.Delete(file);
+ }
}
[Test]
- public async Task FileAdded_BaseFileEmpty()
+ public async Task CreatesEmptyFileForNonExistentFile()
{
- var baseFileContent = null as string;
- var headFileContent = "headFileContent";
- var fileName = "fileName";
- var baseSha = "baseSha";
- var headSha = "headSha";
- var head = false;
-
- var file = await ExtractFile(baseSha, baseFileContent, headSha, headFileContent, baseSha, baseFileContent,
- fileName, head, Encoding.UTF8);
+ var gitClient = MockGitClient();
+ var target = CreateTarget(gitClient);
+ var repository = Substitute.For();
+ var pr = CreatePullRequest();
- Assert.That(string.Empty, Is.EqualTo(File.ReadAllText(file)));
- }
+ gitClient.ExtractFile(Arg.Any(), "123", "filename").Returns(GetFileTask(null));
+ var file = await target.ExtractToTempFile(repository, pr, "filename", "123", Encoding.UTF8);
- [Test]
- public async Task FileDeleted_HeadFileEmpty()
- {
- var baseFileContent = "baseFileContent";
- var headFileContent = null as string;
- var fileName = "fileName";
- var baseSha = "baseSha";
- var headSha = "headSha";
- var baseRef = new GitReferenceModel("ref", "label", baseSha, "uri");
- var headRef = new GitReferenceModel("ref", "label", headSha, "uri");
- var head = true;
-
- var file = await ExtractFile(baseSha, baseFileContent, headSha, headFileContent, baseSha, baseFileContent,
- fileName, head, Encoding.UTF8);
-
- Assert.That(string.Empty, Is.EqualTo(File.ReadAllText(file)));
+ try
+ {
+ Assert.That(File.ReadAllText(file), Is.EqualTo(string.Empty));
+ }
+ finally
+ {
+ File.Delete(file);
+ }
}
// https://github.com/github/VisualStudio/issues/1010
[TestCase("utf-8")] // Unicode (UTF-8)
[TestCase("Windows-1252")] // Western European (Windows)
- public async Task ChangeEncoding(string encodingName)
+ public async Task CanChangeEncoding(string encodingName)
{
var encoding = Encoding.GetEncoding(encodingName);
var repoDir = Path.GetTempPath();
- var baseFileContent = "baseFileContent";
- var headFileContent = null as string;
var fileName = "fileName.txt";
- var baseSha = "baseSha";
- var headSha = "headSha";
- var baseRef = new GitReferenceModel("ref", "label", baseSha, "uri");
- var head = false;
-
- var file = await ExtractFile(baseSha, baseFileContent, headSha, headFileContent,
- baseSha, baseFileContent, fileName, head, encoding, repoDir);
+ var fileContent = "file content";
+ var gitClient = MockGitClient();
+ var target = CreateTarget(gitClient);
+ var repository = Substitute.For();
+ var pr = CreatePullRequest();
var expectedPath = Path.Combine(repoDir, fileName);
- var expectedContent = baseFileContent;
+ var expectedContent = fileContent;
File.WriteAllText(expectedPath, expectedContent, encoding);
- Assert.That(File.ReadAllText(expectedPath), Is.EqualTo(File.ReadAllText(file)));
- Assert.That(File.ReadAllBytes(expectedPath), Is.EqualTo(File.ReadAllBytes(file)));
- }
-
- static bool HasPreamble(string file, Encoding encoding)
- {
- using (var stream = File.OpenRead(file))
- {
- foreach (var b in encoding.GetPreamble())
- {
- if (b != stream.ReadByte())
- {
- return false;
- }
- }
- }
-
- return true;
- }
-
- static async Task ExtractFile(
- string baseSha, object baseFileContent, string headSha, object headFileContent, string mergeBaseSha, object mergeBaseFileContent,
- string fileName, bool head, Encoding encoding, string repoDir = "repoDir", int pullNumber = 666, string baseRef = "baseRef", string headRef = "headRef",
- Exception mergeBaseException = null)
- {
- var repositoryModel = Substitute.For();
- repositoryModel.LocalPath.Returns(repoDir);
-
- var pullRequest = Substitute.For();
- pullRequest.Number.Returns(1);
-
- pullRequest.Base.Returns(new GitReferenceModel(baseRef, "label", baseSha, "uri"));
- pullRequest.Head.Returns(new GitReferenceModel("ref", "label", headSha, "uri"));
-
- var serviceProvider = Substitutes.ServiceProvider;
- var gitClient = MockGitClient();
- var gitService = serviceProvider.GetGitService();
- var service = new PullRequestService(gitClient, gitService, serviceProvider.GetOperatingSystem(), Substitute.For());
+ gitClient.ExtractFile(Arg.Any(), "123", "filename").Returns(GetFileTask(fileContent));
+ var file = await target.ExtractToTempFile(repository, pr, "filename", "123", encoding);
- if (mergeBaseException == null)
- {
- gitClient.GetPullRequestMergeBase(Arg.Any(), Arg.Any(), baseSha, headSha, baseRef, pullNumber).ReturnsForAnyArgs(Task.FromResult(mergeBaseSha));
- }
- else
+ try
{
- gitClient.GetPullRequestMergeBase(Arg.Any(), Arg.Any(), baseSha, headSha, baseRef, pullNumber).ReturnsForAnyArgs(Task.FromException(mergeBaseException));
+ Assert.That(File.ReadAllText(expectedPath), Is.EqualTo(File.ReadAllText(file)));
+ Assert.That(File.ReadAllBytes(expectedPath), Is.EqualTo(File.ReadAllBytes(file)));
}
-
- gitClient.ExtractFile(Arg.Any(), mergeBaseSha, fileName).Returns(GetFileTask(mergeBaseFileContent));
- gitClient.ExtractFile(Arg.Any(), baseSha, fileName).Returns(GetFileTask(baseFileContent));
- gitClient.ExtractFile(Arg.Any(), headSha, fileName).Returns(GetFileTask(headFileContent));
-
- return await service.ExtractFile(repositoryModel, pullRequest, fileName, head, encoding);
- }
-
- static IObservable GetFileObservable(object fileOrException)
- {
- if (fileOrException is string)
+ finally
{
- return Observable.Return((string)fileOrException);
- }
-
- if (fileOrException is Exception)
- {
- return Observable.Throw((Exception)fileOrException);
+ File.Delete(file);
}
-
- return Observable.Throw(new FileNotFoundException());
}
- static Task GetFileTask(object content)
+ static IPullRequestModel CreatePullRequest()
{
- if (content is string)
- {
- return Task.FromResult((string)content);
- }
-
- if (content is Exception)
- {
- return Task.FromException((Exception)content);
- }
-
- if (content == null)
- {
- return Task.FromResult(null);
- }
-
- throw new ArgumentException("Unsupported content type: " + content);
+ var result = Substitute.For();
+ return result;
}
}
@@ -1105,6 +989,24 @@ public async Task ShouldRemoveUnusedRemote()
}
}
+ static PullRequestService CreateTarget(
+ IGitClient gitClient = null,
+ IGitService gitService = null,
+ IOperatingSystem os = null,
+ IUsageTracker usageTracker = null)
+ {
+ gitClient = gitClient ?? Substitute.For();
+ gitService = gitService ?? Substitute.For();
+ os = os ?? Substitute.For();
+ usageTracker = usageTracker ?? Substitute.For();
+
+ return new PullRequestService(
+ gitClient,
+ gitService,
+ os,
+ usageTracker);
+ }
+
static BranchCollection MockBranches(params string[] names)
{
var result = Substitute.For();
@@ -1138,4 +1040,24 @@ static IGitService MockGitService()
result.GetRepository(Arg.Any()).Returns(repository);
return result;
}
+
+ static Task GetFileTask(object content)
+ {
+ if (content is string)
+ {
+ return Task.FromResult((string)content);
+ }
+
+ if (content is Exception)
+ {
+ return Task.FromException((Exception)content);
+ }
+
+ if (content == null)
+ {
+ return Task.FromResult(null);
+ }
+
+ throw new ArgumentException("Unsupported content type: " + content);
+ }
}
diff --git a/test/UnitTests/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModelTests.cs b/test/UnitTests/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModelTests.cs
index c9e409c7c1..b3c216944e 100644
--- a/test/UnitTests/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModelTests.cs
+++ b/test/UnitTests/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModelTests.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
+using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
@@ -15,7 +17,7 @@
namespace UnitTests.GitHub.App.ViewModels.GitHubPane
{
- public class PullRequestDetailViewModelTests : TestBaseClass
+ public class PullRequestDetailViewModelTests
{
static readonly Uri Uri = new Uri("http://foo");
@@ -26,19 +28,19 @@ public async Task ShouldUsePlaceholderBodyIfNoneExists()
{
var target = CreateTarget();
- await target.Load(CreatePullRequest(body: string.Empty));
+ await target.Load(CreatePullRequestModel(body: string.Empty));
Assert.That("*No description provided.*", Is.EqualTo(target.Body));
}
}
- public class TheHeadProperty
+ public class TheHeadProperty : TestBaseClass
{
[Test]
public async Task ShouldAcceptNullHead()
{
var target = CreateTarget();
- var model = CreatePullRequest();
+ var model = CreatePullRequestModel();
// PullRequest.Head can be null for example if a user deletes the repository after creating the PR.
model.Head = null;
@@ -49,103 +51,112 @@ public async Task ShouldAcceptNullHead()
}
}
- public class TheChangedFilesTreeProperty
+ public class TheReviewsProperty : TestBaseClass
{
[Test]
- public async Task ShouldCreateChangesTree()
+ public async Task ShouldShowLatestAcceptedOrChangesRequestedReview()
{
var target = CreateTarget();
- var pr = CreatePullRequest();
+ var model = CreatePullRequestModel(
+ CreatePullRequestReviewModel(1, "grokys", PullRequestReviewState.ChangesRequested),
+ CreatePullRequestReviewModel(2, "shana", PullRequestReviewState.ChangesRequested),
+ CreatePullRequestReviewModel(3, "grokys", PullRequestReviewState.Approved),
+ CreatePullRequestReviewModel(4, "grokys", PullRequestReviewState.Commented));
- pr.ChangedFiles = new[]
- {
- new PullRequestFileModel("readme.md", "abc", PullRequestFileStatus.Modified),
- new PullRequestFileModel("dir1/f1.cs", "abc", PullRequestFileStatus.Modified),
- new PullRequestFileModel("dir1/f2.cs", "abc", PullRequestFileStatus.Modified),
- new PullRequestFileModel("dir1/dir1a/f3.cs", "abc", PullRequestFileStatus.Modified),
- new PullRequestFileModel("dir2/f4.cs", "abc", PullRequestFileStatus.Modified),
- };
-
- await target.Load(pr);
-
- Assert.That(3, Is.EqualTo(target.ChangedFilesTree.Count));
+ await target.Load(model);
- var dir1 = (PullRequestDirectoryNode)target.ChangedFilesTree[0];
- Assert.That("dir1", Is.EqualTo(dir1.DirectoryName));
- Assert.That(2, Is.EqualTo(dir1.Files.Count));
- Assert.That(1, Is.EqualTo(dir1.Directories.Count));
- Assert.That("f1.cs", Is.EqualTo(dir1.Files[0].FileName));
- Assert.That("f2.cs", Is.EqualTo(dir1.Files[1].FileName));
- Assert.That("dir1", Is.EqualTo(dir1.Files[0].DirectoryPath));
- Assert.That("dir1", Is.EqualTo(dir1.Files[1].DirectoryPath));
+ Assert.That(target.Reviews, Has.Count.EqualTo(3));
+ Assert.That(target.Reviews[0].User.Login, Is.EqualTo("grokys"));
+ Assert.That(target.Reviews[1].User.Login, Is.EqualTo("shana"));
+ Assert.That(target.Reviews[2].User.Login, Is.EqualTo("grokys"));
+ Assert.That(target.Reviews[0].Id, Is.EqualTo(3));
+ Assert.That(target.Reviews[1].Id, Is.EqualTo(2));
+ Assert.That(target.Reviews[2].Id, Is.EqualTo(0));
+ }
- var dir1a = (PullRequestDirectoryNode)dir1.Directories[0];
- Assert.That("dir1a", Is.EqualTo(dir1a.DirectoryName));
- Assert.That(1, Is.EqualTo(dir1a.Files.Count));
- Assert.That(0, Is.EqualTo(dir1a.Directories.Count));
+ [Test]
+ public async Task ShouldShowLatestCommentedReviewIfNothingElsePresent()
+ {
+ var target = CreateTarget();
+ var model = CreatePullRequestModel(
+ CreatePullRequestReviewModel(1, "shana", PullRequestReviewState.Commented),
+ CreatePullRequestReviewModel(2, "shana", PullRequestReviewState.Commented));
- var dir2 = (PullRequestDirectoryNode)target.ChangedFilesTree[1];
- Assert.That("dir2", Is.EqualTo(dir2.DirectoryName));
- Assert.That(1, Is.EqualTo(dir2.Files.Count));
- Assert.That(0, Is.EqualTo(dir2.Directories.Count));
+ await target.Load(model);
- var readme = (PullRequestFileNode)target.ChangedFilesTree[2];
- Assert.That("readme.md", Is.EqualTo(readme.FileName));
+ Assert.That(target.Reviews, Has.Count.EqualTo(2));
+ Assert.That(target.Reviews[0].User.Login, Is.EqualTo("shana"));
+ Assert.That(target.Reviews[1].User.Login, Is.EqualTo("grokys"));
+ Assert.That(target.Reviews[0].Id, Is.EqualTo(2));
}
[Test]
- public async Task FileCommentCountShouldTrackSessionInlineComments()
+ public async Task ShouldNotShowStartNewReviewWhenHasPendingReview()
{
- var pr = CreatePullRequest();
- var file = Substitute.For();
- var thread1 = CreateThread(5);
- var thread2 = CreateThread(6);
- var outdatedThread = CreateThread(-1);
- var session = Substitute.For();
- var sessionManager = Substitute.For();
+ var target = CreateTarget();
+ var model = CreatePullRequestModel(
+ CreatePullRequestReviewModel(1, "grokys", PullRequestReviewState.Pending));
- file.InlineCommentThreads.Returns(new[] { thread1 });
- session.GetFile("readme.md").Returns(Task.FromResult(file));
- sessionManager.GetSession(pr).Returns(Task.FromResult(session));
+ await target.Load(model);
- var target = CreateTarget(sessionManager: sessionManager);
+ Assert.That(target.Reviews, Has.Count.EqualTo(1));
+ Assert.That(target.Reviews[0].User.Login, Is.EqualTo("grokys"));
+ Assert.That(target.Reviews[0].Id, Is.EqualTo(1));
+ }
- pr.ChangedFiles = new[]
- {
- new PullRequestFileModel("readme.md", "abc", PullRequestFileStatus.Modified),
- };
+ [Test]
+ public async Task ShouldShowPendingReviewOverApproved()
+ {
+ var target = CreateTarget();
+ var model = CreatePullRequestModel(
+ CreatePullRequestReviewModel(1, "grokys", PullRequestReviewState.Approved),
+ CreatePullRequestReviewModel(2, "grokys", PullRequestReviewState.Pending));
- await target.Load(pr);
- Assert.That(1, Is.EqualTo(((IPullRequestFileNode)target.ChangedFilesTree[0]).CommentCount));
+ await target.Load(model);
- file.InlineCommentThreads.Returns(new[] { thread1, thread2 });
- RaisePropertyChanged(file, nameof(file.InlineCommentThreads));
- Assert.That(2, Is.EqualTo(((IPullRequestFileNode)target.ChangedFilesTree[0]).CommentCount));
+ Assert.That(target.Reviews, Has.Count.EqualTo(1));
+ Assert.That(target.Reviews[0].User.Login, Is.EqualTo("grokys"));
+ Assert.That(target.Reviews[0].Id, Is.EqualTo(2));
+ }
- // Outdated comment is not included in the count.
- file.InlineCommentThreads.Returns(new[] { thread1, thread2, outdatedThread });
- RaisePropertyChanged(file, nameof(file.InlineCommentThreads));
- Assert.That(2, Is.EqualTo(((IPullRequestFileNode)target.ChangedFilesTree[0]).CommentCount));
+ [Test]
+ public async Task ShouldNotShowPendingReviewForOtherUser()
+ {
+ var target = CreateTarget();
+ var model = CreatePullRequestModel(
+ CreatePullRequestReviewModel(1, "shana", PullRequestReviewState.Pending));
+
+ await target.Load(model);
- file.Received(1).PropertyChanged += Arg.Any();
+ Assert.That(target.Reviews, Has.Count.EqualTo(1));
+ Assert.That(target.Reviews[0].User.Login, Is.EqualTo("grokys"));
+ Assert.That(target.Reviews[0].Id, Is.EqualTo(0));
}
- IInlineCommentThreadModel CreateThread(int lineNumber)
+ static PullRequestModel CreatePullRequestModel(
+ params IPullRequestReviewModel[] reviews)
{
- var result = Substitute.For();
- result.LineNumber.Returns(lineNumber);
- return result;
+ return PullRequestDetailViewModelTests.CreatePullRequestModel(reviews: reviews);
}
- void RaisePropertyChanged(T o, string propertyName)
- where T : INotifyPropertyChanged
+ static PullRequestReviewModel CreatePullRequestReviewModel(
+ long id,
+ string login,
+ PullRequestReviewState state)
{
- o.PropertyChanged += Raise.Event(new PropertyChangedEventArgs(propertyName));
- }
+ var account = Substitute.For();
+ account.Login.Returns(login);
+ return new PullRequestReviewModel
+ {
+ Id = id,
+ User = account,
+ State = state,
+ };
+ }
}
- public class TheCheckoutCommand
+ public class TheCheckoutCommand : TestBaseClass
{
[Test]
public async Task CheckedOutAndUpToDate()
@@ -154,7 +165,7 @@ public async Task CheckedOutAndUpToDate()
currentBranch: "pr/123",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.False(target.Checkout.CanExecute(null));
Assert.That(target.CheckoutState, Is.Null);
@@ -167,7 +178,7 @@ public async Task NotCheckedOut()
currentBranch: "master",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.True(target.Checkout.CanExecute(null));
Assert.True(target.CheckoutState.IsEnabled);
@@ -182,7 +193,7 @@ public async Task NotCheckedOutWithWorkingDirectoryDirty()
existingPrBranch: "pr/123",
dirty: true);
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.False(target.Checkout.CanExecute(null));
Assert.That("Cannot checkout as your working directory has uncommitted changes.", Is.EqualTo(target.CheckoutState.ToolTip));
@@ -195,7 +206,7 @@ public async Task CheckoutExistingLocalBranch()
currentBranch: "master",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest(number: 123));
+ await target.Load(CreatePullRequestModel(number: 123));
Assert.True(target.Checkout.CanExecute(null));
Assert.That("Checkout pr/123", Is.EqualTo(target.CheckoutState.Caption));
@@ -207,7 +218,7 @@ public async Task CheckoutNonExistingLocalBranch()
var target = CreateTarget(
currentBranch: "master");
- await target.Load(CreatePullRequest(number: 123));
+ await target.Load(CreatePullRequestModel(number: 123));
Assert.True(target.Checkout.CanExecute(null));
Assert.That("Checkout to pr/123", Is.EqualTo(target.CheckoutState.Caption));
@@ -219,7 +230,7 @@ public async Task UpdatesOperationErrorWithExceptionMessage()
var target = CreateTarget(
currentBranch: "master",
existingPrBranch: "pr/123");
- var pr = CreatePullRequest();
+ var pr = CreatePullRequestModel();
pr.Head = new GitReferenceModel("source", null, "sha", (string)null);
@@ -236,7 +247,7 @@ public async Task SetsOperationErrorOnCheckoutFailure()
currentBranch: "master",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.True(target.Checkout.CanExecute(null));
@@ -252,7 +263,7 @@ public async Task ClearsOperationErrorOnCheckoutSuccess()
currentBranch: "master",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.True(target.Checkout.CanExecute(null));
Assert.ThrowsAsync(async () => await target.Checkout.ExecuteAsyncTask());
@@ -269,7 +280,7 @@ public async Task ClearsOperationErrorOnCheckoutRefresh()
currentBranch: "master",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.True(target.Checkout.CanExecute(null));
Assert.ThrowsAsync(async () => await target.Checkout.ExecuteAsyncTask());
@@ -280,7 +291,7 @@ public async Task ClearsOperationErrorOnCheckoutRefresh()
}
}
- public class ThePullCommand
+ public class ThePullCommand : TestBaseClass
{
[Test]
public async Task NotCheckedOut()
@@ -289,7 +300,7 @@ public async Task NotCheckedOut()
currentBranch: "master",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.False(target.Pull.CanExecute(null));
Assert.That(target.UpdateState, Is.Null);
@@ -302,7 +313,7 @@ public async Task CheckedOutAndUpToDate()
currentBranch: "pr/123",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.False(target.Pull.CanExecute(null));
Assert.That(0, Is.EqualTo(target.UpdateState.CommitsAhead));
@@ -318,7 +329,7 @@ public async Task CheckedOutAndBehind()
existingPrBranch: "pr/123",
behindBy: 2);
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.True(target.Pull.CanExecute(null));
Assert.That(0, Is.EqualTo(target.UpdateState.CommitsAhead));
@@ -335,7 +346,7 @@ public async Task CheckedOutAndAheadAndBehind()
aheadBy: 3,
behindBy: 2);
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.True(target.Pull.CanExecute(null));
Assert.That(3, Is.EqualTo(target.UpdateState.CommitsAhead));
@@ -352,7 +363,7 @@ public async Task CheckedOutAndBehindFork()
prFromFork: true,
behindBy: 2);
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.True(target.Pull.CanExecute(null));
Assert.That(0, Is.EqualTo(target.UpdateState.CommitsAhead));
@@ -367,14 +378,14 @@ public async Task UpdatesOperationErrorWithExceptionMessage()
currentBranch: "master",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.ThrowsAsync(() => target.Pull.ExecuteAsyncTask(null));
Assert.That("Pull threw", Is.EqualTo(target.OperationError));
}
}
- public class ThePushCommand
+ public class ThePushCommand : TestBaseClass
{
[Test]
public async Task NotCheckedOut()
@@ -383,7 +394,7 @@ public async Task NotCheckedOut()
currentBranch: "master",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.False(target.Push.CanExecute(null));
Assert.That(target.UpdateState, Is.Null);
@@ -396,7 +407,7 @@ public async Task CheckedOutAndUpToDate()
currentBranch: "pr/123",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.False(target.Push.CanExecute(null));
Assert.That(0, Is.EqualTo(target.UpdateState.CommitsAhead));
@@ -412,7 +423,7 @@ public async Task CheckedOutAndAhead()
existingPrBranch: "pr/123",
aheadBy: 2);
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.True(target.Push.CanExecute(null));
Assert.That(2, Is.EqualTo(target.UpdateState.CommitsAhead));
@@ -428,7 +439,7 @@ public async Task CheckedOutAndBehind()
existingPrBranch: "pr/123",
behindBy: 2);
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.False(target.Push.CanExecute(null));
Assert.That(0, Is.EqualTo(target.UpdateState.CommitsAhead));
@@ -445,7 +456,7 @@ public async Task CheckedOutAndAheadAndBehind()
aheadBy: 3,
behindBy: 2);
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.False(target.Push.CanExecute(null));
Assert.That(3, Is.EqualTo(target.UpdateState.CommitsAhead));
@@ -462,7 +473,7 @@ public async Task CheckedOutAndAheadOfFork()
prFromFork: true,
aheadBy: 2);
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.True(target.Push.CanExecute(null));
Assert.That(2, Is.EqualTo(target.UpdateState.CommitsAhead));
@@ -477,7 +488,7 @@ public async Task UpdatesOperationErrorWithExceptionMessage()
currentBranch: "master",
existingPrBranch: "pr/123");
- await target.Load(CreatePullRequest());
+ await target.Load(CreatePullRequestModel());
Assert.ThrowsAsync(() => target.Push.ExecuteAsyncTask(null));
Assert.That("Push threw", Is.EqualTo(target.OperationError));
@@ -550,28 +561,45 @@ static Tuple CreateTargetAndSer
pullRequestService.CalculateHistoryDivergence(repository, Arg.Any())
.Returns(Observable.Return(divergence));
+ if (sessionManager == null)
+ {
+ var currentSession = Substitute.For();
+ currentSession.User.Login.Returns("grokys");
+
+ sessionManager = Substitute.For();
+ sessionManager.CurrentSession.Returns(currentSession);
+ sessionManager.GetSession(null).ReturnsForAnyArgs(currentSession);
+ }
+
var vm = new PullRequestDetailViewModel(
pullRequestService,
- sessionManager ?? Substitute.For(),
+ sessionManager,
Substitute.For(),
Substitute.For(),
Substitute.For(),
- Substitute.For());
+ Substitute.For(),
+ Substitute.For());
vm.InitializeAsync(repository, Substitute.For(), "owner", "repo", 1).Wait();
return Tuple.Create(vm, pullRequestService);
}
- static PullRequestModel CreatePullRequest(int number = 1, string body = "PR Body")
+ static PullRequestModel CreatePullRequestModel(
+ int number = 1,
+ string body = "PR Body",
+ IEnumerable reviews = null)
{
var author = Substitute.For();
+ reviews = reviews ?? new IPullRequestReviewModel[0];
+
return new PullRequestModel(number, "PR 1", author, DateTimeOffset.Now)
{
State = PullRequestStateEnum.Open,
Body = string.Empty,
Head = new GitReferenceModel("source", "foo:baz", "sha", "https://github.com/foo/bar.git"),
Base = new GitReferenceModel("dest", "foo:bar", "sha", "https://github.com/foo/bar.git"),
+ Reviews = reviews.ToList(),
};
}
diff --git a/test/UnitTests/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModelTests.cs b/test/UnitTests/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModelTests.cs
new file mode 100644
index 0000000000..67a6cf87e6
--- /dev/null
+++ b/test/UnitTests/GitHub.App/ViewModels/GitHubPane/PullRequestFilesViewModelTests.cs
@@ -0,0 +1,128 @@
+using System;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using GitHub.Models;
+using GitHub.Services;
+using GitHub.ViewModels.GitHubPane;
+using NSubstitute;
+using NUnit.Framework;
+
+namespace UnitTests.GitHub.App.ViewModels.GitHubPane
+{
+ public class PullRequestFilesViewModelTests
+ {
+ static readonly Uri Uri = new Uri("http://foo");
+
+ [Test]
+ public async Task ShouldCreateChangesTree()
+ {
+ var target = CreateTarget();
+ var session = CreateSession();
+
+ session.PullRequest.ChangedFiles.Returns(new[]
+ {
+ new PullRequestFileModel("readme.md", "abc", PullRequestFileStatus.Modified),
+ new PullRequestFileModel("dir1/f1.cs", "abc", PullRequestFileStatus.Modified),
+ new PullRequestFileModel("dir1/f2.cs", "abc", PullRequestFileStatus.Modified),
+ new PullRequestFileModel("dir1/dir1a/f3.cs", "abc", PullRequestFileStatus.Modified),
+ new PullRequestFileModel("dir2/f4.cs", "abc", PullRequestFileStatus.Modified),
+ });
+
+ await target.InitializeAsync(session);
+
+ Assert.That(target.Items.Count, Is.EqualTo(3));
+
+ var dir1 = (PullRequestDirectoryNode)target.Items[0];
+ Assert.That(dir1.DirectoryName, Is.EqualTo("dir1"));
+ Assert.That(dir1.Files, Has.Exactly(2).Items);
+
+ Assert.That(dir1.Directories, Has.One.Items);
+ Assert.That(dir1.Files[0].FileName, Is.EqualTo("f1.cs"));
+ Assert.That(dir1.Files[1].FileName, Is.EqualTo("f2.cs"));
+ Assert.That(dir1.Files[0].RelativePath, Is.EqualTo("dir1\\f1.cs"));
+ Assert.That(dir1.Files[1].RelativePath, Is.EqualTo("dir1\\f2.cs"));
+
+ var dir1a = (PullRequestDirectoryNode)dir1.Directories[0];
+ Assert.That(dir1a.DirectoryName, Is.EqualTo("dir1a"));
+ Assert.That(dir1a.Files, Has.One.Items);
+ Assert.That(dir1a.Directories, Is.Empty);
+
+ var dir2 = (PullRequestDirectoryNode)target.Items[1];
+ Assert.That(dir2.DirectoryName, Is.EqualTo("dir2"));
+ Assert.That(dir2.Files, Has.One.Items);
+ Assert.That(dir2.Directories, Is.Empty);
+
+ var readme = (PullRequestFileNode)target.Items[2];
+ Assert.That(readme.FileName, Is.EqualTo("readme.md"));
+ }
+
+ [Test]
+ public async Task FileCommentCountShouldTrackSessionInlineComments()
+ {
+ var outdatedThread = CreateThread(-1);
+ var session = CreateSession();
+
+ session.PullRequest.ChangedFiles.Returns(new[]
+ {
+ new PullRequestFileModel("readme.md", "abc", PullRequestFileStatus.Modified),
+ });
+
+ var file = Substitute.For();
+ var thread1 = CreateThread(5);
+ var thread2 = CreateThread(6);
+ file.InlineCommentThreads.Returns(new[] { thread1 });
+ session.GetFile("readme.md").Returns(Task.FromResult(file));
+
+ var target = CreateTarget();
+
+ await target.InitializeAsync(session);
+ Assert.That(((IPullRequestFileNode)target.Items[0]).CommentCount, Is.EqualTo(1));
+
+ file.InlineCommentThreads.Returns(new[] { thread1, thread2 });
+ RaisePropertyChanged(file, nameof(file.InlineCommentThreads));
+ Assert.That(((IPullRequestFileNode)target.Items[0]).CommentCount, Is.EqualTo(2));
+
+ // Outdated comment is not included in the count.
+ file.InlineCommentThreads.Returns(new[] { thread1, thread2, outdatedThread });
+ RaisePropertyChanged(file, nameof(file.InlineCommentThreads));
+ Assert.That(((IPullRequestFileNode)target.Items[0]).CommentCount, Is.EqualTo(2));
+
+ file.Received(1).PropertyChanged += Arg.Any();
+ }
+
+ static PullRequestFilesViewModel CreateTarget()
+ {
+ var pullRequestService = Substitute.For();
+ var editorService = Substitute.For();
+ return new PullRequestFilesViewModel(pullRequestService, editorService);
+ }
+
+ static IPullRequestSession CreateSession()
+ {
+ var author = Substitute.For();
+ var pr = Substitute.For();
+
+ var repository = Substitute.For();
+ repository.LocalPath.Returns(@"C:\Foo");
+
+ var result = Substitute.For();
+ result.LocalRepository.Returns(repository);
+ result.PullRequest.Returns(pr);
+ return result;
+ }
+
+ IInlineCommentThreadModel CreateThread(int lineNumber)
+ {
+ var result = Substitute.For