Merge pull request #1798 from github/features/check-suites

Functionality to support Check Suites API
This commit is contained in:
Stanley Goldman 2018-08-21 13:20:45 -04:00 коммит произвёл GitHub
Родитель f7424c8a9e caaf122976
Коммит 23e48d1395
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
32 изменённых файлов: 624 добавлений и 159 удалений

Двоичные данные
lib/Octokit.GraphQL.0.1.1-beta.nupkg Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -50,11 +50,11 @@
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="Octokit.GraphQL, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
<Reference Include="Octokit.GraphQL, Version=0.1.1.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
</Reference>
<Reference Include="Octokit.GraphQL.Core, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
<Reference Include="Octokit.GraphQL.Core, Version=0.1.1.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
</Reference>
<Reference Include="Serilog, Version=2.0.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll</HintPath>

Просмотреть файл

@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.Primitives;
@ -23,7 +24,7 @@ namespace GitHub.Api
this.address = address;
}
public async Task<string> GetCredentials()
public async Task<string> GetCredentials(CancellationToken cancellationToken = default)
{
var userPass = await keychain.Load(address).ConfigureAwait(false);
return userPass?.Item2;

Просмотреть файл

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" />
<package id="Octokit.GraphQL" version="0.1.0-beta" targetFramework="net461" />
<package id="Octokit.GraphQL" version="0.1.1-beta" targetFramework="net461" />
<package id="Serilog" version="2.5.0" targetFramework="net461" />
</packages>

Просмотреть файл

@ -144,11 +144,11 @@
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="Octokit.GraphQL, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
<Reference Include="Octokit.GraphQL, Version=0.1.1.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
</Reference>
<Reference Include="Octokit.GraphQL.Core, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
<Reference Include="Octokit.GraphQL.Core, Version=0.1.1.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
@ -229,6 +229,7 @@
<Compile Include="SampleData\PullRequestUserReviewsViewModelDesigner.cs" />
<Compile Include="SampleData\UserFilterViewModelDesigner.cs" />
<Compile Include="Services\EnterpriseCapabilitiesService.cs" />
<Compile Include="Services\FromGraphQlExtensions.cs" />
<Compile Include="Services\GitHubContextService.cs" />
<Compile Include="Services\GlobalConnection.cs" />
<Compile Include="Services\RepositoryForkService.cs" />
@ -259,6 +260,7 @@
<Compile Include="ViewModels\GitHubPane\NavigationViewModel.cs" />
<Compile Include="ViewModels\GitHubPane\GitHubPaneViewModel.cs" />
<Compile Include="SampleData\PullRequestCheckViewModelDesigner.cs" />
<Compile Include="ViewModels\GitHubPane\PullRequestCheckType.cs" />
<Compile Include="ViewModels\GitHubPane\PullRequestFilesViewModel.cs" />
<Compile Include="ViewModels\GitHubPane\PullRequestListItemViewModel.cs" />
<Compile Include="ViewModels\GitHubPane\PullRequestListViewModel.cs" />
@ -416,8 +418,8 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources.en-US.resx">
<DependentUpon>Resources.resx</DependentUpon>
</EmbeddedResource>
<DependentUpon>Resources.resx</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>

Просмотреть файл

@ -16,10 +16,6 @@ namespace GitHub.SampleData
public Uri DetailsUrl { get; set; } = new Uri("http://github.com");
public string AvatarUrl { get; set; } = "https://avatars1.githubusercontent.com/u/417571?s=88&v=4";
public BitmapImage Avatar { get; set; } = null;
public ReactiveCommand<object> OpenDetailsUrl { get; set; } = null;
}
}

Просмотреть файл

@ -0,0 +1,105 @@
using System;
using GitHub.Models;
using Octokit.GraphQL.Model;
using CheckConclusionState = GitHub.Models.CheckConclusionState;
using CheckStatusState = GitHub.Models.CheckStatusState;
using PullRequestReviewState = GitHub.Models.PullRequestReviewState;
using StatusState = GitHub.Models.StatusState;
namespace GitHub.Services
{
public static class FromGraphQlExtensions
{
public static CheckConclusionState? FromGraphQl(this Octokit.GraphQL.Model.CheckConclusionState? value)
{
switch (value)
{
case null:
return null;
case Octokit.GraphQL.Model.CheckConclusionState.ActionRequired:
return CheckConclusionState.ActionRequired;
case Octokit.GraphQL.Model.CheckConclusionState.TimedOut:
return CheckConclusionState.TimedOut;
case Octokit.GraphQL.Model.CheckConclusionState.Cancelled:
return CheckConclusionState.Cancelled;
case Octokit.GraphQL.Model.CheckConclusionState.Failure:
return CheckConclusionState.Failure;
case Octokit.GraphQL.Model.CheckConclusionState.Success:
return CheckConclusionState.Success;
case Octokit.GraphQL.Model.CheckConclusionState.Neutral:
return CheckConclusionState.Neutral;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
public static PullRequestStateEnum FromGraphQl(this PullRequestState value)
{
switch (value)
{
case PullRequestState.Open:
return PullRequestStateEnum.Open;
case PullRequestState.Closed:
return PullRequestStateEnum.Closed;
case PullRequestState.Merged:
return PullRequestStateEnum.Merged;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
public static StatusState FromGraphQl(this Octokit.GraphQL.Model.StatusState value)
{
switch (value)
{
case Octokit.GraphQL.Model.StatusState.Expected:
return StatusState.Expected;
case Octokit.GraphQL.Model.StatusState.Error:
return StatusState.Error;
case Octokit.GraphQL.Model.StatusState.Failure:
return StatusState.Failure;
case Octokit.GraphQL.Model.StatusState.Pending:
return StatusState.Pending;
case Octokit.GraphQL.Model.StatusState.Success:
return StatusState.Success;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
public static CheckStatusState FromGraphQl(this Octokit.GraphQL.Model.CheckStatusState value)
{
switch (value)
{
case Octokit.GraphQL.Model.CheckStatusState.Queued:
return CheckStatusState.Queued;
case Octokit.GraphQL.Model.CheckStatusState.InProgress:
return CheckStatusState.InProgress;
case Octokit.GraphQL.Model.CheckStatusState.Completed:
return CheckStatusState.Completed;
case Octokit.GraphQL.Model.CheckStatusState.Requested:
return CheckStatusState.Requested;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
public static GitHub.Models.PullRequestReviewState FromGraphQl(this Octokit.GraphQL.Model.PullRequestReviewState value)
{
switch (value) {
case Octokit.GraphQL.Model.PullRequestReviewState.Pending:
return PullRequestReviewState.Pending;
case Octokit.GraphQL.Model.PullRequestReviewState.Commented:
return PullRequestReviewState.Commented;
case Octokit.GraphQL.Model.PullRequestReviewState.Approved:
return PullRequestReviewState.Approved;
case Octokit.GraphQL.Model.PullRequestReviewState.ChangesRequested:
return PullRequestReviewState.ChangesRequested;
case Octokit.GraphQL.Model.PullRequestReviewState.Dismissed:
return PullRequestReviewState.Dismissed;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
}

Просмотреть файл

@ -13,6 +13,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
using GitHub.Api;
using GitHub.App.Services;
using GitHub.Extensions;
using GitHub.Logging;
using GitHub.Models;
@ -23,6 +24,8 @@ using Octokit.GraphQL.Model;
using Rothko;
using static System.FormattableString;
using static Octokit.GraphQL.Variable;
using CheckConclusionState = GitHub.Models.CheckConclusionState;
using CheckStatusState = GitHub.Models.CheckStatusState;
using StatusState = GitHub.Models.StatusState;
namespace GitHub.Services
@ -38,6 +41,7 @@ namespace GitHub.Services
static readonly Regex BranchCapture = new Regex(@"branch\.(?<branch>.+)\.ghfvs-pr", RegexOptions.ECMAScript);
static ICompiledQuery<Page<ActorModel>> readAssignableUsers;
static ICompiledQuery<Page<PullRequestListItemModel>> readPullRequests;
static ICompiledQuery<Page<PullRequestListItemModel>> readPullRequestsEnterprise;
static readonly string[] TemplatePaths = new[]
{
@ -78,51 +82,120 @@ namespace GitHub.Services
string after,
PullRequestStateEnum[] states)
{
if (readPullRequests == null)
ICompiledQuery<Page<PullRequestListItemModel>> query;
if (address.IsGitHubDotCom())
{
readPullRequests = new Query()
.Repository(Var(nameof(owner)), Var(nameof(name)))
.PullRequests(
first: 100,
after: Var(nameof(after)),
orderBy: new IssueOrder { Direction = OrderDirection.Desc, Field = IssueOrderField.CreatedAt },
states: Var(nameof(states)))
.Select(page => new Page<PullRequestListItemModel>
{
EndCursor = page.PageInfo.EndCursor,
HasNextPage = page.PageInfo.HasNextPage,
TotalCount = page.TotalCount,
Items = page.Nodes.Select(pr => new ListItemAdapter
{
Id = pr.Id.Value,
LastCommit = pr.Commits(null, null, 1, null).Nodes.Select(commit =>
new LastCommitSummaryModel
{
Statuses = commit.Commit.Status
.Select(context =>
context.Contexts.Select(statusContext => new StatusSummaryModel
{
State = (StatusState)statusContext.State,
}).ToList()
).SingleOrDefault()
}).ToList().FirstOrDefault(),
Author = new ActorModel
{
Login = pr.Author.Login,
AvatarUrl = pr.Author.AvatarUrl(null),
},
CommentCount = pr.Comments(0, null, null, null).TotalCount,
Number = pr.Number,
Reviews = pr.Reviews(null, null, null, null, null, null).AllPages().Select(review => new ReviewAdapter
{
Body = review.Body,
CommentCount = review.Comments(null, null, null, null).TotalCount,
}).ToList(),
State = (PullRequestStateEnum)pr.State,
Title = pr.Title,
UpdatedAt = pr.UpdatedAt,
}).ToList(),
}).Compile();
if (readPullRequests == null)
{
readPullRequests = new Query()
.Repository(Var(nameof(owner)), Var(nameof(name)))
.PullRequests(
first: 100,
after: Var(nameof(after)),
orderBy: new IssueOrder { Direction = OrderDirection.Desc, Field = IssueOrderField.CreatedAt },
states: Var(nameof(states)))
.Select(page => new Page<PullRequestListItemModel>
{
EndCursor = page.PageInfo.EndCursor,
HasNextPage = page.PageInfo.HasNextPage,
TotalCount = page.TotalCount,
Items = page.Nodes.Select(pr => new ListItemAdapter
{
Id = pr.Id.Value,
LastCommit = pr.Commits(null, null, 1, null).Nodes.Select(commit =>
new LastCommitSummaryAdapter
{
CheckSuites = commit.Commit.CheckSuites(null, null, null, null, null).AllPages(10)
.Select(suite => new CheckSuiteSummaryModel
{
CheckRuns = suite.CheckRuns(null, null, null, null, null).AllPages(10)
.Select(run => new CheckRunSummaryModel
{
Conclusion = run.Conclusion.FromGraphQl(),
Status = run.Status.FromGraphQl()
}).ToList()
}).ToList(),
Statuses = commit.Commit.Status
.Select(context =>
context.Contexts.Select(statusContext => new StatusSummaryModel
{
State = statusContext.State.FromGraphQl(),
}).ToList()
).SingleOrDefault()
}).ToList().FirstOrDefault(),
Author = new ActorModel
{
Login = pr.Author.Login,
AvatarUrl = pr.Author.AvatarUrl(null),
},
CommentCount = pr.Comments(0, null, null, null).TotalCount,
Number = pr.Number,
Reviews = pr.Reviews(null, null, null, null, null, null).AllPages().Select(review => new ReviewAdapter
{
Body = review.Body,
CommentCount = review.Comments(null, null, null, null).TotalCount,
}).ToList(),
State = pr.State.FromGraphQl(),
Title = pr.Title,
UpdatedAt = pr.UpdatedAt,
}).ToList(),
}).Compile();
}
query = readPullRequests;
}
else
{
if (readPullRequestsEnterprise == null)
{
readPullRequestsEnterprise = new Query()
.Repository(Var(nameof(owner)), Var(nameof(name)))
.PullRequests(
first: 100,
after: Var(nameof(after)),
orderBy: new IssueOrder { Direction = OrderDirection.Desc, Field = IssueOrderField.CreatedAt },
states: Var(nameof(states)))
.Select(page => new Page<PullRequestListItemModel>
{
EndCursor = page.PageInfo.EndCursor,
HasNextPage = page.PageInfo.HasNextPage,
TotalCount = page.TotalCount,
Items = page.Nodes.Select(pr => new ListItemAdapter
{
Id = pr.Id.Value,
LastCommit = pr.Commits(null, null, 1, null).Nodes.Select(commit =>
new LastCommitSummaryAdapter
{
Statuses = commit.Commit.Status
.Select(context =>
context.Contexts.Select(statusContext => new StatusSummaryModel
{
State = statusContext.State.FromGraphQl(),
}).ToList()
).SingleOrDefault()
}).ToList().FirstOrDefault(),
Author = new ActorModel
{
Login = pr.Author.Login,
AvatarUrl = pr.Author.AvatarUrl(null),
},
CommentCount = pr.Comments(0, null, null, null).TotalCount,
Number = pr.Number,
Reviews = pr.Reviews(null, null, null, null, null, null).AllPages().Select(review => new ReviewAdapter
{
Body = review.Body,
CommentCount = review.Comments(null, null, null, null).TotalCount,
}).ToList(),
State = pr.State.FromGraphQl(),
Title = pr.Title,
UpdatedAt = pr.UpdatedAt,
}).ToList(),
}).Compile();
}
query = readPullRequestsEnterprise;
}
var graphql = await graphqlFactory.CreateConnection(address);
@ -134,38 +207,65 @@ namespace GitHub.Services
{ nameof(states), states.Select(x => (PullRequestState)x).ToList() },
};
var result = await graphql.Run(readPullRequests, vars);
var result = await graphql.Run(query, vars);
foreach (var item in result.Items.Cast<ListItemAdapter>())
{
item.CommentCount += item.Reviews.Sum(x => x.Count);
item.Reviews = null;
var hasStatuses = item.LastCommit.Statuses != null
&& item.LastCommit.Statuses.Any();
var checkRuns = item.LastCommit?.CheckSuites?.SelectMany(model => model.CheckRuns).ToArray();
if (!hasStatuses)
var hasCheckRuns = checkRuns?.Any() ?? false;
var hasStatuses = item.LastCommit?.Statuses?.Any() ?? false;
if (!hasCheckRuns && !hasStatuses)
{
item.Checks = PullRequestChecksState.None;
}
else
{
var statusHasFailure = item.LastCommit
.Statuses
.Any(status => status.State == StatusState.Failure);
var checksHasFailure = false;
var checksHasCompleteSuccess = true;
var statusHasCompleteSuccess = true;
if (!statusHasFailure)
if (hasCheckRuns)
{
statusHasCompleteSuccess =
item.LastCommit.Statuses.All(status => status.State == StatusState.Success);
checksHasFailure = checkRuns
.Any(model => model.Conclusion.HasValue
&& (model.Conclusion.Value == CheckConclusionState.Failure
|| model.Conclusion.Value == CheckConclusionState.ActionRequired));
if (!checksHasFailure)
{
checksHasCompleteSuccess = checkRuns
.All(model => model.Conclusion.HasValue
&& (model.Conclusion.Value == CheckConclusionState.Success
|| model.Conclusion.Value == CheckConclusionState.Neutral));
}
}
if (statusHasFailure)
var statusHasFailure = false;
var statusHasCompleteSuccess = true;
if (!checksHasFailure && hasStatuses)
{
statusHasFailure = item.LastCommit
.Statuses
.Any(status => status.State == StatusState.Failure
|| status.State == StatusState.Error);
if (!statusHasFailure)
{
statusHasCompleteSuccess =
item.LastCommit.Statuses.All(status => status.State == StatusState.Success);
}
}
if (checksHasFailure || statusHasFailure)
{
item.Checks = PullRequestChecksState.Failure;
}
else if (statusHasCompleteSuccess)
else if (statusHasCompleteSuccess && checksHasCompleteSuccess)
{
item.Checks = PullRequestChecksState.Success;
}
@ -900,7 +1000,7 @@ namespace GitHub.Services
{
public IList<ReviewAdapter> Reviews { get; set; }
public LastCommitSummaryModel LastCommit { get; set; }
public LastCommitSummaryAdapter LastCommit { get; set; }
}
class ReviewAdapter
@ -910,14 +1010,27 @@ namespace GitHub.Services
public int Count => CommentCount + (!string.IsNullOrWhiteSpace(Body) ? 1 : 0);
}
class LastCommitSummaryAdapter
{
public List<CheckSuiteSummaryModel> CheckSuites { get; set; }
public List<StatusSummaryModel> Statuses { get; set; }
}
class CheckSuiteSummaryModel
{
public List<CheckRunSummaryModel> CheckRuns { get; set; }
}
class CheckRunSummaryModel
{
public CheckConclusionState? Conclusion { get; set; }
public CheckStatusState Status { get; set; }
}
class StatusSummaryModel
{
public StatusState State { get; set; }
}
class LastCommitSummaryModel
{
public List<StatusSummaryModel> Statuses { get; set; }
}
}
}

Просмотреть файл

@ -0,0 +1,8 @@
namespace GitHub.ViewModels.GitHubPane
{
public enum PullRequestCheckType
{
StatusApi,
ChecksApi
}
}

Просмотреть файл

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Linq.Expressions;
using System.Reactive;
using System.Reactive.Linq;
using System.Windows.Media.Imaging;
@ -22,7 +23,7 @@ namespace GitHub.ViewModels.GitHubPane
public static IEnumerable<IPullRequestCheckViewModel> Build(IViewViewModelFactory viewViewModelFactory, PullRequestDetailModel pullRequest)
{
return pullRequest.Statuses?.Select(model =>
var statuses = pullRequest.Statuses?.Select(model =>
{
PullRequestCheckStatus checkStatus;
switch (model.State)
@ -43,18 +44,62 @@ namespace GitHub.ViewModels.GitHubPane
}
var pullRequestCheckViewModel = (PullRequestCheckViewModel) viewViewModelFactory.CreateViewModel<IPullRequestCheckViewModel>();
pullRequestCheckViewModel.CheckType = PullRequestCheckType.StatusApi;
pullRequestCheckViewModel.Title = model.Context;
pullRequestCheckViewModel.Description = model.Description;
pullRequestCheckViewModel.Status = checkStatus;
pullRequestCheckViewModel.DetailsUrl = new Uri(model.TargetUrl);
pullRequestCheckViewModel.AvatarUrl = model.AvatarUrl ?? DefaultAvatar;
pullRequestCheckViewModel.Avatar = model.AvatarUrl != null
? new BitmapImage(new Uri(model.AvatarUrl))
: AvatarProvider.CreateBitmapImage(DefaultAvatar);
pullRequestCheckViewModel.DetailsUrl = !string.IsNullOrEmpty(model.TargetUrl) ? new Uri(model.TargetUrl) : null;
return pullRequestCheckViewModel;
}) ?? new PullRequestCheckViewModel[0];
var checks = pullRequest.CheckSuites?.SelectMany(model => model.CheckRuns)
.Select(model =>
{
PullRequestCheckStatus checkStatus;
switch (model.Status)
{
case CheckStatusState.Requested:
case CheckStatusState.Queued:
case CheckStatusState.InProgress:
checkStatus = PullRequestCheckStatus.Pending;
break;
case CheckStatusState.Completed:
switch (model.Conclusion)
{
case CheckConclusionState.Success:
checkStatus = PullRequestCheckStatus.Success;
break;
case CheckConclusionState.ActionRequired:
case CheckConclusionState.TimedOut:
case CheckConclusionState.Cancelled:
case CheckConclusionState.Failure:
case CheckConclusionState.Neutral:
checkStatus = PullRequestCheckStatus.Failure;
break;
default:
throw new ArgumentOutOfRangeException();
}
break;
default:
throw new ArgumentOutOfRangeException();
}
var pullRequestCheckViewModel = (PullRequestCheckViewModel)viewViewModelFactory.CreateViewModel<IPullRequestCheckViewModel>();
pullRequestCheckViewModel.CheckType = PullRequestCheckType.ChecksApi;
pullRequestCheckViewModel.Title = model.Name;
pullRequestCheckViewModel.Description = model.Summary;
pullRequestCheckViewModel.Status = checkStatus;
pullRequestCheckViewModel.DetailsUrl = new Uri(model.DetailsUrl);
return pullRequestCheckViewModel;
}) ?? new PullRequestCheckViewModel[0];
return statuses.Concat(checks).OrderBy(model => model.Title);
}
[ImportingConstructor]
@ -66,21 +111,29 @@ namespace GitHub.ViewModels.GitHubPane
private void DoOpenDetailsUrl(object obj)
{
usageTracker.IncrementCounter(x => x.NumberOfPRCheckStatusesOpenInGitHub).Forget();
Expression<Func<UsageModel.MeasuresModel, int>> expression;
if (CheckType == PullRequestCheckType.StatusApi)
{
expression = x => x.NumberOfPRStatusesOpenInGitHub;
}
else
{
expression = x => x.NumberOfPRChecksOpenInGitHub;
}
usageTracker.IncrementCounter(expression).Forget();
}
public string Title { get; private set; }
public string Description { get; private set; }
public PullRequestCheckType CheckType { get; private set; }
public PullRequestCheckStatus Status{ get; private set; }
public Uri DetailsUrl { get; private set; }
public string AvatarUrl { get; private set; }
public BitmapImage Avatar { get; private set; }
public ReactiveCommand<object> OpenDetailsUrl { get; }
}
}

Просмотреть файл

@ -126,10 +126,16 @@ namespace GitHub.ViewModels.GitHubPane
SyncSubmodules.Subscribe(_ => Refresh().ToObservable());
SubscribeOperationError(SyncSubmodules);
OpenOnGitHub = ReactiveCommand.Create();
OpenOnGitHub = ReactiveCommand.Create().OnExecuteCompleted(DoOpenDetailsUrl);
ShowReview = ReactiveCommand.Create().OnExecuteCompleted(DoShowReview);
}
private void DoOpenDetailsUrl(object obj)
{
usageTracker.IncrementCounter(measuresModel => measuresModel.NumberOfPRDetailsOpenInGitHub).Forget();
}
/// <summary>
/// Gets the underlying pull request model.
/// </summary>

Просмотреть файл

@ -21,7 +21,7 @@
<package id="Microsoft.VisualStudio.TextManager.Interop.8.0" version="8.0.50728" targetFramework="net461" />
<package id="Microsoft.VisualStudio.Utilities" version="14.3.25407" targetFramework="net461" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" />
<package id="Octokit.GraphQL" version="0.1.0-beta" targetFramework="net461" />
<package id="Octokit.GraphQL" version="0.1.1-beta" targetFramework="net461" />
<package id="Rothko" version="0.0.3-ghfvs" targetFramework="net461" />
<package id="Rx-Core" version="2.2.5-custom" targetFramework="net45" />
<package id="Rx-Interfaces" version="2.2.5-custom" targetFramework="net45" />

Просмотреть файл

@ -30,16 +30,6 @@ namespace GitHub.ViewModels.GitHubPane
/// </summary>
Uri DetailsUrl { get; }
/// <summary>
/// The AvatarUrl of the Status/Check application
/// </summary>
string AvatarUrl { get; }
/// <summary>
/// The BitmapImage of the AvatarUrl
/// </summary>
BitmapImage Avatar { get; }
/// <summary>
/// A command that opens the DetailsUrl in a browser
/// </summary>

Просмотреть файл

@ -171,6 +171,12 @@
<Compile Include="Extensions\ConnectionManagerExtensions.cs" />
<Compile Include="GitHubLogicException.cs" />
<Compile Include="Models\ActorModel.cs" />
<Compile Include="Models\AnnotationModel.cs" />
<Compile Include="Models\CheckAnnotationLevel.cs" />
<Compile Include="Models\CheckConclusionState.cs" />
<Compile Include="Models\CheckRunModel.cs" />
<Compile Include="Models\CheckStatusState.cs" />
<Compile Include="Models\CheckSuiteModel.cs" />
<Compile Include="Models\CommitMessage.cs" />
<Compile Include="Models\DiffChangeType.cs" />
<Compile Include="Models\DiffChunk.cs" />

Просмотреть файл

@ -0,0 +1,48 @@
namespace GitHub.Models
{
/// <summary>
/// Model for a single check annotation.
/// </summary>
public class CheckRunAnnotationModel
{
/// <summary>
/// The path to the file that this annotation was made on.
/// </summary>
public string BlobUrl { get; set; }
/// <summary>
/// The starting line number (1 indexed).
/// </summary>
public int StartLine { get; set; }
/// <summary>
/// The ending line number (1 indexed).
/// </summary>
public int EndLine { get; set; }
/// <summary>
/// The path that this annotation was made on.
/// </summary>
public string Filename { get; set; }
/// <summary>
/// The annotation's message.
/// </summary>
public string Message { get; set; }
/// <summary>
/// The annotation's title.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The annotation's severity level.
/// </summary>
public CheckAnnotationLevel? AnnotationLevel { get; set; }
/// <summary>
/// Additional information about the annotation.
/// </summary>
public string RawDetails { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,9 @@
namespace GitHub.Models
{
public enum CheckAnnotationLevel
{
Failure,
Notice,
Warning,
}
}

Просмотреть файл

@ -0,0 +1,12 @@
namespace GitHub.Models
{
public enum CheckConclusionState
{
ActionRequired,
TimedOut,
Cancelled,
Failure,
Success,
Neutral,
}
}

Просмотреть файл

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
namespace GitHub.Models
{
/// <summary>
/// Model for a single check run.
/// </summary>
public class CheckRunModel
{
/// <summary>The conclusion of the check run.</summary>
public CheckConclusionState? Conclusion { get; set; }
/// <summary>
/// The current status of a Check Run.
/// </summary>
public CheckStatusState Status { get; set; }
/// <summary>
/// Identifies the date and time when the check run was completed.
/// </summary>
public DateTimeOffset? CompletedAt { get; set; }
/// <summary>The name of the check for this check run.</summary>
public string Name { get; set; }
/// <summary>
/// The URL from which to find full details of the check run on the integrator's site.
/// </summary>
public string DetailsUrl { get; set; }
/// <summary>
/// The summary of a Check Run.
/// </summary>
public string Summary { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,10 @@
namespace GitHub.Models
{
public enum CheckStatusState
{
Queued,
InProgress,
Completed,
Requested,
}
}

Просмотреть файл

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
namespace GitHub.Models
{
/// <summary>
/// Model for a single check suite.
/// </summary>
public class CheckSuiteModel
{
/// <summary>
/// The check runs associated with a check suite.
/// </summary>
public List<CheckRunModel> CheckRuns { get; set; }
}
}

Просмотреть файл

@ -93,8 +93,13 @@ namespace GitHub.Models
public IReadOnlyList<PullRequestReviewThreadModel> Threads { get; set; }
/// <summary>
/// Gets or sets a collection of pull request Checks & Statuses
/// Gets or sets a collection of pull request Checks Suites
/// </summary>
public List<StatusModel> Statuses { get; set; }
public IReadOnlyList<CheckSuiteModel> CheckSuites { get; set; }
/// <summary>
/// Gets or sets a collection of pull request Statuses
/// </summary>
public IReadOnlyList<StatusModel> Statuses { get; set; }
}
}

Просмотреть файл

@ -1,7 +1,7 @@
namespace GitHub.Models
{
/// <summary>
/// Holds details about a pull request Status
/// Model for a single pull request Status.
/// </summary>
public class StatusModel
{
@ -24,10 +24,5 @@
/// The descritption for the Status
/// </summary>
public string Description { get; set; }
/// <summary>
/// The Url for the avatar for the Status
/// </summary>
public string AvatarUrl { get; set; }
}
}

Просмотреть файл

@ -59,7 +59,8 @@ namespace GitHub.Models
public int NumberOfWelcomeTrainingClicks { get; set; }
public int NumberOfGitHubPaneHelpClicks { get; set; }
public int NumberOfPRDetailsOpenInGitHub { get; set; }
public int NumberOfPRCheckStatusesOpenInGitHub { get; set; }
public int NumberOfPRStatusesOpenInGitHub { get; set; }
public int NumberOfPRChecksOpenInGitHub { get; set; }
public int NumberOfPRDetailsViewChanges { get; set; }
public int NumberOfPRDetailsViewFile { get; set; }
public int NumberOfPRDetailsCompareWithSolution { get; set; }

Просмотреть файл

@ -373,11 +373,11 @@
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="Octokit.GraphQL, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
<Reference Include="Octokit.GraphQL, Version=0.1.1.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
</Reference>
<Reference Include="Octokit.GraphQL.Core, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
<Reference Include="Octokit.GraphQL.Core, Version=0.1.1.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />

Просмотреть файл

@ -8,6 +8,7 @@ using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using GitHub.Api;
using GitHub.App.Services;
using GitHub.Factories;
using GitHub.InlineReviews.Models;
using GitHub.Models;
@ -17,6 +18,7 @@ using GitHub.Services;
using LibGit2Sharp;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Projection;
using Octokit;
using Octokit.GraphQL;
using Octokit.GraphQL.Core;
using Octokit.GraphQL.Model;
@ -24,6 +26,13 @@ using ReactiveUI;
using Serilog;
using PullRequestReviewEvent = Octokit.PullRequestReviewEvent;
using static Octokit.GraphQL.Variable;
using CheckAnnotationLevel = GitHub.Models.CheckAnnotationLevel;
using CheckConclusionState = GitHub.Models.CheckConclusionState;
using CheckStatusState = GitHub.Models.CheckStatusState;
using DraftPullRequestReviewComment = Octokit.GraphQL.Model.DraftPullRequestReviewComment;
using FileMode = System.IO.FileMode;
using NotFoundException = LibGit2Sharp.NotFoundException;
using PullRequestReviewState = Octokit.GraphQL.Model.PullRequestReviewState;
using StatusState = GitHub.Models.StatusState;
// GraphQL DatabaseId field are marked as deprecated, but we need them for interop with REST.
@ -40,6 +49,7 @@ namespace GitHub.InlineReviews.Services
static readonly ILogger log = LogManager.ForContext<PullRequestSessionService>();
static ICompiledQuery<PullRequestDetailModel> readPullRequest;
static ICompiledQuery<IEnumerable<LastCommitAdapter>> readCommitStatuses;
static ICompiledQuery<IEnumerable<LastCommitAdapter>> readCommitStatusesEnterprise;
static ICompiledQuery<ActorModel> readViewer;
readonly IGitService gitService;
@ -292,14 +302,14 @@ namespace GitHub.InlineReviews.Services
HeadRefName = pr.HeadRefName,
HeadRefSha = pr.HeadRefOid,
HeadRepositoryOwner = pr.HeadRepositoryOwner != null ? pr.HeadRepositoryOwner.Login : null,
State = (PullRequestStateEnum)pr.State,
State = pr.State.FromGraphQl(),
UpdatedAt = pr.UpdatedAt,
Reviews = pr.Reviews(null, null, null, null, null, null).AllPages().Select(review => new PullRequestReviewModel
{
Id = review.Id.Value,
Body = review.Body,
CommitId = review.Commit.Oid,
State = (GitHub.Models.PullRequestReviewState)review.State,
State = review.State.FromGraphQl(),
SubmittedAt = review.SubmittedAt,
Author = new ActorModel
{
@ -346,6 +356,7 @@ namespace GitHub.InlineReviews.Services
var lastCommitModel = await GetPullRequestLastCommitAdapter(address, owner, name, number);
result.Statuses = lastCommitModel.Statuses;
result.CheckSuites = lastCommitModel.CheckSuites;
result.ChangedFiles = files.Select(file => new PullRequestFileModel
{
@ -743,26 +754,69 @@ namespace GitHub.InlineReviews.Services
async Task<LastCommitAdapter> GetPullRequestLastCommitAdapter(HostAddress address, string owner, string name, int number)
{
if (readCommitStatuses == null)
ICompiledQuery<IEnumerable<LastCommitAdapter>> query;
if (address.IsGitHubDotCom())
{
readCommitStatuses = new Query()
.Repository(Var(nameof(owner)), Var(nameof(name)))
.PullRequest(Var(nameof(number))).Commits(last: 1).Nodes.Select(
commit => new LastCommitAdapter
{
Statuses = commit.Commit.Status
.Select(context =>
context.Contexts.Select(statusContext => new StatusModel
{
State = (StatusState)statusContext.State,
Context = statusContext.Context,
TargetUrl = statusContext.TargetUrl,
Description = statusContext.Description,
AvatarUrl = statusContext.Creator.AvatarUrl(null)
}).ToList()
).SingleOrDefault()
}
).Compile();
if (readCommitStatuses == null)
{
readCommitStatuses = new Query()
.Repository(Var(nameof(owner)), Var(nameof(name)))
.PullRequest(Var(nameof(number))).Commits(last: 1).Nodes.Select(
commit => new LastCommitAdapter
{
CheckSuites = commit.Commit.CheckSuites(null, null, null, null, null).AllPages(10)
.Select(suite => new CheckSuiteModel
{
CheckRuns = suite.CheckRuns(null, null, null, null, null).AllPages(10)
.Select(run => new CheckRunModel
{
Conclusion = run.Conclusion.FromGraphQl(),
Status = run.Status.FromGraphQl(),
Name = run.Name,
DetailsUrl = run.Permalink,
Summary = run.Summary,
}).ToList()
}).ToList(),
Statuses = commit.Commit.Status
.Select(context =>
context.Contexts.Select(statusContext => new StatusModel
{
State = statusContext.State.FromGraphQl(),
Context = statusContext.Context,
TargetUrl = statusContext.TargetUrl,
Description = statusContext.Description,
}).ToList()
).SingleOrDefault()
}
).Compile();
}
query = readCommitStatuses;
}
else
{
if (readCommitStatusesEnterprise == null)
{
readCommitStatusesEnterprise = new Query()
.Repository(Var(nameof(owner)), Var(nameof(name)))
.PullRequest(Var(nameof(number))).Commits(last: 1).Nodes.Select(
commit => new LastCommitAdapter
{
Statuses = commit.Commit.Status
.Select(context =>
context.Contexts.Select(statusContext => new StatusModel
{
State = statusContext.State.FromGraphQl(),
Context = statusContext.Context,
TargetUrl = statusContext.TargetUrl,
Description = statusContext.Description,
}).ToList()
).SingleOrDefault()
}
).Compile();
}
query = readCommitStatusesEnterprise;
}
var vars = new Dictionary<string, object>
@ -773,7 +827,7 @@ namespace GitHub.InlineReviews.Services
};
var connection = await graphqlFactory.CreateConnection(address);
var result = await connection.Run(readCommitStatuses, vars);
var result = await connection.Run(query, vars);
return result.First();
}
@ -836,11 +890,6 @@ namespace GitHub.InlineReviews.Services
model.Threads = threads;
}
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)
@ -869,6 +918,8 @@ namespace GitHub.InlineReviews.Services
class LastCommitAdapter
{
public List<CheckSuiteModel> CheckSuites { get; set; }
public List<StatusModel> Statuses { get; set; }
}
}

Просмотреть файл

@ -34,7 +34,7 @@
<package id="Microsoft.VisualStudio.Validation" version="14.1.111" targetFramework="net452" />
<package id="Microsoft.VSSDK.BuildTools" version="14.3.25407" targetFramework="net452" developmentDependency="true" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" />
<package id="Octokit.GraphQL" version="0.1.0-beta" targetFramework="net461" />
<package id="Octokit.GraphQL" version="0.1.1-beta" targetFramework="net461" />
<package id="Rx-Core" version="2.2.5-custom" targetFramework="net461" />
<package id="Rx-Interfaces" version="2.2.5-custom" targetFramework="net461" />
<package id="Rx-Linq" version="2.2.5-custom" targetFramework="net461" />

Просмотреть файл

@ -258,11 +258,11 @@
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="Octokit.GraphQL, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
<Reference Include="Octokit.GraphQL, Version=0.1.1.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
</Reference>
<Reference Include="Octokit.GraphQL.Core, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
<Reference Include="Octokit.GraphQL.Core, Version=0.1.1.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
</Reference>
<Reference Include="rothko, Version=0.0.3.0, Culture=neutral, PublicKeyToken=9f664c41f503810a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rothko.0.0.3-ghfvs\lib\net45\rothko.dll</HintPath>

Просмотреть файл

@ -25,7 +25,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="ColumnZero" />
<ColumnDefinition Width="*" SharedSizeGroup="ColumnOne" />
<!-- <ColumnDefinition Width="*" SharedSizeGroup="ColumnOne" /> -->
<ColumnDefinition Width="Auto" SharedSizeGroup="ColumnTwo" />
<ColumnDefinition Width="*"/>
<ColumnDefinition MinWidth="50" Width="Auto" SharedSizeGroup="ColumnFour" />
@ -41,12 +41,13 @@
<!--
<Image Grid.Column="1" Source="{Binding Avatar}" Height="16" Width="16" />
-->
<Label Grid.Column="2" Foreground="{DynamicResource VsBrush.WindowText}" Content="{Binding Title}"/>
<Label Grid.Column="1" Foreground="{DynamicResource VsBrush.WindowText}" Content="{Binding Title}"/>
<!--
<Label Grid.Column="3" HorizontalAlignment="Right" Content="{Binding Description}" ToolTip="{Binding Description}" />
-->
<Label Grid.Column="4" HorizontalAlignment="Right">
<Hyperlink ToolTip="{Binding DetailsUrl}" Command="{Binding OpenDetailsUrl}">Details</Hyperlink>
<Label Grid.Column="3" HorizontalAlignment="Right"
Visibility="{Binding DetailsUrl, Converter={ghfvs:NullToVisibilityConverter}}">
<Hyperlink ToolTip="{Binding DetailsUrl}" Command="{Binding OpenDetailsUrl}">Details</Hyperlink>
</Label>
</Grid>
</local:GenericPullRequestCheckView>

Просмотреть файл

@ -16,7 +16,7 @@
Name="root">
<d:DesignData.DataContext>
<vm:PullRequestReviewSummaryViewModel Id="1" State="Pending" FileCommentCount="2">
<vm:PullRequestReviewSummaryViewModel Id="1" State="Commented" FileCommentCount="2">
<vm:PullRequestReviewSummaryViewModel.User>
<ghfvs:ActorViewModel Login="meaghanlewis"/>
</vm:PullRequestReviewSummaryViewModel.User>

Просмотреть файл

@ -37,7 +37,7 @@
<package id="Microsoft.VSSDK.BuildTools" version="15.0.26201" targetFramework="net461" developmentDependency="true" />
<package id="Microsoft.VSSDK.Vsixsigntool" version="14.1.24720" targetFramework="net45" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" />
<package id="Octokit.GraphQL" version="0.1.0-beta" targetFramework="net461" />
<package id="Octokit.GraphQL" version="0.1.1-beta" targetFramework="net461" />
<package id="Rothko" version="0.0.3-ghfvs" targetFramework="net461" />
<package id="Rx-Core" version="2.2.5-custom" targetFramework="net45" />
<package id="Rx-Interfaces" version="2.2.5-custom" targetFramework="net45" />

Просмотреть файл

@ -91,11 +91,11 @@
<HintPath>..\..\packages\NSubstitute.2.0.3\lib\net45\NSubstitute.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Octokit.GraphQL, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
<Reference Include="Octokit.GraphQL, Version=0.1.1.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
</Reference>
<Reference Include="Octokit.GraphQL.Core, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
<Reference Include="Octokit.GraphQL.Core, Version=0.1.1.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
<HintPath>..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />

Просмотреть файл

@ -30,7 +30,7 @@
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" />
<package id="NSubstitute" version="2.0.3" targetFramework="net461" />
<package id="NUnit" version="3.9.0" targetFramework="net461" />
<package id="Octokit.GraphQL" version="0.1.0-beta" targetFramework="net461" />
<package id="Octokit.GraphQL" version="0.1.1-beta" targetFramework="net461" />
<package id="Rothko" version="0.0.3-ghfvs" targetFramework="net461" />
<package id="Rx-Core" version="2.2.5-custom" targetFramework="net45" />
<package id="Rx-Interfaces" version="2.2.5-custom" targetFramework="net45" />