Merge branch 'master' into patch-2

This commit is contained in:
Stanley Goldman 2019-04-16 10:43:47 -04:00 коммит произвёл GitHub
Родитель ec4ffcb215 9a9b819741
Коммит f29db95223
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
378 изменённых файлов: 39741 добавлений и 6665 удалений

45
.github/ISSUE_TEMPLATE/localization_suggestion.md поставляемый Normal file
Просмотреть файл

@ -0,0 +1,45 @@
---
name: Localization suggestion
about: Suggest an improvement to our localization
labels: localization, bug
title: 'Localization: '
---
<!-- Hello! Please read the [Contributing Guidelines](https://github.com/github/VisualStudio/blob/master/CONTRIBUTING.md) before submitting an issue regarding the GitHub Extension for Visual Studio. -->
## Language
<!-- Czech -->
<!-- German -->
<!-- Spanish -->
<!-- French -->
<!-- Italian -->
<!-- Japanese -->
<!-- Korean -->
<!-- Polish -->
<!-- Portuguese (Brazil) -->
<!-- Russian -->
<!-- Turkish -->
<!-- Chinese (Simplified) -->
<!-- Chinese (Traditional) -->
## English source
<!-- Translations are made from the English sources, please paste it here. -->
```
```
## Original Translation
<!-- Please paste the original translation here. -->
```
```
## Screenshots
<!-- Translations are sometimes provided by vendors that do not have access to the UI. Providing screenshots can give them a lot of context. -->
## Notes
<!-- If you can, please explain what is incorrect about the translation. -->
## Suggested Translation
<!-- If you can, please suggest an alternate translation. -->
```
```

4
.gitignore поставляемый
Просмотреть файл

@ -26,9 +26,10 @@ build/
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
#NUNIT
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
@ -248,5 +249,4 @@ output.binlog
AkavacheSqliteLinkerOverride.cs
NuGetBuild
WiX.Toolset.DummyFile.txt
nunit-*.xml
GitHubVS.sln.DotSettings

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

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Product>GitHub Extension for Visual Studio</Product>
<Version>2.8.0.0</Version>
<Version>2.10.0.0</Version>
<Copyright>Copyright © GitHub, Inc. 2014-2018</Copyright>
<LangVersion>7.3</LangVersion>
</PropertyGroup>

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

@ -1,8 +1,10 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27703.2035
# Visual Studio Version 16
VisualStudioVersion = 16.0.28603.18
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.VisualStudio.Vsix", "src\GitHub.VisualStudio.Vsix\GitHub.VisualStudio.Vsix.csproj", "{D26B4B40-0C94-48AD-8019-0B9BE46E0071}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.VisualStudio", "src\GitHub.VisualStudio\GitHub.VisualStudio.csproj", "{11569514-5AE5-4B5B-92A2-F10B0967DE5F}"
ProjectSection(ProjectDependencies) = postProject
{7F5ED78B-74A3-4406-A299-70CFB5885B8B} = {7F5ED78B-74A3-4406-A299-70CFB5885B8B}
@ -33,7 +35,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{8E1F
ProjectSection(SolutionItems) = preProject
scripts\Modules\BuildUtils.psm1 = scripts\Modules\BuildUtils.psm1
scripts\Modules\Debugging.psm1 = scripts\Modules\Debugging.psm1
scripts\Modules\Vsix.psm1 = scripts\Modules\Vsix.psm1
scripts\modules\Versioning.ps1 = scripts\modules\Versioning.ps1
scripts\modules\Vsix.ps1 = scripts\modules\Vsix.ps1
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{7B6C5F8D-14B3-443D-B044-0E209AE12BDF}"
@ -141,7 +144,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Octokit.GraphQL.Core", "sub
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Octokit.GraphQL", "submodules\octokit.graphql.net\Octokit.GraphQL\Octokit.GraphQL.csproj", "{791B408C-0ABC-465B-9EB1-A2422D67F418}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.StartPage.UnitTests", "test\GitHub.StartPage.UnitTests\GitHub.StartPage.UnitTests.csproj", "{B467682B-9F0E-42D8-8A20-1DE78F798793}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitHub.StartPage.UnitTests", "test\GitHub.StartPage.UnitTests\GitHub.StartPage.UnitTests.csproj", "{B467682B-9F0E-42D8-8A20-1DE78F798793}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -151,6 +154,14 @@ Global
ReleaseWithoutVsix|Any CPU = ReleaseWithoutVsix|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D26B4B40-0C94-48AD-8019-0B9BE46E0071}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D26B4B40-0C94-48AD-8019-0B9BE46E0071}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D26B4B40-0C94-48AD-8019-0B9BE46E0071}.DebugWithoutVsix|Any CPU.ActiveCfg = DebugWithoutVsix|Any CPU
{D26B4B40-0C94-48AD-8019-0B9BE46E0071}.DebugWithoutVsix|Any CPU.Build.0 = DebugWithoutVsix|Any CPU
{D26B4B40-0C94-48AD-8019-0B9BE46E0071}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D26B4B40-0C94-48AD-8019-0B9BE46E0071}.Release|Any CPU.Build.0 = Release|Any CPU
{D26B4B40-0C94-48AD-8019-0B9BE46E0071}.ReleaseWithoutVsix|Any CPU.ActiveCfg = ReleaseWithoutVsix|Any CPU
{D26B4B40-0C94-48AD-8019-0B9BE46E0071}.ReleaseWithoutVsix|Any CPU.Build.0 = ReleaseWithoutVsix|Any CPU
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.DebugWithoutVsix|Any CPU.ActiveCfg = DebugWithoutVsix|Any CPU

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

@ -19,7 +19,6 @@ Official builds of this extension are available at [the official website](https:
[![Build status](https://ci.appveyor.com/api/projects/status/dl8is5iqwt9qf3t7/branch/master?svg=true)](https://ci.appveyor.com/project/github-windows/visualstudio/branch/master)
[![Build Status](https://github-editor-tools.visualstudio.com/VisualStudio/_apis/build/status/github.VisualStudio?branchName=master)](https://github-editor-tools.visualstudio.com/VisualStudio/_build/latest?definitionId=4&branchName=master)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/github-visual-studio/localized.svg)](https://crowdin.com/project/github-visual-studio)
[![codecov](https://codecov.io/gh/GitHub/VisualStudio/branch/master/graph/badge.svg)](https://codecov.io/gh/GitHub/VisualStudio)
[![Follow GitHub for Visual Studio](https://img.shields.io/twitter/follow/GitHubVS.svg?style=social "Follow GitHubVS")](https://twitter.com/githubvs?ref_src=twsrc%5Etfw) [![Join the chat at https://gitter.im/github/VisualStudio](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/github/VisualStudio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

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

@ -1,5 +1,5 @@
os: Visual Studio 2017
version: '2.8.0.{build}'
version: '2.10.0.{build}'
skip_tags: true
install:
@ -69,6 +69,7 @@ for:
branches:
only:
- master
- /releases/(?:(?!-vsinstaller).)*?/
environment:
matrix:
- BUILD_TYPE: package

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

@ -1,9 +0,0 @@
preserve_hierarchy: true
files:
- source: /src/GitHub.Resources/Resources.resx
translation: /%original_path%/Resources.%locale%.resx
- source: /src/GitHub.VisualStudio/xlf/GitHub.VisualStudio.vsct.zh-CN.xlf
translation: /%original_path%/GitHub.VisualStudio.vsct.%locale%.xlf
- source: /src/GitHub.VisualStudio/xlf/VSPackage.zh-CN.xlf
translation: /%original_path%/VSPackage.%locale%.xlf

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

@ -2,7 +2,7 @@
After you provide your GitHub or GitHub Enterprise credentials to GitHub for Visual Studio, the extension automatically detects the personal, collaborator and organization repositories you have access to on your account.
## Opening the clone dialog
## Opening the clone dialog
### From **Team Explorer**
@ -17,7 +17,12 @@ Next to the account you want to clone from, click **Clone**.
### From the **Start Page**
Using Visual Studio 2017, click the `GitHub` button on the `Start Page` to open the clone dialog.
Using Visual Studio 2017, click the `GitHub` button on the `Start Page` to open the clone dialog.
### From the **Start Window**
Using Visual Studio 2019, on the `Start Window` select `Clone or check out code` and then click the `GitHub` button to open the clone dialog.
### From the **File** menu
@ -26,14 +31,19 @@ Go to `File > Open > Open From GitHub...`
## Clone repositories
1. In the list of repositories, scroll until you find the repository you'd like to clone. You can also filter the repository results by using the *Filter* text box.
1. In the list of repositories, scroll until you find the repository you'd like to clone.
![List of GitHub repositories that can be cloned inside a dialog](images/clone-dialog.png)
You can also filter the repository results by using the *Filter* text box.
In addition to using the list of personal, collaborator and organization repositories, you can use the URL tab to clone a public repository by its URL or using the repository owner and name.
In addition to using the list of personal, collaborator and organization repositories, you can enter a repository URL to clone a public repository.
![List of GitHub repositories that can be cloned inside a dialog](images/clone-url-dialog.png)
![Unified clone and open dialog](images/unified-clone-dialog.png)
2. If desired, change the local path that the repository will be cloned into, or leave the default as-is.
3. Once a repository is selected and the path is set, Click **Clone**.
4. In Team Explorer, under the list of solutions, double-click on a solution to open it in Visual Studio.
## Open repositories
For any repository that you select from the list or provide a URL for that you already have cloned locally, the **Open** button becomes enabled and a message shows that you have already cloned the repository to that location.
![Open option enabled in clone dialog](images/open-cloned-repository.png)

Двоичные данные
docs/using/images/clone-dialog.png

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

До

Ширина:  |  Высота:  |  Размер: 210 KiB

Двоичные данные
docs/using/images/clone-url-dialog.png

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

До

Ширина:  |  Высота:  |  Размер: 125 KiB

Двоичные данные
docs/using/images/open-cloned-repository.png Normal file

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

После

Ширина:  |  Высота:  |  Размер: 182 KiB

Двоичные данные
docs/using/images/unified-clone-dialog.png Normal file

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

После

Ширина:  |  Высота:  |  Размер: 176 KiB

Двоичные данные
docs/using/images/view-conversation.png Normal file

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

После

Ширина:  |  Высота:  |  Размер: 957 KiB

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

@ -10,5 +10,5 @@
5. Click the **Publish to GitHub** button.
![Location of the Publish to GitHub button in the Team Explorer pane](images/publish-to-github.png)
6. Enter a name and description for the repository on GitHub.
7. Check the **Private Repository** box if you want to upload the repository as a private repository on GitHub. You must have a [Developer, Team or Business account](https://github.com/pricing) to create private repositories.
7. Check the **Private Repository** box if you want to upload the repository as a private repository on GitHub.
8. Click the **Publish** button.

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

@ -15,13 +15,19 @@ GitHub for Visual Studio provides facilities for reviewing a pull request direct
The Pull Request Details view shows the current state of the pull request, including:
- information about who created the pull request
- the source and target branch
- a description of the pull request
- a description of the pull request (collapsed by default)
- reviewers and the status of their review
- checks (if checks have been enabled for the repository)
- the files changed
![The details of a single pull request in the GitHub pane](images/pr-detail-view.png)
## Viewing conversation details
Click the comment count link in the GitHub pane to open up the conversation view. The conversation view shows the Pull Request description, a history of commits, and comments made.
![View the conversation for a pull request](images/view-conversation.png)
## Checking out a pull request
To check out the pull request branch, click the **Checkout [branch]** link where [branch] is the name of the branch that will be checked out.

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

@ -1,5 +1,5 @@
{
"msbuild-sdks": {
"MSBuild.Sdk.Extras": "1.6.52"
}
"msbuild-sdks": {
"MSBuild.Sdk.Extras": "1.6.61"
}
}

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

@ -1,7 +1,7 @@
Set-StrictMode -Version Latest
New-Module -ScriptBlock {
$gitHubDirectory = Join-Path $rootDirectory src\GitHub.VisualStudio
$gitHubDirectory = Join-Path $rootDirectory src\GitHub.VisualStudio.Vsix
function Get-VsixManifestPath {
Join-Path $gitHubDirectory source.extension.vsixmanifest

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -0,0 +1,34 @@
/*
Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)
This file is part of FileCache (http://github.com/acarteas/FileCache).
FileCache is distributed under the Apache License 2.0.
Consult "LICENSE.txt" included in this package for the Apache License 2.0.
*/
using System.Reflection;
namespace System.Runtime.Caching
{
/// <summary>
/// You should be able to copy & paste this code into your local project to enable caching custom objects.
/// </summary>
public sealed class FileCacheBinder : System.Runtime.Serialization.SerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
Type typeToDeserialize = null;
String currentAssembly = Assembly.GetExecutingAssembly().FullName;
// In this case we are always using the current assembly
assemblyName = currentAssembly;
// Get the type using the typeName and assemblyName
typeToDeserialize = Type.GetType(String.Format("{0}, {1}",
typeName, assemblyName));
return typeToDeserialize;
}
}
}

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

@ -0,0 +1,22 @@
/*
Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)
This file is part of FileCache (http://github.com/acarteas/FileCache).
FileCache is distributed under the Apache License 2.0.
Consult "LICENSE.txt" included in this package for the Apache License 2.0.
*/
namespace System.Runtime.Caching
{
public class FileCacheEventArgs : EventArgs
{
public long CurrentCacheSize { get; private set; }
public long MaxCacheSize { get; private set; }
public FileCacheEventArgs(long currentSize, long maxSize)
{
CurrentCacheSize = currentSize;
MaxCacheSize = maxSize;
}
}
}

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

@ -0,0 +1,33 @@
/*
Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)
This file is part of FileCache (http://github.com/acarteas/FileCache).
FileCache is distributed under the Apache License 2.0.
Consult "LICENSE.txt" included in this package for the Apache License 2.0.
*/
namespace System.Runtime.Caching
{
[Serializable]
public class FileCachePayload
{
public object Payload { get; set; }
public SerializableCacheItemPolicy Policy { get; set; }
public FileCachePayload(object payload)
{
Payload = payload;
Policy = new SerializableCacheItemPolicy()
{
AbsoluteExpiration = DateTime.Now.AddYears(10)
};
}
public FileCachePayload(object payload, SerializableCacheItemPolicy policy)
{
Payload = payload;
Policy = policy;
}
}
}

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

@ -0,0 +1,207 @@
/*
Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)
This file is part of FileCache (http://github.com/acarteas/FileCache).
FileCache is distributed under the Apache License 2.0.
Consult "LICENSE.txt" included in this package for the Apache License 2.0.
*/
using System.Collections.Generic;
namespace System.Runtime.Caching
{
/// <summary>
/// A basic min priorty queue (min heap)
/// </summary>
/// <typeparam name="T">Data type to store</typeparam>
public class PriortyQueue<T> where T : IComparable<T>
{
private List<T> _items;
private IComparer<T> _comparer;
/// <summary>
/// Default constructor.
/// </summary>
/// <param name="comparer">The comparer to use. The default comparer will make the smallest item the root of the heap.
///
/// </param>
public PriortyQueue(IComparer<T> comparer = null)
{
_items = new List<T>();
if (comparer == null)
{
_comparer = new GenericComparer<T>();
}
}
/// <summary>
/// Constructor that will convert an existing list into a min heap
/// </summary>
/// <param name="unsorted">The unsorted list of items</param>
/// <param name="comparer">The comparer to use. The default comparer will make the smallest item the root of the heap.</param>
public PriortyQueue(List<T> unsorted, IComparer<T> comparer = null)
: this(comparer)
{
for (int i = 0; i < unsorted.Count; i++)
{
_items.Add(unsorted[i]);
}
BuildHeap();
}
private void BuildHeap()
{
for (int i = _items.Count / 2; i >= 0; i--)
{
adjustHeap(i);
}
}
//Percolates the item specified at by index down into its proper location within a heap. Used
//for dequeue operations and array to heap conversions
private void adjustHeap(int index)
{
//cannot percolate empty list
if (_items.Count == 0)
{
return;
}
//GOAL: get value at index, make sure this value is less than children
// IF NOT: swap with smaller of two
// (continue to do so until we can't swap)
T item = _items[index];
//helps us figure out if a given index has children
int end_location = _items.Count;
//keeps track of smallest index
int smallest_index = index;
//while we're not the last thing in the heap
while (index < end_location)
{
//get left child index
int left_child_index = (2 * index) + 1;
int right_child_index = left_child_index + 1;
//Three cases:
// 1. left index is out of range
// 2. right index is out or range
// 3. both indices are valid
if (left_child_index < end_location)
{
//CASE 1 is FALSE
//remember that left index is the smallest
smallest_index = left_child_index;
if (right_child_index < end_location)
{
//CASE 2 is FALSE (CASE 3 is true)
//TODO: find value of smallest index
smallest_index = (_comparer.Compare(_items[left_child_index], _items[right_child_index]) < 0)
? left_child_index
: right_child_index;
}
}
//we have two things: original index and (potentially) a child index
if (_comparer.Compare(_items[index], _items[smallest_index]) > 0)
{
//move parent down (it was too big)
T temp = _items[index];
_items[index] = _items[smallest_index];
_items[smallest_index] = temp;
//update index
index = smallest_index;
}
else
{
//no swap necessary
break;
}
}
}
public bool isEmpty()
{
return _items.Count == 0;
}
public int GetSize()
{
return _items.Count;
}
public void Enqueue(T item)
{
//calculate positions
int current_position = _items.Count;
int parent_position = (current_position - 1) / 2;
//insert element (note: may get erased if we hit the WHILE loop)
_items.Add(item);
//find parent, but be careful if we are an empty queue
T parent = default(T);
if (parent_position >= 0)
{
//find parent
parent = _items[parent_position];
//bubble up until we're done
while (_comparer.Compare(parent, item) > 0 && current_position > 0)
{
//move parent down
_items[current_position] = parent;
//recalculate position
current_position = parent_position;
parent_position = (current_position - 1) / 2;
//make sure that we have a valid index
if (parent_position >= 0)
{
//find parent
parent = _items[parent_position];
}
}
} //end check for nullptr
//after WHILE loop, current_position will point to the place that
//variable "item" needs to go
_items[current_position] = item;
}
public T GetFirst()
{
return _items[0];
}
public T Dequeue()
{
int last_position = _items.Count - 1;
T last_item = _items[last_position];
T top = _items[0];
_items[0] = last_item;
_items.RemoveAt(_items.Count - 1);
//percolate down
adjustHeap(0);
return top;
}
private class GenericComparer<TInner> : IComparer<TInner> where TInner : IComparable<TInner>
{
public int Compare(TInner x, TInner y)
{
return x.CompareTo(y);
}
}
}
}

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

@ -0,0 +1,44 @@
/*
Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)
This file is part of FileCache (http://github.com/acarteas/FileCache).
FileCache is distributed under the Apache License 2.0.
Consult "LICENSE.txt" included in this package for the Apache License 2.0.
*/
namespace System.Runtime.Caching
{
[Serializable]
public class SerializableCacheItemPolicy
{
public DateTimeOffset AbsoluteExpiration { get; set; }
private TimeSpan _slidingExpiration;
public TimeSpan SlidingExpiration
{
get
{
return _slidingExpiration;
}
set
{
_slidingExpiration = value;
if (_slidingExpiration > new TimeSpan())
{
AbsoluteExpiration = DateTimeOffset.Now.Add(_slidingExpiration);
}
}
}
public SerializableCacheItemPolicy(CacheItemPolicy policy)
{
AbsoluteExpiration = policy.AbsoluteExpiration;
SlidingExpiration = policy.SlidingExpiration;
}
public SerializableCacheItemPolicy()
{
SlidingExpiration = new TimeSpan();
}
}
}

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

@ -17,6 +17,7 @@
<ItemGroup>
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Runtime.Caching" />
</ItemGroup>
<ItemGroup>

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

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.Caching;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Extensions;
using Octokit.GraphQL;
using Octokit.GraphQL.Core;
namespace GitHub.Api
{
public class GraphQLClient : IGraphQLClient
{
public static readonly TimeSpan DefaultCacheDuration = TimeSpan.FromHours(8);
readonly IConnection connection;
readonly FileCache cache;
public GraphQLClient(
IConnection connection,
FileCache cache)
{
this.connection = connection;
this.cache = cache;
}
public Task ClearCache(string regionName)
{
// Switch to background thread because FileCache does not provide an async API.
return Task.Run(() => cache.ClearRegion(GetFullRegionName(regionName)));
}
public Task<T> Run<T>(
IQueryableValue<T> query,
Dictionary<string, object> variables = null,
bool refresh = false,
TimeSpan? cacheDuration = null,
string regionName = null,
CancellationToken cancellationToken = default)
{
return Run(query.Compile(), variables, refresh, cacheDuration, regionName, cancellationToken);
}
public Task<IEnumerable<T>> Run<T>(
IQueryableList<T> query,
Dictionary<string, object> variables = null,
bool refresh = false,
TimeSpan? cacheDuration = null,
string regionName = null,
CancellationToken cancellationToken = default)
{
return Run(query.Compile(), variables, refresh, cacheDuration, regionName, cancellationToken);
}
public async Task<T> Run<T>(
ICompiledQuery<T> query,
Dictionary<string, object> variables = null,
bool refresh = false,
TimeSpan? cacheDuration = null,
string regionName = null,
CancellationToken cancellationToken = default)
{
if (!query.IsMutation)
{
var wrapper = new CachingWrapper(
this,
refresh,
cacheDuration ?? DefaultCacheDuration,
GetFullRegionName(regionName));
return await wrapper.Run(query, variables, cancellationToken);
}
else
{
return await connection.Run(query, variables, cancellationToken);
}
}
string GetFullRegionName(string regionName)
{
var result = connection.Uri.Host;
if (!string.IsNullOrWhiteSpace(regionName))
{
result += Path.DirectorySeparatorChar + regionName;
}
return result.EnsureValidPath();
}
static string GetHash(string input)
{
var sb = new StringBuilder();
using (var hash = SHA256.Create())
{
var result = hash.ComputeHash(Encoding.UTF8.GetBytes(input));
foreach (var b in result)
{
sb.Append(b.ToString("x2", CultureInfo.InvariantCulture));
}
}
return sb.ToString();
}
class CachingWrapper : IConnection
{
readonly GraphQLClient owner;
readonly bool refresh;
readonly TimeSpan cacheDuration;
readonly string regionName;
public CachingWrapper(
GraphQLClient owner,
bool refresh,
TimeSpan cacheDuration,
string regionName)
{
this.owner = owner;
this.refresh = refresh;
this.cacheDuration = cacheDuration;
this.regionName = regionName;
}
public Uri Uri => owner.connection.Uri;
public Task<string> Run(string query, CancellationToken cancellationToken = default)
{
// Switch to background thread because FileCache does not provide an async API.
return Task.Run(async () =>
{
var hash = GetHash(query);
if (refresh)
{
owner.cache.Remove(hash, regionName);
}
var data = (string) owner.cache.Get(hash, regionName);
if (data != null)
{
return data;
}
var result = await owner.connection.Run(query, cancellationToken);
owner.cache.Add(hash, result, DateTimeOffset.Now + cacheDuration, regionName);
return result;
}, cancellationToken);
}
}
}
}

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

@ -1,6 +1,9 @@
using System;
using System.ComponentModel.Composition;
using System.IO;
using System.Runtime.Caching;
using System.Threading.Tasks;
using GitHub.Info;
using GitHub.Models;
using GitHub.Primitives;
using Octokit.GraphQL;
@ -17,6 +20,7 @@ namespace GitHub.Api
{
readonly IKeychain keychain;
readonly IProgram program;
readonly FileCache cache;
/// <summary>
/// Initializes a new instance of the <see cref="GraphQLClientFactory"/> class.
@ -28,14 +32,21 @@ namespace GitHub.Api
{
this.keychain = keychain;
this.program = program;
var cachePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
ApplicationInfo.ApplicationName,
"GraphQLCache");
cache = new FileCache(cachePath);
}
/// <inheirtdoc/>
public Task<Octokit.GraphQL.IConnection> CreateConnection(HostAddress address)
public Task<IGraphQLClient> CreateConnection(HostAddress address)
{
var credentials = new GraphQLKeychainCredentialStore(keychain, address);
var header = new ProductHeaderValue(program.ProductHeader.Name, program.ProductHeader.Version);
return Task.FromResult<Octokit.GraphQL.IConnection>(new Connection(header, address.GraphQLUri, credentials));
var connection = new Connection(header, address.GraphQLUri, credentials);
return Task.FromResult<IGraphQLClient>(new GraphQLClient(connection, cache));
}
}
}

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

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Octokit.GraphQL;
using Octokit.GraphQL.Core;
namespace GitHub.Api
{
public interface IGraphQLClient
{
Task ClearCache(string regionName);
Task<T> Run<T>(
IQueryableValue<T> query,
Dictionary<string, object> variables = null,
bool refresh = false,
TimeSpan? cacheDuration = null,
string regionName = null,
CancellationToken cancellationToken = default);
Task<IEnumerable<T>> Run<T>(
IQueryableList<T> query,
Dictionary<string, object> variables = null,
bool refresh = false,
TimeSpan? cacheDuration = null,
string regionName = null,
CancellationToken cancellationToken = default);
Task<T> Run<T>(
ICompiledQuery<T> query,
Dictionary<string, object> variables = null,
bool refresh = false,
TimeSpan? cacheDuration = null,
string regionName = null,
CancellationToken cancellationToken = default);
}
}

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

@ -4,16 +4,15 @@ using GitHub.Primitives;
namespace GitHub.Api
{
/// <summary>
/// Creates GraphQL <see cref="Octokit.GraphQL.IConnection"/>s for querying the
/// GitHub GraphQL API.
/// Creates <see cref="IGraphQLClient"/>s for querying the GitHub GraphQL API.
/// </summary>
public interface IGraphQLClientFactory
{
/// <summary>
/// Creates a new <see cref="Octokit.GraphQL.IConnection"/>.
/// Creates a new <see cref="IGraphQLClient"/>.
/// </summary>
/// <param name="address">The address of the server.</param>
/// <returns>A task returning the created connection.</returns>
Task<Octokit.GraphQL.IConnection> CreateConnection(HostAddress address);
/// <returns>A task returning the created client.</returns>
Task<IGraphQLClient> CreateConnection(HostAddress address);
}
}

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

@ -42,8 +42,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="LibGit2Sharp" Version="0.23.1" />
<PackageReference Include="LibGit2Sharp.NativeBinaries" Version="1.0.164" />
<PackageReference Include="LibGit2Sharp" Version="0.26.0" />
<PackageReference Include="Madskristensen.VisualStudio.SDK" Version="14.3.75-pre" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.1" />
<PackageReference Include="Microsoft.VisualStudio.StaticReviews.Embeddable" Version="0.1.14-alpha" />

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

@ -110,8 +110,8 @@ namespace GitHub.Models
}
}
PullRequestStateEnum status;
public PullRequestStateEnum State
PullRequestState status;
public PullRequestState State
{
get { return status; }
set
@ -126,8 +126,8 @@ namespace GitHub.Models
}
// TODO: Remove these property once maintainer workflow has been merged to master.
public bool IsOpen => State == PullRequestStateEnum.Open;
public bool Merged => State == PullRequestStateEnum.Merged;
public bool IsOpen => State == PullRequestState.Open;
public bool Merged => State == PullRequestState.Merged;
int commentCount;
public int CommentCount

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

@ -0,0 +1,51 @@
using System;
using GitHub.Extensions;
using GitHub.Helpers;
namespace GitHub.Models
{
/// <summary>
/// Represents a single auto completion suggestion (mentions, emojis, issues) in a generic format that can be
/// easily cached.
/// </summary>
public class SuggestionItem
{
public SuggestionItem(string name, string description)
{
Guard.ArgumentNotEmptyString(name, "name");
Guard.ArgumentNotEmptyString(description, "description");
Name = name;
Description = description;
}
public SuggestionItem(string name, string description, string imageUrl)
{
Guard.ArgumentNotEmptyString(name, "name");
Name = name;
Description = description;
ImageUrl = imageUrl;
}
/// <summary>
/// The name to display for this entry
/// </summary>
public string Name { get; set; }
/// <summary>
/// Additional details about the entry
/// </summary>
public string Description { get; set; }
/// <summary>
/// An image url for this entry
/// </summary>
public string ImageUrl { get; set; }
/// <summary>
/// The date this suggestion was last modified according to the API.
/// </summary>
public DateTimeOffset? LastModifiedDate { get; set; }
}
}

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

@ -2,7 +2,9 @@
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData.Dialog.Clone")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData.Documents")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Dialog")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Dialog.Clone")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Documents")]
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.GitHubPane")]

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

@ -1,6 +1,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using GitHub.ViewModels;
using ReactiveUI;
@ -22,7 +25,9 @@ namespace GitHub.SampleData
public CommentEditState EditState { get; set; }
public bool IsReadOnly { get; set; }
public bool IsSubmitting { get; set; }
public bool CanCancel { get; } = true;
public bool CanDelete { get; } = true;
public string CommitCaption { get; set; } = "Comment";
public ICommentThreadViewModel Thread { get; }
public DateTimeOffset CreatedAt => DateTime.Now.Subtract(TimeSpan.FromDays(3));
public IActorViewModel Author { get; set; }
@ -31,7 +36,13 @@ namespace GitHub.SampleData
public ReactiveCommand<Unit, Unit> BeginEdit { get; }
public ReactiveCommand<Unit, Unit> CancelEdit { get; }
public ReactiveCommand<Unit, Unit> CommitEdit { get; }
public ReactiveCommand<Unit, Unit> OpenOnGitHub { get; }
public ReactiveCommand<Unit, Unit> OpenOnGitHub { get; } = ReactiveCommand.Create(() => { });
public ReactiveCommand<Unit, Unit> Delete { get; }
public IAutoCompleteAdvisor AutoCompleteAdvisor { get; }
public Task InitializeAsync(ICommentThreadViewModel thread, ActorModel currentUser, CommentModel comment, CommentEditState state)
{
return Task.CompletedTask;
}
}
}

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

@ -2,11 +2,13 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
using System.Windows.Data;
using GitHub.Models;
using GitHub.ViewModels;
using GitHub.ViewModels.Dialog.Clone;
using ReactiveUI;
namespace GitHub.SampleData.Dialog.Clone
{

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

@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.ViewModels;
using GitHub.ViewModels.Documents;
using ReactiveUI;
namespace GitHub.SampleData.Documents
{
public class IssueishCommentThreadViewModelDesigner : ViewModelBase, IIssueishCommentThreadViewModel
{
public IActorViewModel CurrentUser { get; } = new ActorViewModelDesigner("grokys");
public Task InitializeAsync(ActorModel currentUser, IssueishDetailModel model, bool addPlaceholder) => Task.CompletedTask;
public Task DeleteComment(ICommentViewModel comment) => Task.CompletedTask;
public Task EditComment(ICommentViewModel comment) => Task.CompletedTask;
public Task PostComment(ICommentViewModel comment) => Task.CompletedTask;
public Task CloseOrReopen(ICommentViewModel comment) => Task.CompletedTask;
}
}

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

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.ViewModels;
using GitHub.ViewModels.Documents;
using ReactiveUI;
namespace GitHub.SampleData.Documents
{
public class PullRequestPageViewModelDesigner : ViewModelBase, IPullRequestPageViewModel
{
public PullRequestPageViewModelDesigner()
{
Body = @"Save drafts of inline comments, PR reviews and PRs.
> Note: This feature required a refactoring of the comment view models because they now need async initialization and to be available from GitHub.App. This part of the PR has been submitted separately as #1993 to ease review. The two PRs can alternatively be reviewed as one if that's more convenient.
As described in #1905, it is easy to lose a comment that you're working on if you close the diff view accidentally. This PR saves drafts of comments as they are being written to an SQLite database.
In addition to saving drafts of inline comments, it also saves comments to PR reviews and PRs themselves.
The comments are written to an SQLite database directly instead of going through Akavache because in the case of inline reviews, there can be many drafts in progress on a separate file. When a diff is opened we need to look for any comments present on that file and show the most recent. That use-case didn't fit well with Akavache (being a pure key/value store).
## Testing
### Inline Comments
- Open a PR
- Open the diff of a file
- Start adding a comment
- Close the comment by closing the peek view, or the document tab
- Reopen the diff
- You should see the comment displayed in edit mode with the draft of the comment you were previously writing
### PR reviews
- Open a PR
- Click ""Add your review""
- Start adding a review
- Click the ""Back"" button and navigate to a different PR
- Click the ""Back"" button and navigate to the original PR
- Click ""Add your review""
- You should see the the draft of the review you were previously writing
### PRs
-Click ""Create new"" at the top of the PR list
- Start adding a PR title/ description
- Close VS
- Restart VS and click ""Create new"" again
- You should see the the draft of the PR you were previously writing
Depends on #1993
Fixes #1905";
Timeline = new IViewModel[]
{
new CommitListViewModel(
new CommitSummaryViewModel(new CommitModel
{
Author = new CommitActorModel { User = new ActorModel{ Login = "grokys" }},
AbbreviatedOid = "c7c7d25",
MessageHeadline = "Refactor comment view models."
}),
new CommitSummaryViewModel(new CommitModel
{
Author = new CommitActorModel { User = new ActorModel{ Login = "shana" }},
AbbreviatedOid = "04e6a90",
MessageHeadline = "Refactor comment view models.",
})),
new CommentViewModelDesigner
{
Author = new ActorViewModelDesigner("meaghanlewis"),
Body = @"This is looking great! Really enjoying using this feature so far.
When leaving an inline comment, the comment posts successfully and then a new comment is drafted with the same text.",
},
new CommentViewModelDesigner
{
Author = new ActorViewModelDesigner("grokys"),
Body = @"Oops, sorry about that @meaghanlewis - I was sure I tested those things, but must have got messed up again at some point. Should be fixed now.",
},
};
}
public string Id { get; set; }
public PullRequestState State { get; set; } = PullRequestState.Open;
public IReadOnlyList<IViewModel> Timeline { get; }
public string SourceBranchDisplayName { get; set; } = "feature/save-drafts";
public string TargetBranchDisplayName { get; set; } = "master";
public IActorViewModel Author { get; set; } = new ActorViewModelDesigner("grokys");
public int CommitCount { get; set; } = 2;
public string Body { get; set; }
public int Number { get; set; } = 1994;
public LocalRepositoryModel LocalRepository { get; }
public RemoteRepositoryModel Repository { get; set; }
public string Title { get; set; } = "Save drafts of comments";
public Uri WebUrl { get; set; }
public ReactiveCommand<Unit, Unit> OpenOnGitHub { get; }
public ReactiveCommand<string, Unit> ShowCommit { get; }
public Task InitializeAsync(RemoteRepositoryModel repository, LocalRepositoryModel localRepository, ActorModel currentUser, PullRequestDetailModel model)
{
throw new NotImplementedException();
}
}
}

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

@ -15,5 +15,8 @@ namespace GitHub.SampleData
public IRepository GetRepository(string path) => null;
public UriString GetUri(string path, string remote = "origin") => null;
public UriString GetUri(IRepository repository, string remote = "origin") => null;
public Task<Patch> Compare(IRepository repository, string sha1, string sha2, string path) => null;
public Task<ContentChanges> CompareWith(IRepository repository, string sha1, string sha2, string path, byte[] contents) => null;
public Task<TreeChanges> Compare(IRepository repository, string sha1, string sha2, bool detectRenames = false) => null;
}
}

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

@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using GitHub.Validation;
using GitHub.ViewModels.GitHubPane;
using ReactiveUI;
@ -53,6 +54,7 @@ namespace GitHub.SampleData
public string PRTitle { get; set; }
public ReactivePropertyValidator TitleValidator { get; }
public IAutoCompleteAdvisor AutoCompleteAdvisor { get; }
public ReactivePropertyValidator BranchValidator { get; }

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

@ -124,6 +124,7 @@ This requires that errors be propagated from the viewmodel to the view and from
public ReactiveCommand<Unit, Unit> Pull { get; }
public ReactiveCommand<Unit, Unit> Push { get; }
public ReactiveCommand<Unit, Unit> SyncSubmodules { get; }
public ReactiveCommand<Unit, Unit> OpenConversation { get; }
public ReactiveCommand<Unit, Unit> OpenOnGitHub { get; }
public ReactiveCommand<IPullRequestReviewSummaryViewModel, Unit> ShowReview { get; }
public ReactiveCommand<IPullRequestCheckViewModel, Unit> ShowAnnotations { get; }

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

@ -63,6 +63,7 @@ namespace GitHub.SampleData
public IReadOnlyList<string> States { get; }
public Uri WebUrl => null;
public ReactiveCommand<Unit, Unit> CreatePullRequest { get; }
public ReactiveCommand<IPullRequestListItemViewModel, Unit> OpenConversation { get; }
public ReactiveCommand<IIssueListItemViewModelBase, Unit> OpenItem { get; }
public ReactiveCommand<IPullRequestListItemViewModel, IPullRequestListItemViewModel> OpenItemInBrowser { get; }

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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using GitHub.ViewModels.GitHubPane;
using ReactiveUI;
@ -53,6 +54,7 @@ However, if you're two-way binding these properties to a UI, then ignore the rea
public ReactiveCommand<Unit, Unit> Comment { get; }
public ReactiveCommand<Unit, Unit> RequestChanges { get; }
public ReactiveCommand<Unit, Unit> Cancel { get; }
public IAutoCompleteAdvisor AutoCompleteAdvisor { get; }
public Task InitializeAsync(
LocalRepositoryModel localRepository,

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

@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reactive.Linq;
using GitHub.Extensions;
using GitHub.Logging;
using GitHub.Models;
using Serilog;
namespace GitHub.Services
{
[Export(typeof(IAutoCompleteAdvisor))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class AutoCompleteAdvisor : IAutoCompleteAdvisor
{
const int SuggestionCount = 5; // The number of suggestions we'll provide. github.com does 5.
static readonly ILogger log = LogManager.ForContext<AutoCompleteAdvisor>();
readonly Lazy<Dictionary<string, IAutoCompleteSource>> prefixSourceMap;
[ImportingConstructor]
public AutoCompleteAdvisor([ImportMany(typeof(IAutoCompleteSource))]IEnumerable<IAutoCompleteSource> autocompleteSources)
{
prefixSourceMap = new Lazy<Dictionary<string, IAutoCompleteSource>>(
() => autocompleteSources.ToDictionary(s => s.Prefix, s => s));
}
public IObservable<AutoCompleteResult> GetAutoCompletionSuggestions(string text, int caretPosition)
{
Guard.ArgumentNotNull("text", text);
if (caretPosition < 0 || caretPosition > text.Length)
{
string error = String.Format(CultureInfo.InvariantCulture,
"The CaretPosition '{0}', is not in the range of '0' and the text length '{1}' for the text '{2}'",
caretPosition,
text.Length,
text);
// We need to be alerted when this happens because it should never happen.
// But it apparently did happen in production.
Debug.Fail(error);
log.Error(error);
return Observable.Empty<AutoCompleteResult>();
}
var tokenAndSource = PrefixSourceMap
.Select(kvp => new {Source = kvp.Value, Token = ParseAutoCompletionToken(text, caretPosition, kvp.Key)})
.FirstOrDefault(s => s.Token != null);
if (tokenAndSource == null)
{
return Observable.Return(AutoCompleteResult.Empty);
}
return tokenAndSource.Source.GetSuggestions()
.Select(suggestion => new
{
suggestion,
rank = suggestion.GetSortRank(tokenAndSource.Token.SearchSearchPrefix)
})
.Where(suggestion => suggestion.rank > -1)
.ToList()
.Select(suggestions => suggestions
.OrderByDescending(s => s.rank)
.ThenBy(s => s.suggestion.Name)
.Take(SuggestionCount)
.Select(s => s.suggestion)
.ToList())
.Select(suggestions => new AutoCompleteResult(tokenAndSource.Token.Offset,
new ReadOnlyCollection<AutoCompleteSuggestion>(suggestions)))
.Catch<AutoCompleteResult, Exception>(e =>
{
log.Error(e, "Error Getting AutoCompleteResult");
return Observable.Return(AutoCompleteResult.Empty);
});
}
[SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId = "caretPosition-1"
, Justification = "We ensure the argument is greater than -1 so it can't overflow")]
public static AutoCompletionToken ParseAutoCompletionToken(string text, int caretPosition, string triggerPrefix)
{
Guard.ArgumentNotNull("text", text);
Guard.ArgumentInRange(caretPosition, 0, text.Length, "caretPosition");
if (caretPosition == 0 || text.Length == 0) return null;
// :th : 1
//:th : 0
//Hi :th : 3
int beginningOfWord = text.LastIndexOfAny(new[] { ' ', '\n' }, caretPosition - 1) + 1;
string word = text.Substring(beginningOfWord, caretPosition - beginningOfWord);
if (!word.StartsWith(triggerPrefix, StringComparison.Ordinal)) return null;
return new AutoCompletionToken(word.Substring(1), beginningOfWord);
}
Dictionary<string, IAutoCompleteSource> PrefixSourceMap { get { return prefixSourceMap.Value; } }
}
public class AutoCompletionToken
{
public AutoCompletionToken(string searchPrefix, int offset)
{
Guard.ArgumentNotNull(searchPrefix, "searchPrefix");
Guard.ArgumentNonNegative(offset, "offset");
SearchSearchPrefix = searchPrefix;
Offset = offset;
}
/// <summary>
/// Used to filter the list of auto complete suggestions to what the user has typed in.
/// </summary>
public string SearchSearchPrefix { get; private set; }
public int Offset { get; private set; }
}
}

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

@ -34,16 +34,16 @@ namespace GitHub.Services
}
}
public static PullRequestStateEnum FromGraphQl(this PullRequestState value)
public static Models.PullRequestState FromGraphQl(this Octokit.GraphQL.Model.PullRequestState value)
{
switch (value)
{
case PullRequestState.Open:
return PullRequestStateEnum.Open;
case PullRequestState.Closed:
return PullRequestStateEnum.Closed;
case PullRequestState.Merged:
return PullRequestStateEnum.Merged;
case Octokit.GraphQL.Model.PullRequestState.Open:
return Models.PullRequestState.Open;
case Octokit.GraphQL.Model.PullRequestState.Closed:
return Models.PullRequestState.Closed;
case Octokit.GraphQL.Model.PullRequestState.Merged:
return Models.PullRequestState.Merged;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}

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

@ -17,7 +17,6 @@ namespace GitHub.Services
[PartCreationPolicy(CreationPolicy.Shared)]
public class GitClient : IGitClient
{
const string defaultOriginName = "origin";
static readonly ILogger log = LogManager.ForContext<GitClient>();
readonly IGitService gitService;
readonly PullOptions pullOptions;
@ -44,12 +43,17 @@ namespace GitHub.Services
public Task Pull(IRepository repository)
{
Guard.ArgumentNotNull(repository, nameof(repository));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
var signature = repository.Config.BuildSignature(DateTimeOffset.UtcNow);
#pragma warning disable 0618 // TODO: Replace `Network.Pull` with `Commands.Pull`.
repository.Network.Pull(signature, pullOptions);
#pragma warning restore 0618
if (repository is Repository repo)
{
LibGit2Sharp.Commands.Pull(repo, signature, pullOptions);
}
else
{
log.Error("Couldn't pull because {Variable} isn't an instance of {Type}", nameof(repository), typeof(Repository));
}
});
}
@ -59,7 +63,7 @@ namespace GitHub.Services
Guard.ArgumentNotEmptyString(branchName, nameof(branchName));
Guard.ArgumentNotEmptyString(remoteName, nameof(remoteName));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
if (repository.Head?.Commits != null && repository.Head.Commits.Any())
{
@ -75,14 +79,11 @@ namespace GitHub.Services
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(remoteName, nameof(remoteName));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
try
{
var remote = repository.Network.Remotes[remoteName];
#pragma warning disable 0618 // TODO: Replace `Network.Fetch` with `Commands.Fetch`.
repository.Network.Fetch(remote, fetchOptions);
#pragma warning restore 0618
repository.Network.Fetch(remoteName, new[] { "+refs/heads/*:refs/remotes/origin/*" }, fetchOptions);
}
catch (Exception ex)
{
@ -104,7 +105,7 @@ namespace GitHub.Services
}
}
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
try
{
@ -114,17 +115,15 @@ namespace GitHub.Services
var removeRemote = false;
if (repo.Network.Remotes[remoteName] != null)
{
// If a remote with this neme already exists, use a unique name and remove remote afterwards
// If a remote with this name already exists, use a unique name and remove remote afterwards
remoteName = cloneUrl.Owner + "-" + Guid.NewGuid();
removeRemote = true;
}
var remote = repo.Network.Remotes.Add(remoteName, remoteUri.ToString());
repo.Network.Remotes.Add(remoteName, remoteUri.ToString());
try
{
#pragma warning disable 0618 // TODO: Replace `Network.Fetch` with `Commands.Fetch`.
repo.Network.Fetch(remote, refspecs, fetchOptions);
#pragma warning restore 0618
repo.Network.Fetch(remoteName, refspecs, fetchOptions);
}
finally
{
@ -149,14 +148,11 @@ namespace GitHub.Services
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(remoteName, nameof(remoteName));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
try
{
var remote = repository.Network.Remotes[remoteName];
#pragma warning disable 0618 // TODO: Replace `Network.Fetch` with `Commands.Fetch`.
repository.Network.Fetch(remote, refspecs, fetchOptions);
#pragma warning restore 0618
repository.Network.Fetch(remoteName, refspecs, fetchOptions);
}
catch (Exception ex)
{
@ -173,121 +169,41 @@ namespace GitHub.Services
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(branchName, nameof(branchName));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
#pragma warning disable 0618 // TODO: Replace `IRepository.Checkout` with `Commands.Checkout`.
repository.Checkout(branchName);
#pragma warning restore 0618
if (repository is Repository repo)
{
LibGit2Sharp.Commands.Checkout(repo, branchName);
}
else
{
log.Error("Couldn't checkout because {Variable} isn't an instance of {Type}", nameof(repository), typeof(Repository));
}
});
}
public async Task<bool> CommitExists(IRepository repository, string sha)
{
return await Task.Run(() => repository.Lookup<Commit>(sha) != null).ConfigureAwait(false);
}
public Task CreateBranch(IRepository repository, string branchName)
{
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(branchName, nameof(branchName));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
repository.CreateBranch(branchName);
});
}
public Task<TreeChanges> Compare(
IRepository repository,
string sha1,
string sha2,
bool detectRenames)
{
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(sha1, nameof(sha1));
Guard.ArgumentNotEmptyString(sha2, nameof(sha2));
return Task.Factory.StartNew(() =>
{
var options = new CompareOptions
{
Similarity = detectRenames ? SimilarityOptions.Renames : SimilarityOptions.None
};
var commit1 = repository.Lookup<Commit>(sha1);
var commit2 = repository.Lookup<Commit>(sha2);
if (commit1 != null && commit2 != null)
{
return repository.Diff.Compare<TreeChanges>(commit1.Tree, commit2.Tree, options);
}
else
{
return null;
}
});
}
public Task<Patch> Compare(
IRepository repository,
string sha1,
string sha2,
string path)
{
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(sha1, nameof(sha1));
Guard.ArgumentNotEmptyString(sha2, nameof(sha2));
Guard.ArgumentNotEmptyString(path, nameof(path));
return Task.Factory.StartNew(() =>
{
var commit1 = repository.Lookup<Commit>(sha1);
var commit2 = repository.Lookup<Commit>(sha2);
if (commit1 != null && commit2 != null)
{
return repository.Diff.Compare<Patch>(
commit1.Tree,
commit2.Tree,
new[] { path });
}
else
{
return null;
}
});
}
public Task<ContentChanges> CompareWith(IRepository repository, string sha1, string sha2, string path, byte[] contents)
{
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(sha1, nameof(sha1));
Guard.ArgumentNotEmptyString(sha2, nameof(sha1));
Guard.ArgumentNotEmptyString(path, nameof(path));
return Task.Factory.StartNew(() =>
{
var commit1 = repository.Lookup<Commit>(sha1);
var commit2 = repository.Lookup<Commit>(sha2);
var treeChanges = repository.Diff.Compare<TreeChanges>(commit1.Tree, commit2.Tree);
var normalizedPath = path.Replace("/", "\\");
var renamed = treeChanges.FirstOrDefault(x => x.Path == normalizedPath);
var oldPath = renamed?.OldPath ?? path;
if (commit1 != null)
{
var contentStream = contents != null ? new MemoryStream(contents) : new MemoryStream();
var blob1 = commit1[oldPath]?.Target as Blob ?? repository.ObjectDatabase.CreateBlob(new MemoryStream());
var blob2 = repository.ObjectDatabase.CreateBlob(contentStream, path);
return repository.Diff.Compare(blob1, blob2);
}
return null;
});
}
public Task<T> GetConfig<T>(IRepository repository, string key)
{
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(key, nameof(key));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
var result = repository.Config.Get<T>(key);
return result != null ? result.Value : default(T);
@ -300,7 +216,7 @@ namespace GitHub.Services
Guard.ArgumentNotEmptyString(key, nameof(key));
Guard.ArgumentNotEmptyString(value, nameof(value));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
repository.Config.Set(key, value);
});
@ -311,7 +227,7 @@ namespace GitHub.Services
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(remoteName, nameof(remoteName));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
repository.Config.Set("remote." + remoteName + ".url", url.ToString());
repository.Config.Set("remote." + remoteName + ".fetch", "+refs/heads/*:refs/remotes/" + remoteName + "/*");
@ -324,7 +240,7 @@ namespace GitHub.Services
Guard.ArgumentNotEmptyString(branchName, nameof(branchName));
Guard.ArgumentNotEmptyString(remoteName, nameof(remoteName));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
var remoteBranchName = IsCanonical(remoteName) ? remoteName : "refs/remotes/" + remoteName + "/" + branchName;
var remoteBranch = repository.Branches[remoteBranchName];
@ -342,7 +258,7 @@ namespace GitHub.Services
{
Guard.ArgumentNotEmptyString(key, nameof(key));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
repository.Config.Unset(key);
});
@ -353,7 +269,7 @@ namespace GitHub.Services
Guard.ArgumentNotNull(repo, nameof(repo));
Guard.ArgumentNotEmptyString(remote, nameof(remote));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
var uri = gitService.GetRemoteUri(repo, remote);
var remoteName = uri.IsHypertextTransferProtocol ? remote : remote + "-http";
@ -368,9 +284,9 @@ namespace GitHub.Services
{
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(commitSha, nameof(commitSha));
Guard.ArgumentNotEmptyString(fileName, nameof(fileName));
Guard.ArgumentIsGitPath(fileName, nameof(fileName));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
var commit = repository.Lookup<Commit>(commitSha);
if (commit == null)
@ -387,9 +303,9 @@ namespace GitHub.Services
{
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(commitSha, nameof(commitSha));
Guard.ArgumentNotEmptyString(fileName, nameof(fileName));
Guard.ArgumentIsGitPath(fileName, nameof(fileName));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
var commit = repository.Lookup<Commit>(commitSha);
if (commit == null)
@ -416,9 +332,9 @@ namespace GitHub.Services
public Task<bool> IsModified(IRepository repository, string path, byte[] contents)
{
Guard.ArgumentNotNull(repository, nameof(repository));
Guard.ArgumentNotEmptyString(path, nameof(path));
Guard.ArgumentIsGitPath(path, nameof(path));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
if (repository.RetrieveStatus(path) == FileStatus.Unaltered)
{
@ -486,7 +402,7 @@ namespace GitHub.Services
{
Guard.ArgumentNotNull(repo, nameof(repo));
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
return repo.Head.TrackingDetails.AheadBy == 0;
});
@ -498,7 +414,7 @@ namespace GitHub.Services
string compareBranch,
int maxCommits)
{
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
var baseCommit = repo.Lookup<Commit>(baseBranch);
var compareCommit = repo.Lookup<Commit>(compareBranch);

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

@ -406,6 +406,10 @@ namespace GitHub.Services
/// <inheritdoc/>
public bool HasChangesInWorkingDirectory(string repositoryDir, string commitish, string path)
{
Guard.ArgumentNotNull(path, nameof(repositoryDir));
Guard.ArgumentNotNull(path, nameof(commitish));
Guard.ArgumentIsGitPath(path, nameof(path));
using (var repo = gitService.GetRepository(repositoryDir))
{
var commit = repo.Lookup<Commit>(commitish);

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

@ -0,0 +1,13 @@
using System;
using GitHub.Models;
namespace GitHub.Services
{
public interface IAutoCompleteSource
{
IObservable<AutoCompleteSuggestion> GetSuggestions();
// The prefix used to trigger auto completion.
string Prefix { get; }
}
}

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

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using GitHub.Api;
using GitHub.Factories;
using GitHub.Models;
using GitHub.Primitives;
using Octokit;
using Octokit.GraphQL;
using Octokit.GraphQL.Model;
using static Octokit.GraphQL.Variable;
namespace GitHub.Services
{
/// <summary>
/// Base class for issue and pull request services.
/// </summary>
public abstract class IssueishService : IIssueishService
{
static ICompiledQuery<CommentModel> postComment;
readonly IApiClientFactory apiClientFactory;
readonly IGraphQLClientFactory graphqlFactory;
/// <summary>
/// Initializes a new instance of the <see cref="IssueishService"/> class.
/// </summary>
/// <param name="apiClientFactory">The API client factory.</param>
/// <param name="graphqlFactory">The GraphQL client factory.</param>
public IssueishService(
IApiClientFactory apiClientFactory,
IGraphQLClientFactory graphqlFactory)
{
this.apiClientFactory = apiClientFactory;
this.graphqlFactory = graphqlFactory;
}
/// <inheritdoc/>
public async Task CloseIssueish(HostAddress address, string owner, string repository, int number)
{
var client = await apiClientFactory.CreateGitHubClient(address).ConfigureAwait(false);
var update = new IssueUpdate { State = ItemState.Closed };
await client.Issue.Update(owner, repository, number, update).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task ReopenIssueish(HostAddress address, string owner, string repository, int number)
{
var client = await apiClientFactory.CreateGitHubClient(address).ConfigureAwait(false);
var update = new IssueUpdate { State = ItemState.Open };
await client.Issue.Update(owner, repository, number, update).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<CommentModel> PostComment(HostAddress address, string issueishId, string body)
{
var input = new AddCommentInput
{
Body = body,
SubjectId = new ID(issueishId),
};
if (postComment == null)
{
postComment = new Mutation()
.AddComment(Var(nameof(input)))
.CommentEdge
.Node
.Select(comment => new CommentModel
{
Author = new ActorModel
{
Login = comment.Author.Login,
AvatarUrl = comment.Author.AvatarUrl(null),
},
Body = comment.Body,
CreatedAt = comment.CreatedAt,
DatabaseId = comment.DatabaseId.Value,
Id = comment.Id.Value,
Url = comment.Url,
}).Compile();
}
var vars = new Dictionary<string, object>
{
{ nameof(input), input },
};
var graphql = await graphqlFactory.CreateConnection(address).ConfigureAwait(false);
return await graphql.Run(postComment, vars).ConfigureAwait(false);
}
public async Task DeleteComment(
HostAddress address,
string owner,
string repository,
int commentId)
{
var client = await apiClientFactory.CreateGitHubClient(address).ConfigureAwait(false);
await client.Issue.Comment.Delete(owner, repository, commentId).ConfigureAwait(false);
}
public async Task EditComment(
HostAddress address,
string owner,
string repository,
int commentId,
string body)
{
var client = await apiClientFactory.CreateGitHubClient(address).ConfigureAwait(false);
await client.Issue.Comment.Update(owner, repository, commentId, body).ConfigureAwait(false);
}
}
}

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

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Reactive.Linq;
using GitHub.Api;
using GitHub.Extensions;
using GitHub.Models;
using GitHub.Primitives;
using Octokit.GraphQL;
using Octokit.GraphQL.Model;
using static Octokit.GraphQL.Variable;
namespace GitHub.Services
{
[Export(typeof(IAutoCompleteSource))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class IssuesAutoCompleteSource : IAutoCompleteSource
{
readonly ITeamExplorerContext teamExplorerContext;
readonly IGraphQLClientFactory graphqlFactory;
ICompiledQuery<Page<SuggestionItem>> query;
[ImportingConstructor]
public IssuesAutoCompleteSource(ITeamExplorerContext teamExplorerContext, IGraphQLClientFactory graphqlFactory)
{
Guard.ArgumentNotNull(teamExplorerContext, nameof(teamExplorerContext));
Guard.ArgumentNotNull(graphqlFactory, nameof(graphqlFactory));
this.teamExplorerContext = teamExplorerContext;
this.graphqlFactory = graphqlFactory;
}
public IObservable<AutoCompleteSuggestion> GetSuggestions()
{
var localRepositoryModel = teamExplorerContext.ActiveRepository;
var hostAddress = HostAddress.Create(localRepositoryModel.CloneUrl.Host);
var owner = localRepositoryModel.Owner;
var name = localRepositoryModel.Name;
string filter;
string after;
if (query == null)
{
query = new Query().Search(query: Var(nameof(filter)), SearchType.Issue, 100, after: Var(nameof(after)))
.Select(item => new Page<SuggestionItem>
{
Items = item.Nodes.Select(searchResultItem =>
searchResultItem.Switch<SuggestionItem>(selector => selector
.Issue(i => new SuggestionItem("#" + i.Number, i.Title) { LastModifiedDate = i.LastEditedAt })
.PullRequest(p => new SuggestionItem("#" + p.Number, p.Title) { LastModifiedDate = p.LastEditedAt }))
).ToList(),
EndCursor = item.PageInfo.EndCursor,
HasNextPage = item.PageInfo.HasNextPage,
TotalCount = item.IssueCount
})
.Compile();
}
filter = $"repo:{owner}/{name}";
return Observable.FromAsync(async () =>
{
var results = new List<SuggestionItem>();
var variables = new Dictionary<string, object>
{
{nameof(filter), filter },
};
var connection = await graphqlFactory.CreateConnection(hostAddress);
var searchResults = await connection.Run(query, variables);
results.AddRange(searchResults.Items);
while (searchResults.HasNextPage)
{
variables[nameof(after)] = searchResults.EndCursor;
searchResults = await connection.Run(query, variables);
results.AddRange(searchResults.Items);
}
return results.Select(item => new IssueAutoCompleteSuggestion(item, Prefix));
}).SelectMany(observable => observable);
}
class SearchResult
{
public SuggestionItem SuggestionItem { get; set; }
}
public string Prefix
{
get { return "#"; }
}
class IssueAutoCompleteSuggestion : AutoCompleteSuggestion
{
// Just needs to be some value before GitHub stored its first issue.
static readonly DateTimeOffset lowerBound = new DateTimeOffset(2000, 1, 1, 12, 0, 0, TimeSpan.FromSeconds(0));
readonly SuggestionItem suggestion;
public IssueAutoCompleteSuggestion(SuggestionItem suggestion, string prefix)
: base(suggestion.Name, suggestion.Description, prefix)
{
this.suggestion = suggestion;
}
public override int GetSortRank(string text)
{
// We need to override the sort rank behavior because when we display issues, we include the prefix
// unlike mentions. So we need to account for that in how we do filtering.
if (text.Length == 0)
{
return (int) ((suggestion.LastModifiedDate ?? lowerBound) - lowerBound).TotalSeconds;
}
// Name is always "#" followed by issue number.
return Name.StartsWith("#" + text, StringComparison.OrdinalIgnoreCase)
? 1
: DescriptionWords.Any(word => word.StartsWith(text, StringComparison.OrdinalIgnoreCase))
? 0
: -1;
}
// This is what gets "completed" when you tab.
public override string ToString()
{
return Name;
}
}
}
}

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

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Media.Imaging;
using GitHub.Api;
using GitHub.Extensions;
using GitHub.Models;
using GitHub.Primitives;
using Octokit.GraphQL;
using static Octokit.GraphQL.Variable;
namespace GitHub.Services
{
/// <summary>
/// Supplies @mentions auto complete suggestions.
/// </summary>
[Export(typeof(IAutoCompleteSource))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class MentionsAutoCompleteSource : IAutoCompleteSource
{
const string DefaultAvatar = "pack://application:,,,/GitHub.App;component/Images/default_user_avatar.png";
readonly ITeamExplorerContext teamExplorerContext;
readonly IGraphQLClientFactory graphqlFactory;
readonly IAvatarProvider avatarProvider;
ICompiledQuery<List<SuggestionItem>> query;
[ImportingConstructor]
public MentionsAutoCompleteSource(
ITeamExplorerContext teamExplorerContext,
IGraphQLClientFactory graphqlFactory,
IAvatarProvider avatarProvider)
{
Guard.ArgumentNotNull(teamExplorerContext, nameof(teamExplorerContext));
Guard.ArgumentNotNull(graphqlFactory, nameof(graphqlFactory));
Guard.ArgumentNotNull(avatarProvider, nameof(avatarProvider));
this.teamExplorerContext = teamExplorerContext;
this.graphqlFactory = graphqlFactory;
this.avatarProvider = avatarProvider;
}
public IObservable<AutoCompleteSuggestion> GetSuggestions()
{
var localRepositoryModel = teamExplorerContext.ActiveRepository;
var hostAddress = HostAddress.Create(localRepositoryModel.CloneUrl.Host);
var owner = localRepositoryModel.Owner;
var name = localRepositoryModel.Name;
if (query == null)
{
query = new Query().Repository(owner: Var(nameof(owner)), name: Var(nameof(name)))
.Select(repository =>
repository.MentionableUsers(null, null, null, null)
.AllPages()
.Select(sourceItem =>
new SuggestionItem(sourceItem.Login,
sourceItem.Name ?? "(unknown)",
sourceItem.AvatarUrl(null)))
.ToList())
.Compile();
}
var variables = new Dictionary<string, object>
{
{nameof(owner), owner },
{nameof(name), name },
};
return Observable.FromAsync(async () =>
{
var connection = await graphqlFactory.CreateConnection(hostAddress);
var suggestions = await connection.Run(query, variables);
return suggestions.Select(suggestion => new AutoCompleteSuggestion(suggestion.Name,
suggestion.Description,
ResolveImage(suggestion),
Prefix));
}).SelectMany(enumerable => enumerable);
}
IObservable<BitmapSource> ResolveImage(SuggestionItem uri)
{
if (uri.ImageUrl != null)
{
return avatarProvider.GetAvatar(uri.ImageUrl);
}
return Observable.Return(AvatarProvider.CreateBitmapImage(DefaultAvatar));
}
public string Prefix => "@";
}
}

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

@ -390,7 +390,7 @@ namespace GitHub.Services
Head = Create(prCacheItem.Head),
State = prCacheItem.State.HasValue ?
prCacheItem.State.Value :
prCacheItem.IsOpen.Value ? PullRequestStateEnum.Open : PullRequestStateEnum.Closed,
prCacheItem.IsOpen.Value ? PullRequestState.Open : PullRequestState.Closed,
};
}
@ -524,25 +524,25 @@ namespace GitHub.Services
public string Body { get; set; }
// Nullable for compatibility with old caches.
public PullRequestStateEnum? State { get; set; }
public PullRequestState? State { get; set; }
// This fields exists only for compatibility with old caches. The State property should be used.
public bool? IsOpen { get; set; }
public bool? Merged { get; set; }
static PullRequestStateEnum GetState(PullRequest pullRequest)
static PullRequestState GetState(PullRequest pullRequest)
{
if (pullRequest.State == ItemState.Open)
{
return PullRequestStateEnum.Open;
return PullRequestState.Open;
}
else if (pullRequest.Merged)
{
return PullRequestStateEnum.Merged;
return PullRequestState.Merged;
}
else
{
return PullRequestStateEnum.Closed;
return PullRequestState.Closed;
}
}
}

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

@ -17,6 +17,7 @@ using System.Windows.Forms;
using GitHub.Api;
using GitHub.App.Services;
using GitHub.Extensions;
using GitHub.Factories;
using GitHub.Logging;
using GitHub.Models;
using GitHub.Primitives;
@ -35,7 +36,7 @@ namespace GitHub.Services
{
[Export(typeof(IPullRequestService))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class PullRequestService : IPullRequestService, IStaticReviewFileMap
public class PullRequestService : IssueishService, IPullRequestService, IStaticReviewFileMap
{
const string SettingCreatedByGHfVS = "created-by-ghfvs";
const string SettingGHfVSPullRequest = "ghfvs-pr-owner-number";
@ -62,15 +63,17 @@ namespace GitHub.Services
readonly IUsageTracker usageTracker;
readonly IDictionary<string, (string commitId, string repoPath)> tempFileMappings;
[ImportingConstructor]
public PullRequestService(
IGitClient gitClient,
IGitService gitService,
IVSGitExt gitExt,
IApiClientFactory apiClientFactory,
IGraphQLClientFactory graphqlFactory,
IOperatingSystem os,
IUsageTracker usageTracker)
: base(apiClientFactory, graphqlFactory)
{
this.gitClient = gitClient;
this.gitService = gitService;
@ -86,7 +89,7 @@ namespace GitHub.Services
string owner,
string name,
string after,
PullRequestStateEnum[] states)
Models.PullRequestState[] states)
{
ICompiledQuery<Page<PullRequestListItemModel>> query;
@ -212,10 +215,11 @@ namespace GitHub.Services
{ nameof(owner), owner },
{ nameof(name), name },
{ nameof(after), after },
{ nameof(states), states.Select(x => (PullRequestState)x).ToList() },
{ nameof(states), states.Select(x => (Octokit.GraphQL.Model.PullRequestState)x).ToList() },
};
var result = await graphql.Run(query, vars);
var region = owner + '/' + name + "/pr-list";
var result = await graphql.Run(query, vars, regionName: region);
foreach (var item in result.Items.Cast<ListItemAdapter>())
{
@ -290,6 +294,14 @@ namespace GitHub.Services
return result;
}
public async Task ClearPullRequestsCache(HostAddress address, string owner, string name)
{
var region = owner + '/' + name + "/pr-list";
var graphql = await graphqlFactory.CreateConnection(address);
await graphql.ClearCache(region);
}
public async Task<Page<ActorModel>> ReadAssignableUsers(
HostAddress address,
string owner,
@ -322,7 +334,7 @@ namespace GitHub.Services
{ nameof(after), after },
};
return await graphql.Run(readAssignableUsers, vars);
return await graphql.Run(readAssignableUsers, vars, cacheDuration: TimeSpan.FromHours(1));
}
public IObservable<IPullRequestModel> CreatePullRequest(IModelService modelService,
@ -576,6 +588,21 @@ namespace GitHub.Services
});
}
public async Task<bool> FetchCommit(LocalRepositoryModel localRepository, RepositoryModel remoteRepository, string sha)
{
using (var repo = gitService.GetRepository(localRepository.LocalPath))
{
if (!await gitClient.CommitExists(repo, sha).ConfigureAwait(false))
{
var remote = await CreateRemote(repo, remoteRepository.CloneUrl).ConfigureAwait(false);
await gitClient.Fetch(repo, remote).ConfigureAwait(false);
return await gitClient.CommitExists(repo, sha).ConfigureAwait(false);
}
return true;
}
}
public IObservable<string> GetDefaultLocalBranchName(LocalRepositoryModel repository, int pullRequestNumber, string pullRequestTitle)
{
return Observable.Defer(() =>
@ -638,7 +665,7 @@ namespace GitHub.Services
{
var remote = await gitClient.GetHttpRemote(repo, "origin");
await gitClient.Fetch(repo, remote.Name);
var changes = await gitClient.Compare(repo, pullRequest.BaseRefSha, pullRequest.HeadRefSha, detectRenames: true);
var changes = await gitService.Compare(repo, pullRequest.BaseRefSha, pullRequest.HeadRefSha, detectRenames: true);
return Observable.Return(changes);
}
});
@ -752,20 +779,20 @@ namespace GitHub.Services
Encoding encoding)
{
var tempFilePath = CalculateTempFileName(relativePath, commitSha, encoding);
var gitPath = relativePath.TrimStart('/').Replace('\\', '/');
if (!File.Exists(tempFilePath))
{
using (var repo = gitService.GetRepository(repository.LocalPath))
{
var remote = await gitClient.GetHttpRemote(repo, "origin");
await ExtractToTempFile(repo, pullRequest.Number, commitSha, relativePath, encoding, tempFilePath);
await ExtractToTempFile(repo, pullRequest.Number, commitSha, gitPath, encoding, tempFilePath);
}
}
lock (this.tempFileMappings)
lock (tempFileMappings)
{
string gitRelativePath = relativePath.TrimStart('/').Replace('\\', '/');
this.tempFileMappings[CanonicalizeLocalFilePath(tempFilePath)] = (commitSha, gitRelativePath);
tempFileMappings[CanonicalizeLocalFilePath(tempFilePath)] = (commitSha, gitPath);
}
return tempFilePath;
@ -895,22 +922,24 @@ namespace GitHub.Services
IRepository repo,
int pullRequestNumber,
string commitSha,
string relativePath,
string path,
Encoding encoding,
string tempFilePath)
{
Guard.ArgumentIsGitPath(path, nameof(path));
string contents;
try
{
contents = await gitClient.ExtractFile(repo, commitSha, relativePath) ?? string.Empty;
contents = await gitClient.ExtractFile(repo, commitSha, path) ?? string.Empty;
}
catch (FileNotFoundException)
{
var pullHeadRef = $"refs/pull/{pullRequestNumber}/head";
var remote = await gitClient.GetHttpRemote(repo, "origin");
await gitClient.Fetch(repo, remote.Name, commitSha, pullHeadRef);
contents = await gitClient.ExtractFile(repo, commitSha, relativePath) ?? string.Empty;
contents = await gitClient.ExtractFile(repo, commitSha, path) ?? string.Empty;
}
Directory.CreateDirectory(Path.GetDirectoryName(tempFilePath));

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

@ -64,7 +64,7 @@ namespace GitHub.Services
}
/// <inheritdoc/>
public async Task<ViewerRepositoriesModel> ReadViewerRepositories(HostAddress address)
public async Task<ViewerRepositoriesModel> ReadViewerRepositories(HostAddress address, bool refresh = false)
{
if (readViewerRepositories == null)
{
@ -107,7 +107,7 @@ namespace GitHub.Services
}
var graphql = await graphqlFactory.CreateConnection(address).ConfigureAwait(false);
var result = await graphql.Run(readViewerRepositories).ConfigureAwait(false);
var result = await graphql.Run(readViewerRepositories, cacheDuration: TimeSpan.FromHours(1), refresh: refresh).ConfigureAwait(false);
return result;
}
@ -129,7 +129,7 @@ namespace GitHub.Services
var repositoryUrl = url.ToRepositoryUrl();
var isDotCom = HostAddress.IsGitHubDotComUri(repositoryUrl);
if (DestinationDirectoryExists(repositoryPath))
if (DestinationDirectoryExists(repositoryPath) && !DestinationDirectoryEmpty(repositoryPath))
{
if (!IsSolutionInRepository(repositoryPath))
{
@ -206,9 +206,12 @@ namespace GitHub.Services
// Switch to a thread pool thread for IO then back to the main thread to call
// vsGitServices.Clone() as this must be called on the main thread.
await ThreadingHelper.SwitchToPoolThreadAsync();
operatingSystem.Directory.CreateDirectory(repositoryPath);
await ThreadingHelper.SwitchToMainThreadAsync();
if (!DestinationDirectoryExists(repositoryPath))
{
await ThreadingHelper.SwitchToPoolThreadAsync();
operatingSystem.Directory.CreateDirectory(repositoryPath);
await ThreadingHelper.SwitchToMainThreadAsync();
}
try
{
@ -224,6 +227,7 @@ namespace GitHub.Services
catch (Exception ex)
{
log.Error(ex, "Could not clone {CloneUrl} to {Path}", cloneUrl, repositoryPath);
operatingSystem.Directory.DeleteDirectory(repositoryPath);
throw;
}
}
@ -231,6 +235,9 @@ namespace GitHub.Services
/// <inheritdoc/>
public bool DestinationDirectoryExists(string path) => operatingSystem.Directory.DirectoryExists(path);
/// <inheritdoc/>
public bool DestinationDirectoryEmpty(string path) => operatingSystem.Directory.GetDirectory(path).IsEmpty;
/// <inheritdoc/>
public bool DestinationFileExists(string path) => operatingSystem.File.Exists(path);

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

@ -18,7 +18,6 @@ namespace GitHub.ViewModels
/// </summary>
public abstract class CommentThreadViewModel : ReactiveObject, ICommentThreadViewModel
{
readonly ReactiveList<ICommentViewModel> comments = new ReactiveList<ICommentViewModel>();
readonly Dictionary<ICommentViewModel, IObserver<ICommentViewModel>> draftThrottles =
new Dictionary<ICommentViewModel, IObserver<ICommentViewModel>>();
readonly IScheduler timerScheduler;
@ -51,15 +50,9 @@ namespace GitHub.ViewModels
this.timerScheduler = timerScheduler;
}
/// <inheritdoc/>
public IReactiveList<ICommentViewModel> Comments => comments;
/// <inheritdoc/>
public IActorViewModel CurrentUser { get; private set; }
/// <inheritdoc/>
IReadOnlyReactiveList<ICommentViewModel> ICommentThreadViewModel.Comments => comments;
protected IMessageDraftStore DraftStore { get; }
/// <inheritdoc/>
@ -72,15 +65,13 @@ namespace GitHub.ViewModels
public abstract Task DeleteComment(ICommentViewModel comment);
/// <summary>
/// Adds a placeholder comment that will allow the user to enter a reply, and wires up
/// Initializes a placeholder comment that will allow the user to enter a reply, and wires up
/// event listeners for saving drafts.
/// </summary>
/// <param name="placeholder">The placeholder comment view model.</param>
/// <returns>An object which when disposed will remove the event listeners.</returns>
protected IDisposable AddPlaceholder(ICommentViewModel placeholder)
protected IDisposable InitializePlaceholder(ICommentViewModel placeholder)
{
Comments.Add(placeholder);
return placeholder.WhenAnyValue(
x => x.EditState,
x => x.Body,

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

@ -1,4 +1,5 @@
using System;
using System.ComponentModel.Composition;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
@ -13,13 +14,17 @@ using Serilog;
namespace GitHub.ViewModels
{
/// <summary>
/// Base view model for an issue or pull request comment.
/// An issue or pull request comment.
/// </summary>
public abstract class CommentViewModel : ReactiveObject, ICommentViewModel
[Export(typeof(ICommentViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class CommentViewModel : ViewModelBase, ICommentViewModel
{
static readonly ILogger log = LogManager.ForContext<CommentViewModel>();
readonly ICommentService commentService;
readonly ObservableAsPropertyHelper<bool> canCancel;
readonly ObservableAsPropertyHelper<bool> canDelete;
ObservableAsPropertyHelper<string> commitCaption;
string id;
IActorViewModel author;
IActorViewModel currentUser;
@ -36,10 +41,14 @@ namespace GitHub.ViewModels
/// Initializes a new instance of the <see cref="CommentViewModel"/> class.
/// </summary>
/// <param name="commentService">The comment service.</param>
public CommentViewModel(ICommentService commentService)
/// <param name="autoCompleteAdvisor">The auto complete advisor.</param>
[ImportingConstructor]
public CommentViewModel(ICommentService commentService, IAutoCompleteAdvisor autoCompleteAdvisor)
{
Guard.ArgumentNotNull(commentService, nameof(commentService));
Guard.ArgumentNotNull(autoCompleteAdvisor, nameof(autoCompleteAdvisor));
AutoCompleteAdvisor = autoCompleteAdvisor;
this.commentService = commentService;
var canDeleteObservable = this.WhenAnyValue(
@ -70,6 +79,9 @@ namespace GitHub.ViewModels
(ro, body) => !ro && !string.IsNullOrWhiteSpace(body)));
AddErrorHandler(CommitEdit);
canCancel = this.WhenAnyValue(x => x.Id)
.Select(id => id != null)
.ToProperty(this, x => x.CanCancel);
CancelEdit = ReactiveCommand.Create(DoCancelEdit, CommitEdit.IsExecuting.Select(x => !x));
AddErrorHandler(CancelEdit);
@ -140,6 +152,9 @@ namespace GitHub.ViewModels
protected set => this.RaiseAndSetIfChanged(ref isSubmitting, value);
}
/// <inheritdoc/>
public bool CanCancel => canCancel.Value;
/// <inheritdoc/>
public bool CanDelete => canDelete.Value;
@ -150,6 +165,9 @@ namespace GitHub.ViewModels
private set => this.RaiseAndSetIfChanged(ref createdAt, value);
}
/// <inheritdoc/>
public string CommitCaption => commitCaption.Value;
/// <inheritdoc/>
public ICommentThreadViewModel Thread
{
@ -175,14 +193,11 @@ namespace GitHub.ViewModels
/// <inheritdoc/>
public ReactiveCommand<Unit, Unit> Delete { get; }
/// <summary>
/// Initializes the view model with data.
/// </summary>
/// <param name="thread">The thread that the comment is a part of.</param>
/// <param name="currentUser">The current user.</param>
/// <param name="comment">The comment model. May be null.</param>
/// <param name="state">The comment edit state.</param>
protected Task InitializeAsync(
/// <inheritdoc/>
public IAutoCompleteAdvisor AutoCompleteAdvisor { get; }
/// <inheritdoc/>
public Task InitializeAsync(
ICommentThreadViewModel thread,
ActorModel currentUser,
CommentModel comment,
@ -195,13 +210,15 @@ namespace GitHub.ViewModels
CurrentUser = new ActorViewModel(currentUser);
Id = comment?.Id;
DatabaseId = comment?.DatabaseId ?? 0;
PullRequestId = comment?.PullRequestId ?? 0;
PullRequestId = (comment as PullRequestReviewCommentModel)?.PullRequestId ?? 0;
Body = comment?.Body;
EditState = state;
Author = comment != null ? new ActorViewModel(comment.Author) : CurrentUser;
CreatedAt = comment?.CreatedAt ?? DateTimeOffset.MinValue;
WebUrl = comment?.Url != null ? new Uri(comment.Url) : null;
commitCaption = GetCommitCaptionObservable().ToProperty(this, x => x.CommitCaption);
return Task.CompletedTask;
}
@ -210,6 +227,12 @@ namespace GitHub.ViewModels
command.ThrownExceptions.Subscribe(x => ErrorMessage = x.Message);
}
protected virtual IObservable<string> GetCommitCaptionObservable()
{
return this.WhenAnyValue(x => x.Id)
.Select(x => x == null ? Resources.Comment : Resources.UpdateComment);
}
async Task DoDelete()
{
if (commentService.ConfirmCommentDelete())

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

@ -0,0 +1,19 @@
using GitHub.Models;
namespace GitHub.ViewModels
{
public class CommitActorViewModel: ActorViewModel, ICommitActorViewModel
{
public CommitActorViewModel(CommitActorModel model)
:base(model.User)
{
Name = model.Name;
Email = model.Email;
HasLogin = model.User != null;
}
public string Email { get; }
public string Name { get; }
public bool HasLogin { get; }
}
}

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

@ -67,11 +67,13 @@ namespace GitHub.ViewModels.Dialog.Clone
var canClone = Observable.CombineLatest(
repository, this.WhenAnyValue(x => x.Path),
(repo, path) => repo != null && !service.DestinationFileExists(path) && !service.DestinationDirectoryExists(path));
(repo, path) => repo != null && !service.DestinationFileExists(path) &&
(!service.DestinationDirectoryExists(path) || service.DestinationDirectoryEmpty(path)));
var canOpen = Observable.CombineLatest(
repository, this.WhenAnyValue(x => x.Path),
(repo, path) => repo != null && !service.DestinationFileExists(path) && service.DestinationDirectoryExists(path));
(repo, path) => repo != null && !service.DestinationFileExists(path) && service.DestinationDirectoryExists(path)
&& !service.DestinationDirectoryEmpty(path));
Browse = ReactiveCommand.Create(() => BrowseForDirectory());
Clone = ReactiveCommand.CreateFromObservable(
@ -236,13 +238,13 @@ namespace GitHub.ViewModels.Dialog.Clone
return Resources.DestinationAlreadyExists;
}
if (service.DestinationDirectoryExists(path))
if (service.DestinationDirectoryExists(path) && !service.DestinationDirectoryEmpty(path))
{
using (var repository = gitService.GetRepository(path))
{
if (repository == null)
{
return Resources.CantFindARepositoryAtLocalPath;
return Resources.DirectoryAtDestinationNotEmpty;
}
var localUrl = gitService.GetRemoteUri(repository)?.ToRepositoryUrl();
@ -254,7 +256,8 @@ namespace GitHub.ViewModels.Dialog.Clone
var targetUrl = repositoryModel.CloneUrl?.ToRepositoryUrl();
if (localUrl != targetUrl)
{
return string.Format(CultureInfo.CurrentCulture, Resources.LocalRepositoryHasARemoteOf, localUrl);
return string.Format(CultureInfo.CurrentCulture, Resources.LocalRepositoryHasARemoteOf,
localUrl);
}
return Resources.YouHaveAlreadyClonedToThisLocation;

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

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Globalization;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Windows.Data;
@ -30,7 +31,6 @@ namespace GitHub.ViewModels.Dialog.Clone
string filter;
bool isEnabled;
bool isLoading;
bool loadingStarted;
IReadOnlyList<IRepositoryItemViewModel> items;
ICollectionView itemsView;
ObservableAsPropertyHelper<RepositoryModel> repository;
@ -113,26 +113,47 @@ namespace GitHub.ViewModels.Dialog.Clone
public async Task Activate()
{
if (connection == null || loadingStarted) return;
await this.LoadItems(true);
}
static string GroupName(KeyValuePair<string, IReadOnlyList<RepositoryListItemModel>> group, int max)
{
var name = group.Key;
if (group.Value.Count == max)
{
name += $" ({string.Format(CultureInfo.InvariantCulture, Resources.MostRecentlyPushed, max)})";
}
return name;
}
async Task LoadItems(bool refresh)
{
if (connection == null && !IsLoading) return;
Error = null;
IsLoading = true;
loadingStarted = true;
try
{
if (refresh)
{
Items = new List<IRepositoryItemViewModel>();
ItemsView = CollectionViewSource.GetDefaultView(Items);
}
var results = await log.TimeAsync(nameof(service.ReadViewerRepositories),
() => service.ReadViewerRepositories(connection.HostAddress));
() => service.ReadViewerRepositories(connection.HostAddress, refresh));
var yourRepositories = results.Repositories
.Where(r => r.Owner == results.Owner)
.Select(x => new RepositoryItemViewModel(x, "Your repositories"));
.Select(x => new RepositoryItemViewModel(x, Resources.RepositorySelectYourRepositories));
var collaboratorRepositories = results.Repositories
.Where(r => r.Owner != results.Owner)
.OrderBy(r => r.Owner)
.Select(x => new RepositoryItemViewModel(x, "Collaborator repositories"));
.Select(x => new RepositoryItemViewModel(x, Resources.RepositorySelectCollaboratorRepositories));
var repositoriesContributedTo = results.ContributedToRepositories
.Select(x => new RepositoryItemViewModel(x, "Contributed to repositories"));
.Select(x => new RepositoryItemViewModel(x, Resources.RepositorySelectContributedRepositories));
var orgRepositories = results.Organizations
.OrderBy(x => x.Key)
.SelectMany(x => x.Value.Select(y => new RepositoryItemViewModel(y, GroupName(x, 100))));
@ -163,29 +184,25 @@ namespace GitHub.ViewModels.Dialog.Clone
}
}
static string GroupName(KeyValuePair<string, IReadOnlyList<RepositoryListItemModel>> group, int max)
{
var name = group.Key;
if (group.Value.Count == max)
{
name += $" ({string.Format(CultureInfo.InvariantCulture, Resources.MostRecentlyPushed, max)})";
}
return name;
}
bool FilterItem(object obj)
{
if (obj is IRepositoryItemViewModel item && !string.IsNullOrWhiteSpace(Filter))
{
var urlString = item.Url.ToString();
var urlStringWithGit = urlString + ".git";
var urlStringWithSlash = urlString + "/";
return
item.Caption.Contains(Filter, StringComparison.CurrentCultureIgnoreCase) ||
urlString.Contains(Filter, StringComparison.OrdinalIgnoreCase) ||
urlStringWithGit.Contains(Filter, StringComparison.OrdinalIgnoreCase) ||
urlStringWithSlash.Contains(Filter, StringComparison.OrdinalIgnoreCase);
if (new UriString(Filter).IsHypertextTransferProtocol)
{
var urlString = item.Url.ToString();
var urlStringWithGit = urlString + ".git";
var urlStringWithSlash = urlString + "/";
return
urlString.Contains(Filter, StringComparison.OrdinalIgnoreCase) ||
urlStringWithGit.Contains(Filter, StringComparison.OrdinalIgnoreCase) ||
urlStringWithSlash.Contains(Filter, StringComparison.OrdinalIgnoreCase);
}
else
{
return
item.Caption.Contains(Filter, StringComparison.CurrentCultureIgnoreCase);
}
}
return true;

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

@ -90,9 +90,6 @@ namespace GitHub.ViewModels.Dialog
return parsedReference != repoName ? String.Format(CultureInfo.CurrentCulture, Resources.SafeRepositoryNameWarning, parsedReference) : null;
});
this.WhenAny(x => x.BaseRepositoryPathValidator.ValidationResult, x => x.Value)
.Subscribe();
CreateRepository = InitializeCreateRepositoryCommand();
isCreating = CreateRepository.IsExecuting

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

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Text;
namespace GitHub.ViewModels.Documents
{
/// <summary>
/// Displays a list of commit summaries in a pull request timeline.
/// </summary>
[Export(typeof(ICommitListViewModel))]
public class CommitListViewModel : ViewModelBase, ICommitListViewModel
{
/// <summary>
/// Initializes a new instance of the <see cref="CommitListViewModel"/> class.
/// </summary>
/// <param name="commits">The commits to display.</param>
public CommitListViewModel(params ICommitSummaryViewModel[] commits)
{
if (commits.Length == 0)
{
throw new NotSupportedException("Cannot create a CommitListViewModel with 0 commits.");
}
Commits = commits;
Author = Commits[0].Author;
AuthorName = GetAuthorDisplayName(Commits[0].Author);
AuthorCaption = BuildAuthorCaption();
}
/// <summary>
/// Initializes a new instance of the <see cref="CommitListViewModel"/> class.
/// </summary>
/// <param name="commits">The commits to display.</param>
public CommitListViewModel(IEnumerable<ICommitSummaryViewModel> commits)
{
Commits = commits.ToList();
if (Commits.Count == 0)
{
throw new NotSupportedException("Cannot create a CommitListViewModel with 0 commits.");
}
Author = Commits[0].Author;
AuthorName = GetAuthorDisplayName(Commits[0].Author);
AuthorCaption = BuildAuthorCaption();
}
/// <inheritdoc/>
public ICommitActorViewModel Author { get; }
/// <inheritdoc/>
public string AuthorName { get; }
/// <inheritdoc/>
public string AuthorCaption { get; }
/// <inheritdoc/>
public IReadOnlyList<ICommitSummaryViewModel> Commits { get; }
string BuildAuthorCaption()
{
var result = new StringBuilder();
if (Commits.Any(x => GetAuthorDisplayName(x.Author) != AuthorName))
{
result.Append(Resources.AndOthers);
result.Append(' ');
}
result.Append(Resources.AddedSomeCommits);
return result.ToString();
}
string GetAuthorDisplayName(ICommitActorViewModel commitActorViewModel)
{
return commitActorViewModel.HasLogin ? commitActorViewModel.Login : commitActorViewModel.Name;
}
}
}

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

@ -0,0 +1,34 @@
using GitHub.Models;
namespace GitHub.ViewModels.Documents
{
/// <summary>
/// Displays a one-line summary of a commit in a pull request timeline.
/// </summary>
public class CommitSummaryViewModel : ViewModelBase, ICommitSummaryViewModel
{
/// <summary>
/// Initializes a new instance of the <see cref="CommitSummaryViewModel"/> class.
/// </summary>
/// <param name="commit">The commit model.</param>
public CommitSummaryViewModel(CommitModel commit)
{
AbbreviatedOid = commit.AbbreviatedOid;
Author = new CommitActorViewModel(commit.Author);
Header = commit.MessageHeadline;
Oid = commit.Oid;
}
/// <inheritdoc/>
public string AbbreviatedOid { get; private set; }
/// <inheritdoc/>
public ICommitActorViewModel Author { get; private set; }
/// <inheritdoc/>
public string Header { get; private set; }
/// <inheritdoc/>
public string Oid { get; private set; }
}
}

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

@ -0,0 +1,55 @@
using System;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using ReactiveUI;
namespace GitHub.ViewModels.Documents
{
/// <summary>
/// View model for comments on an issue or pull request.
/// </summary>
public interface IIssueishCommentViewModel : ICommentViewModel, IDisposable
{
/// <summary>
/// Gets a value indicating whether the comment will show a button for
/// <see cref="CloseOrReopen"/>.
/// </summary>
bool CanCloseOrReopen { get; }
/// <summary>
/// Gets a a caption for the <see cref="CloseOrReopen"/> command.
/// </summary>
string CloseOrReopenCaption { get; }
/// <summary>
/// Gets a command which when executed will close the issue or pull request if it is open,
/// or reopen it if it is closed.
/// </summary>
ReactiveCommand<Unit, Unit> CloseOrReopen { get; }
/// <summary>
/// Initializes the view model with data.
/// </summary>
/// <param name="thread">The thread that the comment is a part of.</param>
/// <param name="currentUser">The current user.</param>
/// <param name="comment">The comment model. May be null.</param>
/// <param name="isPullRequest">
/// true if the comment is on a pull request, false if the comment is on an issue.
/// </param>
/// <param name="canCloseOrReopen">
/// Whether the user can close or reopen the pull request from this comment.
/// </param>
/// <param name="isOpen">
/// An observable tracking whether the issue or pull request is open. Can be null if
/// <paramref name="canCloseOrReopen"/> is false.
/// </param>
Task InitializeAsync(
IIssueishCommentThreadViewModel thread,
ActorModel currentUser,
CommentModel comment,
bool isPullRequest,
bool canCloseOrReopen,
IObservable<bool> isOpen = null);
}
}

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

@ -0,0 +1,104 @@
using System;
using System.ComponentModel.Composition;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using ReactiveUI;
namespace GitHub.ViewModels.Documents
{
/// <summary>
/// View model for comments on an issue or pull request.
/// </summary>
[Export(typeof(IIssueishCommentViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public sealed class IssueishCommentViewModel : CommentViewModel, IIssueishCommentViewModel
{
bool canCloseOrReopen;
ObservableAsPropertyHelper<string> closeOrReopenCaption;
/// <summary>
/// Initializes a new instance of the <see cref="CommentViewModel"/> class.
/// </summary>
/// <param name="commentService">The comment service.</param>
/// <param name="autoCompleteAdvisor"></param>
[ImportingConstructor]
public IssueishCommentViewModel(ICommentService commentService, IAutoCompleteAdvisor autoCompleteAdvisor)
: base(commentService, autoCompleteAdvisor)
{
CloseOrReopen = ReactiveCommand.CreateFromTask(
DoCloseOrReopen,
this.WhenAnyValue(x => x.CanCloseOrReopen));
AddErrorHandler(CloseOrReopen);
}
/// <inheritdoc/>
public bool CanCloseOrReopen
{
get => canCloseOrReopen;
private set => this.RaiseAndSetIfChanged(ref canCloseOrReopen, value);
}
/// <inheritdoc/>
public string CloseOrReopenCaption => closeOrReopenCaption?.Value;
/// <inheritdoc/>
public ReactiveCommand<Unit, Unit> CloseOrReopen { get; }
/// <inheritdoc/>
public async Task InitializeAsync(
IIssueishCommentThreadViewModel thread,
ActorModel currentUser,
CommentModel comment,
bool isPullRequest,
bool canCloseOrReopen,
IObservable<bool> isOpen = null)
{
await base.InitializeAsync(
thread,
currentUser,
comment,
comment == null ? CommentEditState.Editing : CommentEditState.None)
.ConfigureAwait(true);
CanCloseOrReopen = canCloseOrReopen;
closeOrReopenCaption?.Dispose();
if (canCloseOrReopen && isOpen != null)
{
closeOrReopenCaption =
this.WhenAnyValue(x => x.Body)
.CombineLatest(isOpen, (body, open) => GetCloseOrReopenCaption(isPullRequest, open, body))
.ToProperty(this, x => x.CloseOrReopenCaption);
}
}
public void Dispose() => closeOrReopenCaption?.Dispose();
async Task DoCloseOrReopen()
{
await ((IIssueishCommentThreadViewModel)Thread).CloseOrReopen(this).ConfigureAwait(true);
}
static string GetCloseOrReopenCaption(bool isPullRequest, bool isOpen, string body)
{
if (string.IsNullOrEmpty(body))
{
if (isPullRequest)
{
return isOpen ? Resources.ClosePullRequest : Resources.ReopenPullRequest;
}
else
{
return isOpen ? Resources.CloseIssue: Resources.ReopenIssue;
}
}
else
{
return isOpen ? Resources.CloseAndComment : Resources.ReopenAndComment;
}
}
}
}

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

@ -0,0 +1,86 @@
using System;
using System.ComponentModel.Composition;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.Factories;
using GitHub.Models;
using GitHub.Services;
using ReactiveUI;
namespace GitHub.ViewModels.Documents
{
[Export(typeof(IIssueishPaneViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class IssueishPaneViewModel : ViewModelBase, IIssueishPaneViewModel
{
readonly IViewViewModelFactory factory;
readonly IPullRequestSessionManager sessionManager;
IViewModel content;
string paneCaption;
[ImportingConstructor]
public IssueishPaneViewModel(
IViewViewModelFactory factory,
IPullRequestSessionManager sessionManager)
{
Guard.ArgumentNotNull(factory, nameof(factory));
Guard.ArgumentNotNull(sessionManager, nameof(sessionManager));
this.factory = factory;
this.sessionManager = sessionManager;
}
public IViewModel Content
{
get => content;
private set => this.RaiseAndSetIfChanged(ref content, value);
}
public bool IsInitialized => content != null;
public string PaneCaption
{
get => paneCaption;
private set => this.RaiseAndSetIfChanged(ref paneCaption, value);
}
public Task InitializeAsync(IServiceProvider paneServiceProvider)
{
return Task.CompletedTask;
}
public async Task Load(IConnection connection, string owner, string name, int number)
{
Content = new SpinnerViewModel();
PaneCaption = "#" + number;
// TODO: We will eventually support loading issues here as well.
try
{
var session = await sessionManager.GetSession(owner, name, number).ConfigureAwait(true);
var vm = factory.CreateViewModel<IPullRequestPageViewModel>();
var repository = new RemoteRepositoryModel(
0,
name,
session.LocalRepository.CloneUrl.WithOwner(session.PullRequest.HeadRepositoryOwner),
false,
false,
null,
null);
await vm.InitializeAsync(
repository,
session.LocalRepository,
session.User,
session.PullRequest).ConfigureAwait(true);
Content = vm;
PaneCaption += " " + vm.Title;
}
catch (Exception ex)
{
// TODO: Show exception.
}
}
}
}

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

@ -0,0 +1,216 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.Factories;
using GitHub.Models;
using GitHub.Primitives;
using GitHub.Services;
using ReactiveUI;
namespace GitHub.ViewModels.Documents
{
/// <summary>
/// View model for displaying a pull request in a document window.
/// </summary>
[Export(typeof(IPullRequestPageViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class PullRequestPageViewModel : PullRequestViewModelBase, IPullRequestPageViewModel, IIssueishCommentThreadViewModel
{
readonly IViewViewModelFactory factory;
readonly IPullRequestService service;
readonly IPullRequestSessionManager sessionManager;
readonly ITeamExplorerServices teServices;
readonly IVisualStudioBrowser visualStudioBrowser;
readonly IUsageTracker usageTracker;
ActorModel currentUserModel;
ReactiveList<IViewModel> timeline = new ReactiveList<IViewModel>();
/// <summary>
/// Initializes a new instance of the <see cref="PullRequestPageViewModel"/> class.
/// </summary>
/// <param name="factory">The view model factory.</param>
[ImportingConstructor]
public PullRequestPageViewModel(
IViewViewModelFactory factory,
IPullRequestService service,
IPullRequestSessionManager sessionManager,
ITeamExplorerServices teServices,
IVisualStudioBrowser visualStudioBrowser,
IUsageTracker usageTracker)
{
Guard.ArgumentNotNull(factory, nameof(factory));
Guard.ArgumentNotNull(service, nameof(service));
Guard.ArgumentNotNull(sessionManager, nameof(sessionManager));
Guard.ArgumentNotNull(visualStudioBrowser, nameof(visualStudioBrowser));
Guard.ArgumentNotNull(teServices, nameof(teServices));
this.factory = factory;
this.service = service;
this.sessionManager = sessionManager;
this.teServices = teServices;
this.visualStudioBrowser = visualStudioBrowser;
this.usageTracker = usageTracker;
timeline.ItemsRemoved.Subscribe(TimelineItemRemoved);
ShowCommit = ReactiveCommand.CreateFromTask<string>(DoShowCommit);
OpenOnGitHub = ReactiveCommand.Create(DoOpenOnGitHub);
}
/// <inheritdoc/>
public IActorViewModel CurrentUser { get; private set; }
/// <inheritdoc/>
public int CommitCount { get; private set; }
/// <inheritdoc/>
public IReadOnlyList<IViewModel> Timeline => timeline;
/// <inheritdoc/>
public ReactiveCommand<string, Unit> ShowCommit { get; }
/// <inheritdoc/>
public async Task InitializeAsync(
RemoteRepositoryModel repository,
LocalRepositoryModel localRepository,
ActorModel currentUser,
PullRequestDetailModel model)
{
await base.InitializeAsync(repository, localRepository, model).ConfigureAwait(true);
timeline.Clear();
CommitCount = 0;
currentUserModel = currentUser;
CurrentUser = new ActorViewModel(currentUser);
var commits = new List<CommitSummaryViewModel>();
foreach (var i in model.Timeline)
{
if (!(i is CommitModel) && commits.Count > 0)
{
timeline.Add(new CommitListViewModel(commits));
commits.Clear();
}
switch (i)
{
case CommitModel commit:
commits.Add(new CommitSummaryViewModel(commit));
++CommitCount;
break;
case CommentModel comment:
await AddComment(comment).ConfigureAwait(true);
break;
}
}
if (commits.Count > 0)
{
timeline.Add(new CommitListViewModel(commits));
}
await AddPlaceholder().ConfigureAwait(true);
await usageTracker.IncrementCounter(x => x.NumberOfPRConversationsOpened);
}
/// <inheritdoc/>
public async Task CloseOrReopen(ICommentViewModel comment)
{
var address = HostAddress.Create(Repository.CloneUrl);
if (State == PullRequestState.Open)
{
await service.CloseIssueish(
address,
Repository.Owner,
Repository.Name,
Number).ConfigureAwait(true);
State = PullRequestState.Closed;
}
else
{
await service.ReopenIssueish(
address,
Repository.Owner,
Repository.Name,
Number).ConfigureAwait(true);
State = PullRequestState.Open;
}
}
/// <inheritdoc/>
public async Task PostComment(ICommentViewModel comment)
{
var address = HostAddress.Create(Repository.CloneUrl);
var result = await service.PostComment(address, Id, comment.Body).ConfigureAwait(true);
timeline.Remove(comment);
await AddComment(result).ConfigureAwait(true);
await AddPlaceholder().ConfigureAwait(true);
}
public async Task DeleteComment(ICommentViewModel comment)
{
await service.DeleteComment(
HostAddress.Create(Repository.CloneUrl),
Repository.Owner,
Repository.Name,
comment.DatabaseId).ConfigureAwait(true);
timeline.Remove(comment);
}
public async Task EditComment(ICommentViewModel comment)
{
await service.EditComment(
HostAddress.Create(Repository.CloneUrl),
Repository.Owner,
Repository.Name,
comment.DatabaseId,
comment.Body).ConfigureAwait(false);
}
async Task AddComment(CommentModel comment)
{
var vm = factory.CreateViewModel<IIssueishCommentViewModel>();
await vm.InitializeAsync(
this,
currentUserModel,
comment,
true,
false).ConfigureAwait(true);
timeline.Add(vm);
}
async Task AddPlaceholder()
{
var placeholder = factory.CreateViewModel<IIssueishCommentViewModel>();
await placeholder.InitializeAsync(
this,
currentUserModel,
null,
true,
true,
this.WhenAnyValue(x => x.State, x => x == PullRequestState.Open)).ConfigureAwait(true);
timeline.Add(placeholder);
}
async Task DoShowCommit(string oid)
{
await service.FetchCommit(LocalRepository, Repository, oid).ConfigureAwait(true);
teServices.ShowCommitDetails(oid);
}
void DoOpenOnGitHub()
{
visualStudioBrowser.OpenUrl(WebUrl);
}
void TimelineItemRemoved(IViewModel item)
{
(item as IDisposable)?.Dispose();
}
}
}

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

@ -1,14 +1,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Windows.Threading;
using GitHub.Collections;
using GitHub.Extensions;
using GitHub.Extensions.Reactive;
@ -154,21 +152,21 @@ namespace GitHub.ViewModels.GitHubPane
Forks = new RepositoryModel[]
{
RemoteRepository,
repository,
RemoteRepository,
repository,
};
}
this.WhenAnyValue(x => x.SelectedState, x => x.RemoteRepository)
.Skip(1)
.Subscribe(_ => Refresh().Forget());
.Subscribe(_ => InitializeItemSource(false).Forget());
Observable.Merge(
this.WhenAnyValue(x => x.SearchQuery).Skip(1).SelectUnit(),
AuthorFilter.WhenAnyValue(x => x.Selected).Skip(1).SelectUnit())
.Subscribe(_ => FilterChanged());
await Refresh();
await InitializeItemSource(true);
}
catch (Exception ex)
{
@ -182,18 +180,43 @@ namespace GitHub.ViewModels.GitHubPane
/// Refreshes the view model.
/// </summary>
/// <returns>A task tracking the operation.</returns>
public override Task Refresh()
public override Task Refresh() => InitializeItemSource(true);
/// <summary>
/// When overridden in a derived class, creates the <see cref="IVirtualizingListSource{T}"/>
/// that will act as the source for <see cref="Items"/>.
/// </summary>
/// <param name="refresh">
/// Whether the item source is being created due to <see cref="Refresh"/> being called.
/// </param>
protected abstract Task<IVirtualizingListSource<IIssueListItemViewModelBase>> CreateItemSource(bool refresh);
/// <summary>
/// When overridden in a derived class, navigates to the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>A task tracking the operation.</returns>
protected abstract Task DoOpenItem(IIssueListItemViewModelBase item);
/// <summary>
/// Loads a page of authors for the <see cref="AuthorFilter"/>.
/// </summary>
/// <param name="after">The GraphQL "after" cursor.</param>
/// <returns>A task that returns a page of authors.</returns>
protected abstract Task<Page<ActorModel>> LoadAuthors(string after);
async Task InitializeItemSource(bool refresh)
{
if (RemoteRepository == null)
{
// If an exception occurred reading the parent repository, do nothing.
return Task.CompletedTask;
return;
}
subscription?.Dispose();
var dispose = new CompositeDisposable();
var itemSource = CreateItemSource();
var itemSource = await CreateItemSource(refresh);
var items = new VirtualizingList<IIssueListItemViewModelBase>(itemSource, null);
var view = new VirtualizingListCollectionView<IIssueListItemViewModelBase>(items);
@ -219,30 +242,8 @@ namespace GitHub.ViewModels.GitHubPane
x => items.InitializationError -= x)
.Subscribe(x => Error = x.EventArgs.GetException()));
subscription = dispose;
return Task.CompletedTask;
}
/// <summary>
/// When overridden in a derived class, creates the <see cref="IVirtualizingListSource{T}"/>
/// that will act as the source for <see cref="Items"/>.
/// </summary>
protected abstract IVirtualizingListSource<IIssueListItemViewModelBase> CreateItemSource();
/// <summary>
/// When overridden in a derived class, navigates to the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>A task tracking the operation.</returns>
protected abstract Task DoOpenItem(IIssueListItemViewModelBase item);
/// <summary>
/// Loads a page of authors for the <see cref="AuthorFilter"/>.
/// </summary>
/// <param name="after">The GraphQL "after" cursor.</param>
/// <returns>A task that returns a page of authors.</returns>
protected abstract Task<Page<ActorModel>> LoadAuthors(string after);
void FilterChanged()
{
if (!string.IsNullOrWhiteSpace(SearchQuery))

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

@ -18,6 +18,7 @@ using GitHub.Models;
using GitHub.Models.Drafts;
using GitHub.Primitives;
using GitHub.Services;
using GitHub.UI;
using GitHub.Validation;
using Octokit;
using ReactiveUI;
@ -51,8 +52,9 @@ namespace GitHub.ViewModels.GitHubPane
IPullRequestService service,
INotificationService notifications,
IMessageDraftStore draftStore,
IGitService gitService)
: this(modelServiceFactory, service, notifications, draftStore, gitService, DefaultScheduler.Instance)
IGitService gitService,
IAutoCompleteAdvisor autoCompleteAdvisor)
: this(modelServiceFactory, service, notifications, draftStore, gitService, autoCompleteAdvisor, DefaultScheduler.Instance)
{
}
@ -62,6 +64,7 @@ namespace GitHub.ViewModels.GitHubPane
INotificationService notifications,
IMessageDraftStore draftStore,
IGitService gitService,
IAutoCompleteAdvisor autoCompleteAdvisor,
IScheduler timerScheduler)
{
Guard.ArgumentNotNull(modelServiceFactory, nameof(modelServiceFactory));
@ -69,12 +72,14 @@ namespace GitHub.ViewModels.GitHubPane
Guard.ArgumentNotNull(notifications, nameof(notifications));
Guard.ArgumentNotNull(draftStore, nameof(draftStore));
Guard.ArgumentNotNull(gitService, nameof(gitService));
Guard.ArgumentNotNull(autoCompleteAdvisor, nameof(autoCompleteAdvisor));
Guard.ArgumentNotNull(timerScheduler, nameof(timerScheduler));
this.service = service;
this.modelServiceFactory = modelServiceFactory;
this.draftStore = draftStore;
this.gitService = gitService;
this.AutoCompleteAdvisor = autoCompleteAdvisor;
this.timerScheduler = timerScheduler;
this.WhenAnyValue(x => x.Branches)
@ -336,6 +341,7 @@ namespace GitHub.ViewModels.GitHubPane
public RemoteRepositoryModel GitHubRepository { get { return githubRepository?.Value; } }
bool IsExecuting { get { return isExecuting.Value; } }
public IAutoCompleteAdvisor AutoCompleteAdvisor { get; }
bool initialized;
bool Initialized

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

@ -24,6 +24,7 @@ using ReactiveUI.Legacy;
using Serilog;
using static System.FormattableString;
using ReactiveCommand = ReactiveUI.ReactiveCommand;
using GitHub.Primitives;
namespace GitHub.ViewModels.GitHubPane
{
@ -42,6 +43,7 @@ namespace GitHub.ViewModels.GitHubPane
readonly ISyncSubmodulesCommand syncSubmodulesCommand;
readonly IViewViewModelFactory viewViewModelFactory;
readonly IGitService gitService;
readonly IOpenIssueishDocumentCommand openDocumentCommand;
IModelService modelService;
PullRequestDetailModel model;
@ -82,7 +84,8 @@ namespace GitHub.ViewModels.GitHubPane
IPullRequestFilesViewModel files,
ISyncSubmodulesCommand syncSubmodulesCommand,
IViewViewModelFactory viewViewModelFactory,
IGitService gitService)
IGitService gitService,
IOpenIssueishDocumentCommand openDocumentCommand)
{
Guard.ArgumentNotNull(pullRequestsService, nameof(pullRequestsService));
Guard.ArgumentNotNull(sessionManager, nameof(sessionManager));
@ -92,6 +95,7 @@ namespace GitHub.ViewModels.GitHubPane
Guard.ArgumentNotNull(syncSubmodulesCommand, nameof(syncSubmodulesCommand));
Guard.ArgumentNotNull(viewViewModelFactory, nameof(viewViewModelFactory));
Guard.ArgumentNotNull(gitService, nameof(gitService));
Guard.ArgumentNotNull(openDocumentCommand, nameof(openDocumentCommand));
this.pullRequestsService = pullRequestsService;
this.sessionManager = sessionManager;
@ -101,6 +105,7 @@ namespace GitHub.ViewModels.GitHubPane
this.syncSubmodulesCommand = syncSubmodulesCommand;
this.viewViewModelFactory = viewViewModelFactory;
this.gitService = gitService;
this.openDocumentCommand = openDocumentCommand;
Files = files;
@ -134,6 +139,8 @@ namespace GitHub.ViewModels.GitHubPane
SyncSubmodules.Subscribe(_ => Refresh().ToObservable());
SubscribeOperationError(SyncSubmodules);
OpenConversation = ReactiveCommand.Create(DoOpenConversation);
OpenOnGitHub = ReactiveCommand.Create(DoOpenDetailsUrl);
ShowReview = ReactiveCommand.Create<IPullRequestReviewSummaryViewModel>(DoShowReview);
@ -144,11 +151,6 @@ namespace GitHub.ViewModels.GitHubPane
[Import(AllowDefault = true)]
private IStaticReviewFileMapManager StaticReviewFileMapManager { get; set; }
private void DoOpenDetailsUrl()
{
usageTracker.IncrementCounter(measuresModel => measuresModel.NumberOfPRDetailsOpenInGitHub).Forget();
}
/// <inheritdoc/>
public PullRequestDetailModel Model
{
@ -273,6 +275,9 @@ namespace GitHub.ViewModels.GitHubPane
/// <inheritdoc/>
public ReactiveCommand<Unit, Unit> SyncSubmodules { get; }
/// <inheritdoc/>
public ReactiveCommand<Unit, Unit> OpenConversation { get; }
/// <inheritdoc/>
public ReactiveCommand<Unit, Unit> OpenOnGitHub { get; }
@ -359,7 +364,8 @@ namespace GitHub.ViewModels.GitHubPane
Checks = (IReadOnlyList<IPullRequestCheckViewModel>)PullRequestCheckViewModel.Build(viewViewModelFactory, pullRequest)?.ToList() ?? Array.Empty<IPullRequestCheckViewModel>();
await Files.InitializeAsync(Session);
// Only show unresolved comments
await Files.InitializeAsync(Session, c => !c.IsResolved);
var localBranches = await pullRequestsService.GetLocalBranches(LocalRepository, pullRequest).ToList();
@ -650,6 +656,21 @@ namespace GitHub.ViewModels.GitHubPane
}
}
void DoOpenConversation()
{
var p = new OpenIssueishParams(
HostAddress.Create(LocalRepository.CloneUrl),
RemoteRepositoryOwner,
LocalRepository.Name,
Number);
openDocumentCommand.Execute(p);
}
void DoOpenDetailsUrl()
{
usageTracker.IncrementCounter(measuresModel => measuresModel.NumberOfPRDetailsOpenInGitHub).Forget();
}
void DoShowReview(IPullRequestReviewSummaryViewModel review)
{
if (review.State == PullRequestReviewState.Pending)

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

@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using GitHub.Collections;
using GitHub.Commands;
using GitHub.Extensions;
using GitHub.Models;
using GitHub.Primitives;
@ -68,8 +68,16 @@ namespace GitHub.ViewModels.GitHubPane
public ReactiveCommand<IPullRequestListItemViewModel, IPullRequestListItemViewModel> OpenItemInBrowser { get; }
/// <inheritdoc/>
protected override IVirtualizingListSource<IIssueListItemViewModelBase> CreateItemSource()
protected override async Task<IVirtualizingListSource<IIssueListItemViewModelBase>> CreateItemSource(bool refresh)
{
if (refresh)
{
await service.ClearPullRequestsCache(
HostAddress.Create(RemoteRepository.CloneUrl),
RemoteRepository.Owner,
RemoteRepository.Name);
}
return new ItemSource(this);
}
@ -125,18 +133,18 @@ namespace GitHub.ViewModels.GitHubPane
protected override async Task<Page<PullRequestListItemModel>> LoadPage(string after)
{
PullRequestStateEnum[] states;
PullRequestState[] states;
switch (owner.SelectedState)
{
case "Open":
states = new[] { PullRequestStateEnum.Open };
states = new[] { PullRequestState.Open };
break;
case "Closed":
states = new[] { PullRequestStateEnum.Closed, PullRequestStateEnum.Merged };
states = new[] { PullRequestState.Closed, PullRequestState.Merged };
break;
default:
states = new[] { PullRequestStateEnum.Open, PullRequestStateEnum.Closed, PullRequestStateEnum.Merged };
states = new[] { PullRequestState.Open, PullRequestState.Closed, PullRequestState.Merged };
break;
}

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

@ -45,8 +45,9 @@ namespace GitHub.ViewModels.GitHubPane
IPullRequestEditorService editorService,
IPullRequestSessionManager sessionManager,
IMessageDraftStore draftStore,
IPullRequestFilesViewModel files)
: this(pullRequestService, editorService, sessionManager,draftStore, files, DefaultScheduler.Instance)
IPullRequestFilesViewModel files,
IAutoCompleteAdvisor autoCompleteAdvisor)
: this(pullRequestService, editorService, sessionManager,draftStore, files, autoCompleteAdvisor, DefaultScheduler.Instance)
{
}
@ -56,12 +57,14 @@ namespace GitHub.ViewModels.GitHubPane
IPullRequestSessionManager sessionManager,
IMessageDraftStore draftStore,
IPullRequestFilesViewModel files,
IAutoCompleteAdvisor autoCompleteAdvisor,
IScheduler timerScheduler)
{
Guard.ArgumentNotNull(editorService, nameof(editorService));
Guard.ArgumentNotNull(sessionManager, nameof(sessionManager));
Guard.ArgumentNotNull(draftStore, nameof(draftStore));
Guard.ArgumentNotNull(files, nameof(files));
Guard.ArgumentNotNull(autoCompleteAdvisor, nameof(autoCompleteAdvisor));
Guard.ArgumentNotNull(timerScheduler, nameof(timerScheduler));
this.pullRequestService = pullRequestService;
@ -77,6 +80,7 @@ namespace GitHub.ViewModels.GitHubPane
.ToProperty(this, x => x.CanApproveRequestChanges);
Files = files;
AutoCompleteAdvisor = autoCompleteAdvisor;
var hasBodyOrComments = this.WhenAnyValue(
x => x.Body,
@ -118,6 +122,9 @@ namespace GitHub.ViewModels.GitHubPane
/// <inheritdoc/>
public IPullRequestFilesViewModel Files { get; }
/// <inheritdoc/>
public IAutoCompleteAdvisor AutoCompleteAdvisor { get; }
/// <inheritdoc/>
public string Body
{

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

@ -3,9 +3,11 @@ using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.Logging;
using GitHub.Models;
using GitHub.Services;
using ReactiveUI;
using Serilog;
namespace GitHub.ViewModels.GitHubPane
{
@ -14,6 +16,8 @@ namespace GitHub.ViewModels.GitHubPane
/// </summary>
public class PullRequestReviewCommentViewModel : IPullRequestReviewFileCommentViewModel
{
static readonly ILogger log = LogManager.ForContext<PullRequestReviewCommentViewModel>();
readonly IPullRequestEditorService editorService;
readonly IPullRequestSession session;
readonly PullRequestReviewCommentModel model;
@ -52,8 +56,24 @@ namespace GitHub.ViewModels.GitHubPane
{
if (thread == null)
{
var file = await session.GetFile(RelativePath, model.Thread.CommitSha);
thread = file.InlineCommentThreads.FirstOrDefault(t => t.Comments.Any(c => c.Comment.Id == model.Id));
if(model.Thread.IsOutdated)
{
var file = await session.GetFile(RelativePath, model.Thread.OriginalCommitSha);
thread = file.InlineCommentThreads.FirstOrDefault(t => t.Comments.Any(c => c.Comment.Id == model.Id));
}
else
{
var file = await session.GetFile(RelativePath, model.Thread.CommitSha);
thread = file.InlineCommentThreads.FirstOrDefault(t => t.Comments.Any(c => c.Comment.Id == model.Id));
if(thread?.LineNumber == -1)
{
log.Warning("Couldn't find line number for comment on {RelativePath} @ {CommitSha}", RelativePath, model.Thread.CommitSha);
// Fall back to opening outdated file if we can't find a line number for the comment
file = await session.GetFile(RelativePath, model.Thread.OriginalCommitSha);
thread = file.InlineCommentThreads.FirstOrDefault(t => t.Comments.Any(c => c.Comment.Id == model.Id));
}
}
}
if (thread != null && thread.LineNumber != -1)
@ -61,9 +81,9 @@ namespace GitHub.ViewModels.GitHubPane
await editorService.OpenDiff(session, RelativePath, thread);
}
}
catch (Exception)
catch (Exception e)
{
// TODO: Show error.
log.Error(e, nameof(DoOpen));
}
}
}

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

@ -144,7 +144,6 @@ namespace GitHub.ViewModels.GitHubPane
try
{
await Task.Delay(0);
PullRequestTitle = pullRequest.Title;
var reviews = new List<IPullRequestReviewViewModel>();

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

@ -0,0 +1,85 @@
using System;
using System.ComponentModel.Composition;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Logging;
using GitHub.Models;
using ReactiveUI;
using Serilog;
namespace GitHub.ViewModels
{
/// <summary>
/// Base class for issue and pull request view models.
/// </summary>
public class IssueishViewModel : ViewModelBase, IIssueishViewModel
{
static readonly ILogger log = LogManager.ForContext<IssueishViewModel>();
IActorViewModel author;
string body;
string title;
Uri webUrl;
/// <summary>
/// Initializes a new instance of the <see cref="IssueishViewModel"/> class.
/// </summary>
[ImportingConstructor]
public IssueishViewModel()
{
}
/// <inheritdoc/>
public RemoteRepositoryModel Repository { get; private set; }
/// <inheritdoc/>
public string Id { get; private set; }
/// <inheritdoc/>
public int Number { get; private set; }
/// <inheritdoc/>
public IActorViewModel Author
{
get => author;
private set => this.RaiseAndSetIfChanged(ref author, value);
}
/// <inheritdoc/>
public string Body
{
get => body;
protected set => this.RaiseAndSetIfChanged(ref body, value);
}
/// <inheritdoc/>
public string Title
{
get => title;
protected set => this.RaiseAndSetIfChanged(ref title, value);
}
/// <inheritdoc/>
public Uri WebUrl
{
get { return webUrl; }
protected set { this.RaiseAndSetIfChanged(ref webUrl, value); }
}
/// <inheritdoc/>
public ReactiveCommand<Unit, Unit> OpenOnGitHub { get; protected set; }
protected Task InitializeAsync(
RemoteRepositoryModel repository,
IssueishDetailModel model)
{
Repository = repository;
Id = model.Id;
Author = new ActorViewModel(model.Author);
Body = model.Body;
Number = model.Number;
Title = model.Title;
return Task.CompletedTask;
}
}
}

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

@ -23,6 +23,7 @@ namespace GitHub.ViewModels
[PartCreationPolicy(CreationPolicy.NonShared)]
public class PullRequestReviewCommentThreadViewModel : CommentThreadViewModel, IPullRequestReviewCommentThreadViewModel
{
readonly ReactiveList<ICommentViewModel> comments = new ReactiveList<ICommentViewModel>();
readonly IViewViewModelFactory factory;
readonly ObservableAsPropertyHelper<bool> needsPush;
IPullRequestSessionFile file;
@ -50,6 +51,9 @@ namespace GitHub.ViewModels
.ToProperty(this, x => x.NeedsPush);
}
/// <inheritdoc/>
public IReactiveList<ICommentViewModel> Comments => comments;
/// <inheritdoc/>
public IPullRequestSession Session { get; private set; }
@ -66,6 +70,9 @@ namespace GitHub.ViewModels
/// <inheritdoc/>
public DiffSide Side { get; private set; }
/// <inheritdoc/>
public bool IsResolved { get; private set; }
public bool IsNewThread
{
get => isNewThread;
@ -76,7 +83,11 @@ namespace GitHub.ViewModels
public bool NeedsPush => needsPush.Value;
/// <inheritdoc/>
public async Task InitializeAsync(IPullRequestSession session,
IReadOnlyReactiveList<ICommentViewModel> IPullRequestReviewCommentThreadViewModel.Comments => comments;
/// <inheritdoc/>
public async Task InitializeAsync(
IPullRequestSession session,
IPullRequestSessionFile file,
IInlineCommentThreadModel thread,
bool addPlaceholder)
@ -89,6 +100,7 @@ namespace GitHub.ViewModels
File = file;
LineNumber = thread.LineNumber;
Side = thread.DiffLineType == DiffChangeType.Delete ? DiffSide.Left : DiffSide.Right;
IsResolved = thread.IsResolved;
foreach (var comment in thread.Comments)
{
@ -121,7 +133,8 @@ namespace GitHub.ViewModels
vm.Body = draft.Body;
}
AddPlaceholder(vm);
InitializePlaceholder(vm);
comments.Add(vm);
}
}
@ -154,7 +167,8 @@ namespace GitHub.ViewModels
vm.Body = draft.Body;
}
AddPlaceholder(vm);
InitializePlaceholder(vm);
comments.Add(vm);
}
public override async Task PostComment(ICommentViewModel comment)

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

@ -19,31 +19,25 @@ namespace GitHub.ViewModels
public class PullRequestReviewCommentViewModel : CommentViewModel, IPullRequestReviewCommentViewModel
{
readonly ObservableAsPropertyHelper<bool> canStartReview;
readonly ObservableAsPropertyHelper<string> commitCaption;
IPullRequestSession session;
bool isPending;
/// <summary>
/// Initializes a new instance of the <see cref="PullRequestReviewCommentViewModel"/> class.
/// </summary>
/// <param name="commentService">The comment service</param>
/// <param name="commentService">The comment service.</param>
/// <param name="autoCompleteAdvisor">The auto complete advisor.</param>
[ImportingConstructor]
public PullRequestReviewCommentViewModel(ICommentService commentService)
: base(commentService)
public PullRequestReviewCommentViewModel(ICommentService commentService,
IAutoCompleteAdvisor autoCompleteAdvisor)
: base(commentService, autoCompleteAdvisor)
{
var pendingAndIsNew = this.WhenAnyValue(
canStartReview = this.WhenAnyValue(
x => x.IsPending,
x => x.Id,
(isPending, id) => (isPending, isNewComment: id == null));
canStartReview = pendingAndIsNew
.Select(arg => !arg.isPending && arg.isNewComment)
(isPending, id) => !isPending && id == null)
.ToProperty(this, x => x.CanStartReview);
commitCaption = pendingAndIsNew
.Select(arg => !arg.isNewComment ? Resources.UpdateComment : arg.isPending ? Resources.AddReviewComment : Resources.AddSingleComment)
.ToProperty(this, x => x.CommitCaption);
StartReview = ReactiveCommand.CreateFromTask(DoStartReview, CommitEdit.CanExecute);
AddErrorHandler(StartReview);
}
@ -84,9 +78,6 @@ namespace GitHub.ViewModels
/// <inheritdoc/>
public bool CanStartReview => canStartReview.Value;
/// <inheritdoc/>
public string CommitCaption => commitCaption.Value;
/// <inheritdoc/>
public bool IsPending
{
@ -97,6 +88,16 @@ namespace GitHub.ViewModels
/// <inheritdoc/>
public ReactiveCommand<Unit, Unit> StartReview { get; }
protected override IObservable<string> GetCommitCaptionObservable()
{
return this.WhenAnyValue(
x => x.IsPending,
x => x.Id,
(pending, id) => id != null ?
Resources.UpdateComment :
pending ? Resources.AddReviewComment : Resources.AddSingleComment);
}
async Task DoStartReview()
{
IsSubmitting = true;

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

@ -0,0 +1,78 @@
using System;
using System.ComponentModel.Composition;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.Logging;
using GitHub.Models;
using ReactiveUI;
using Serilog;
namespace GitHub.ViewModels
{
/// <summary>
/// Base class for pull request view models.
/// </summary>
public class PullRequestViewModelBase : IssueishViewModel, IPullRequestViewModelBase
{
static readonly ILogger log = LogManager.ForContext<PullRequestViewModelBase>();
PullRequestState state;
string sourceBranchDisplayName;
string targetBranchDisplayName;
/// <summary>
/// Initializes a new instance of the <see cref="PullRequestViewModelBase"/> class.
/// </summary>
[ImportingConstructor]
public PullRequestViewModelBase()
{
}
/// <inheritdoc/>
public LocalRepositoryModel LocalRepository { get; private set; }
public PullRequestState State
{
get => state;
protected set => this.RaiseAndSetIfChanged(ref state, value);
}
public string SourceBranchDisplayName
{
get => sourceBranchDisplayName;
private set => this.RaiseAndSetIfChanged(ref sourceBranchDisplayName, value);
}
public string TargetBranchDisplayName
{
get => targetBranchDisplayName;
private set => this.RaiseAndSetIfChanged(ref targetBranchDisplayName, value);
}
protected virtual async Task InitializeAsync(
RemoteRepositoryModel repository,
LocalRepositoryModel localRepository,
PullRequestDetailModel model)
{
await base.InitializeAsync(repository, model).ConfigureAwait(true);
var fork = model.BaseRepositoryOwner != model.HeadRepositoryOwner;
LocalRepository = localRepository;
State = model.State;
SourceBranchDisplayName = GetBranchDisplayName(fork, model.HeadRepositoryOwner, model.HeadRefName);
TargetBranchDisplayName = GetBranchDisplayName(fork, model.BaseRepositoryOwner, model.BaseRefName);
WebUrl = localRepository.CloneUrl.ToRepositoryUrl().Append("pull/" + Number);
}
static string GetBranchDisplayName(bool isFromFork, string owner, string label)
{
if (owner != null)
{
return isFromFork ? owner + ':' + label : label;
}
else
{
return Resources.InvalidBranchName;
}
}
}
}

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

@ -0,0 +1,14 @@
using System;
using System.ComponentModel.Composition;
namespace GitHub.ViewModels
{
/// <summary>
/// View model which displays a spinner.
/// </summary>
[Export(typeof(ISpinnerViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class SpinnerViewModel : ViewModelBase, ISpinnerViewModel
{
}
}

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

@ -31,7 +31,6 @@ namespace GitHub.ViewModels.TeamExplorer
readonly IModelServiceFactory modelServiceFactory;
readonly ObservableAsPropertyHelper<IReadOnlyList<IAccount>> accounts;
readonly ObservableAsPropertyHelper<bool> isHostComboBoxVisible;
readonly ObservableAsPropertyHelper<string> title;
readonly IUsageTracker usageTracker;
[ImportingConstructor]
@ -52,14 +51,6 @@ namespace GitHub.ViewModels.TeamExplorer
this.usageTracker = usageTracker;
this.modelServiceFactory = modelServiceFactory;
title = this.WhenAny(
x => x.SelectedConnection,
x => x.Value != null ?
string.Format(CultureInfo.CurrentCulture, Resources.PublishToTitle, x.Value.HostAddress.Title) :
Resources.PublishTitle
)
.ToProperty(this, x => x.Title);
Connections = connectionManager.Connections;
this.repositoryPublishService = repositoryPublishService;
@ -109,8 +100,6 @@ namespace GitHub.ViewModels.TeamExplorer
});
}
public string Title { get { return title.Value; } }
public ReactiveCommand<Unit, ProgressState> PublishRepository { get; private set; }
public IReadOnlyObservableCollection<IConnection> Connections { get; private set; }

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

@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace GitHub.Models
{
public class AutoCompleteResult
{
public static AutoCompleteResult Empty = new AutoCompleteResult(0, new AutoCompleteSuggestion[] {});
public AutoCompleteResult(int offset, IReadOnlyList<AutoCompleteSuggestion> suggestions)
{
Offset = offset;
Suggestions = suggestions;
}
public int Offset { get; private set; }
public IReadOnlyList<AutoCompleteSuggestion> Suggestions { get; private set; }
}
}

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

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Media.Imaging;
using GitHub.Extensions;
using GitHub.Helpers;
using ReactiveUI;
namespace GitHub.Models
{
public class AutoCompleteSuggestion
{
readonly string prefix;
readonly string suffix;
readonly string[] descriptionWords;
public AutoCompleteSuggestion(string name, string description, string prefix)
: this(name, description, Observable.Return<BitmapSource>(null), prefix)
{
}
public AutoCompleteSuggestion(string name, string description, IObservable<BitmapSource> image, string prefix)
: this(name, description, image, prefix, null)
{
}
public AutoCompleteSuggestion(string name, IObservable<BitmapSource> image, string prefix, string suffix)
: this(name, null, image, prefix, suffix)
{
}
public AutoCompleteSuggestion(string name, string description, IObservable<BitmapSource> image, string prefix, string suffix)
{
Guard.ArgumentNotEmptyString(name, "name");
Guard.ArgumentNotEmptyString(prefix, "prefix"); // Suggestions have to have a triggering prefix.
Guard.ArgumentNotNull(image, "image");
Name = name;
Description = description;
if (image != null)
{
image = image.ObserveOn(RxApp.MainThreadScheduler);
}
Image = image;
this.prefix = prefix;
this.suffix = suffix;
// This is pretty naive, but since the Description is currently limited to a user's FullName,
// This is fine. When we add #issue completion, we may need to fancy this up a bit.
descriptionWords = (description ?? String.Empty)
.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries);
}
/// <summary>
/// The name to display in the autocomplete list box. This should not have the "@" or ":" characters around it.
/// </summary>
public string Name { get; private set; }
public string Description { get; private set; }
public IObservable<BitmapSource> Image { get; private set; }
protected IReadOnlyCollection<string> DescriptionWords { get { return descriptionWords; } }
// What gets autocompleted.
public override string ToString()
{
return prefix + Name + suffix;
}
/// <summary>
/// Used to determine if the suggestion matches the text and if so, how it should be sorted. The larger the
/// rank, the higher it sorts.
/// </summary>
/// <remarks>
/// For mentions we sort suggestions in the following order:
///
/// 1. Login starts with text
/// 2. Component of Name starts with text (split name by spaces, then match each word)
///
/// Non matches return -1. The secondary sort is by Login ascending.
/// </remarks>
/// <param name="text">The suggestion text to match</param>
/// <returns>-1 for non-match and the sort order described in the remarks for matches</returns>
public virtual int GetSortRank(string text)
{
Guard.ArgumentNotNull(text, "text");
return Name.StartsWith(text, StringComparison.OrdinalIgnoreCase)
? 1
: descriptionWords.Any(word => word.StartsWith(text, StringComparison.OrdinalIgnoreCase))
? 0
: -1;
}
}
}

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

@ -49,5 +49,10 @@ namespace GitHub.Models
/// Gets the relative path to the file that the thread is on.
/// </summary>
string RelativePath { get; }
/// <summary>
/// Gets a value indicating whether comment thread has been marked as resolved by a user.
/// </summary>
bool IsResolved { get; }
}
}

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

@ -28,7 +28,6 @@ namespace GitHub.Models
: base(name, cloneUrl)
{
Guard.ArgumentNotEmptyString(name, nameof(name));
Guard.ArgumentNotNull(ownerAccount, nameof(ownerAccount));
Id = id;
OwnerAccount = ownerAccount;

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

@ -0,0 +1,10 @@
using System;
using GitHub.Models;
namespace GitHub.Services
{
public interface IAutoCompleteAdvisor
{
IObservable<AutoCompleteResult> GetAutoCompletionSuggestions(string text, int caretPosition);
}
}

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

@ -64,6 +64,14 @@ namespace GitHub.Services
/// <returns></returns>
Task Checkout(IRepository repository, string branchName);
/// <summary>
/// Checks if a commit exists a the repository.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="sha">The SHA of the commit.</param>
/// <returns></returns>
Task<bool> CommitExists(IRepository repository, string sha);
/// <summary>
/// Creates a new branch.
/// </summary>
@ -72,44 +80,6 @@ namespace GitHub.Services
/// <returns></returns>
Task CreateBranch(IRepository repository, string branchName);
/// <summary>
/// Compares two commits.
/// </summary>
/// <param name="repository">The repository</param>
/// <param name="sha1">The SHA of the first commit.</param>
/// <param name="sha2">The SHA of the second commit.</param>
/// <param name="detectRenames">Whether to detect renames</param>
/// <returns>
/// A <see cref="TreeChanges"/> object or null if one of the commits could not be found in the repository,
/// (e.g. it is from a fork).
/// </returns>
Task<TreeChanges> Compare(IRepository repository, string sha1, string sha2, bool detectRenames = false);
/// <summary>
/// Compares a file in two commits.
/// </summary>
/// <param name="repository">The repository</param>
/// <param name="sha1">The SHA of the first commit.</param>
/// <param name="sha2">The SHA of the second commit.</param>
/// <param name="path">The relative path to the file.</param>
/// <returns>
/// A <see cref="Patch"/> object or null if one of the commits could not be found in the repository.
/// </returns>
Task<Patch> Compare(IRepository repository, string sha1, string sha2, string path);
/// <summary>
/// Compares a file in a commit to a string.
/// </summary>
/// <param name="repository">The repository</param>
/// <param name="sha1">The SHA of the first commit.</param>
/// <param name="sha2">The SHA of the second commit.</param>
/// <param name="path">The relative path to the file.</param>
/// <param name="contents">The contents to compare with the file.</param>
/// <returns>
/// A <see cref="Patch"/> object or null if the commit could not be found in the repository.
/// </returns>
Task<ContentChanges> CompareWith(IRepository repository, string sha1, string sha2, string path, byte[] contents);
/// <summary>
/// Gets the value of a configuration key.
/// </summary>

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

@ -0,0 +1,71 @@
using System;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Primitives;
namespace GitHub.Services
{
/// <summary>
/// Services for issues and pull requests.
/// </summary>
public interface IIssueishService
{
/// <summary>
/// Closes an issue or pull request.
/// </summary>
/// <param name="address">The address of the server.</param>
/// <param name="owner">The repository owner.</param>
/// <param name="repository">The repository name.</param>
/// <param name="number">The issue or pull request number.</param>
Task CloseIssueish(HostAddress address, string owner, string repository, int number);
/// <summary>
/// Reopens an issue or pull request.
/// </summary>
/// <param name="address">The address of the server.</param>
/// <param name="owner">The repository owner.</param>
/// <param name="repository">The repository name.</param>
/// <param name="number">The issue or pull request number.</param>
Task ReopenIssueish(HostAddress address, string owner, string repository, int number);
/// <summary>
/// Posts an issue or pull request comment.
/// </summary>
/// <param name="address">The address of the server.</param>
/// <param name="issueishId">The GraphQL ID of the issue or pull request.</param>
/// <param name="body">The comment body.</param>
/// <returns>The model for the comment that was added.</returns>
Task<CommentModel> PostComment(
HostAddress address,
string issueishId,
string body);
/// <summary>
/// Deletes an issue or pull request comment.
/// </summary>
/// <param name="address">The address of the server.</param>
/// <param name="owner">The repository owner.</param>
/// <param name="repository">The repository name.</param>
/// <param name="commentId">The database ID of the comment.</param>
Task DeleteComment(
HostAddress address,
string owner,
string repository,
int commentId);
/// <summary>
/// Edits an issue or pull request comment.
/// </summary>
/// <param name="address">The address of the server.</param>
/// <param name="owner">The repository owner.</param>
/// <param name="repository">The repository name.</param>
/// <param name="commentId">The database ID of the comment.</param>
/// <param name="body">The new comment body.</param>
Task EditComment(
HostAddress address,
string owner,
string repository,
int commentId,
string body);
}
}

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

@ -9,7 +9,7 @@ using LibGit2Sharp;
namespace GitHub.Services
{
public interface IPullRequestService
public interface IPullRequestService : IIssueishService
{
/// <summary>
/// Reads a page of pull request items.
@ -19,13 +19,22 @@ namespace GitHub.Services
/// <param name="name">The repository name.</param>
/// <param name="after">The end cursor of the previous page, or null for the first page.</param>
/// <param name="states">The pull request states to filter by</param>
/// <param name="refresh">Whether the data should be refreshed instead of read from the cache.</param>
/// <returns>A page of pull request item models.</returns>
Task<Page<PullRequestListItemModel>> ReadPullRequests(
HostAddress address,
string owner,
string name,
string after,
PullRequestStateEnum[] states);
PullRequestState[] states);
/// <summary>
/// Clears the cache for <see cref="ReadPullRequests"/>.
/// </summary>
/// <param name="address">The host address.</param>
/// <param name="owner">The repository owner.</param>
/// <param name="name">The repository name.</param>
Task ClearPullRequestsCache(HostAddress address, string owner, string name);
/// <summary>
/// Reads a page of users that can be assigned to pull requests.
@ -69,6 +78,15 @@ namespace GitHub.Services
/// <returns></returns>
IObservable<Unit> Checkout(LocalRepositoryModel repository, PullRequestDetailModel pullRequest, string localBranchName);
/// <summary>
/// Checks if a commit is available and if not tries to fetch the commit.
/// </summary>
/// <param name="localRepository">The local repository.</param>
/// <param name="remoteRepository">The remote repository.</param>
/// <param name="sha">The SHA of the commit.</param>
/// <returns>True if the commit was found, otherwise false.</returns>
Task<bool> FetchCommit(LocalRepositoryModel localRepository, RepositoryModel remoteRepository, string sha);
/// <summary>
/// Carries out a pull on the current branch.
/// </summary>

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

@ -58,6 +58,15 @@ namespace GitHub.Services
/// </returns>
bool DestinationDirectoryExists(string path);
/// <summary>
/// Checks whether the specified destination directory is empty.
/// </summary>
/// <param name="path">The destination path.</param>
/// <returns>
/// true if a directory is empty <paramref name="path"/>; otherwise false.
/// </returns>
bool DestinationDirectoryEmpty(string path);
/// <summary>
/// Checks whether the specified destination file already exists.
/// </summary>
@ -67,6 +76,6 @@ namespace GitHub.Services
/// </returns>
bool DestinationFileExists(string path);
Task<ViewerRepositoriesModel> ReadViewerRepositories(HostAddress address);
Task<ViewerRepositoriesModel> ReadViewerRepositories(HostAddress address, bool refresh = false);
}
}

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

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using ReactiveUI;
namespace GitHub.ViewModels.Dialog.Clone
{

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

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
namespace GitHub.ViewModels.Documents
{
/// <summary>
/// Displays a list of commit summaries in a pull request timeline.
/// </summary>
public interface ICommitListViewModel : IViewModel
{
/// <summary>
/// Gets the first author of the commits in the list.
/// </summary>
ICommitActorViewModel Author { get; }
/// <summary>
/// Gets a string to display the author login or the author name.
/// </summary>
string AuthorName { get; }
/// <summary>
/// Gets a string to display next to the author in the view.
/// </summary>
string AuthorCaption { get; }
/// <summary>
/// Gets the commits.
/// </summary>
IReadOnlyList<ICommitSummaryViewModel> Commits { get; }
}
}

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

@ -0,0 +1,28 @@
namespace GitHub.ViewModels.Documents
{
/// <summary>
/// Displays a one-line summary of a commit in a pull request timeline.
/// </summary>
public interface ICommitSummaryViewModel : IViewModel
{
/// <summary>
/// Gets the abbreviated OID (SHA) of the commit.
/// </summary>
string AbbreviatedOid { get; }
/// <summary>
/// Gets the commit author.
/// </summary>
ICommitActorViewModel Author { get; }
/// <summary>
/// Gets the commit message header.
/// </summary>
string Header { get; }
/// <summary>
/// Gets the OID (SHA) of the commit.
/// </summary>
string Oid { get; }
}
}

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

@ -0,0 +1,17 @@
using System;
using System.Threading.Tasks;
namespace GitHub.ViewModels.Documents
{
/// <summary>
/// A thread of issue or pull request comments.
/// </summary>
public interface IIssueishCommentThreadViewModel : ICommentThreadViewModel
{
/// <summary>
/// Called by a comment in the thread to close the issue or pull request.
/// </summary>
/// <param name="body">The comment requesting the close.</param>
Task CloseOrReopen(ICommentViewModel comment);
}
}

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

@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using ReactiveUI;
namespace GitHub.ViewModels.Documents
{
/// <summary>
/// View model for displaying a pull request in a document window.
/// </summary>
public interface IPullRequestPageViewModel : IPullRequestViewModelBase
{
/// <summary>
/// Gets the number of commits in the pull request.
/// </summary>
int CommitCount { get; }
/// <summary>
/// Gets the pull request's timeline.
/// </summary>
IReadOnlyList<IViewModel> Timeline { get; }
/// <summary>
/// Gets a command that will open a commit in Team Explorer.
/// </summary>
ReactiveCommand<string, Unit> ShowCommit { get; }
/// <summary>
/// Initializes the view model with data.
/// </summary>
/// <param name="repository">The repository to which the pull request belongs.</param>
/// <param name="localRepository">The local repository.</param>
/// <param name="currentUser">The currently logged in user.</param>
/// <param name="model">The pull request model.</param>
Task InitializeAsync(
RemoteRepositoryModel repository,
LocalRepositoryModel localRepository,
ActorModel currentUser,
PullRequestDetailModel model);
}
}

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

@ -4,6 +4,7 @@ using GitHub.Validation;
using ReactiveUI;
using System.Threading.Tasks;
using System.Reactive;
using GitHub.Services;
namespace GitHub.ViewModels.GitHubPane
{
@ -16,6 +17,7 @@ namespace GitHub.ViewModels.GitHubPane
ReactiveCommand<Unit, Unit> Cancel { get; }
string PRTitle { get; set; }
ReactivePropertyValidator TitleValidator { get; }
IAutoCompleteAdvisor AutoCompleteAdvisor { get; }
Task InitializeAsync(LocalRepositoryModel repository, IConnection connection);
}

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

@ -171,6 +171,11 @@ namespace GitHub.ViewModels.GitHubPane
/// </summary>
ReactiveCommand<Unit, Unit> SyncSubmodules { get; }
/// <summary>
/// Gets a command that opens the pull request conversation in a document pane.
/// </summary>
ReactiveCommand<Unit, Unit> OpenConversation { get; }
/// <summary>
/// Gets a command that opens the pull request on GitHub.
/// </summary>

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

@ -15,7 +15,7 @@ namespace GitHub.ViewModels.GitHubPane
ReactiveCommand<Unit, Unit> CreatePullRequest { get; }
/// <summary>
/// Gets a command that opens pull request item on GitHub.
/// Gets a command that opens the pull request item on GitHub.
/// </summary>
ReactiveCommand<IPullRequestListItemViewModel, IPullRequestListItemViewModel> OpenItemInBrowser { get; }
}

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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Reactive;
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using ReactiveUI;
namespace GitHub.ViewModels.GitHubPane
@ -87,6 +88,11 @@ namespace GitHub.ViewModels.GitHubPane
/// </summary>
ReactiveCommand<Unit, Unit> Cancel { get; }
/// <summary>
/// Provides an AutoCompleteAdvisor.
/// </summary>
IAutoCompleteAdvisor AutoCompleteAdvisor { get; }
/// <summary>
/// Initializes the view model for creating a new review.
/// </summary>

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

@ -10,12 +10,7 @@ namespace GitHub.ViewModels
public interface ICommentThreadViewModel : IViewModel
{
/// <summary>
/// Gets the comments in the thread.
/// </summary>
IReadOnlyReactiveList<ICommentViewModel> Comments { get; }
/// <summary>
/// Gets the current user under whos account new comments will be created.
/// Gets the current user under whose account new comments will be created.
/// </summary>
IActorViewModel CurrentUser { get; }

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

@ -1,5 +1,6 @@
using System;
using System.Reactive;
using GitHub.Services;
using ReactiveUI;
namespace GitHub.ViewModels
@ -11,8 +12,8 @@ namespace GitHub.ViewModels
Placeholder,
}
/// <summary>
/// View model for an issue or pull request comment.
/// <summary>
/// View model for an issue, pull request or pull request review comment.
/// </summary>
public interface ICommentViewModel : IViewModel
{
@ -63,7 +64,12 @@ namespace GitHub.ViewModels
bool IsSubmitting { get; }
/// <summary>
/// Gets a value indicating whether the comment can be edited or deleted by the current user
/// Gets a value indicating whether the comment edit state can be canceled.
/// </summary>
bool CanCancel { get; }
/// <summary>
/// Gets a value indicating whether the comment can be edited or deleted by the current user.
/// </summary>
bool CanDelete { get; }
@ -72,6 +78,14 @@ namespace GitHub.ViewModels
/// </summary>
DateTimeOffset CreatedAt { get; }
/// <summary>
/// Gets the caption for the "Commit" button.
/// </summary>
/// <remarks>
/// This will be "Comment" when editing a new comment and "Update" when editing an existing comment.
/// </remarks>
string CommitCaption { get; }
/// <summary>
/// Gets the thread that the comment is a part of.
/// </summary>
@ -106,5 +120,10 @@ namespace GitHub.ViewModels
/// Deletes a comment.
/// </summary>
ReactiveCommand<Unit, Unit> Delete { get; }
/// <summary>
/// Provides an AutoCompleteAdvisor.
/// </summary>
IAutoCompleteAdvisor AutoCompleteAdvisor { get; }
}
}

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

@ -0,0 +1,9 @@
namespace GitHub.ViewModels
{
public interface ICommitActorViewModel : IActorViewModel
{
string Email { get; }
string Name { get; }
bool HasLogin { get; }
}
}

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

@ -0,0 +1,53 @@
using System;
using System.Reactive;
using GitHub.Models;
using ReactiveUI;
namespace GitHub.ViewModels
{
/// <summary>
/// Base interface for issue and pull request view models.
/// </summary>
public interface IIssueishViewModel : IViewModel
{
/// <summary>
/// Gets the GraphQL ID for the issue or pull request.
/// </summary>
string Id { get; }
/// <summary>
/// Gets the issue or pull request author.
/// </summary>
IActorViewModel Author { get; }
/// <summary>
/// Gets the issue or pull request body.
/// </summary>
string Body { get; }
/// <summary>
/// Gets the issue or pull request number.
/// </summary>
int Number { get; }
/// <summary>
/// Gets the repository that the issue or pull request comes from.
/// </summary>
RemoteRepositoryModel Repository { get; }
/// <summary>
/// Gets the issue or pull request title.
/// </summary>
string Title { get; }
/// <summary>
/// Gets the URL of the issue or pull request.
/// </summary>
Uri WebUrl { get; }
/// <summary>
/// Gets a command which opens the issue or pull request in a browser.
/// </summary>
ReactiveCommand<Unit, Unit> OpenOnGitHub { get; }
}
}

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

@ -2,6 +2,7 @@
using System.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using ReactiveUI;
namespace GitHub.ViewModels
{
@ -10,6 +11,11 @@ namespace GitHub.ViewModels
/// </summary>
public interface IPullRequestReviewCommentThreadViewModel : ICommentThreadViewModel
{
/// <summary>
/// Gets the comments in the thread.
/// </summary>
IReadOnlyReactiveList<ICommentViewModel> Comments { get; }
/// <summary>
/// Gets the current pull request review session.
/// </summary>
@ -30,6 +36,11 @@ namespace GitHub.ViewModels
/// </summary>
DiffSide Side { get; }
/// <summary>
/// Gets a value indicating whether comment thread has been marked as resolved by a user.
/// </summary>
bool IsResolved { get; }
/// <summary>
/// Gets a value indicating whether the thread is a new thread being authored, that is not
/// yet present on the server.

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше