Merge branch 'master' into docs-changes

This commit is contained in:
Jamie Cansdale 2018-09-28 17:07:25 +01:00 коммит произвёл GitHub
Родитель 6e1a1a2023 34b1fe8226
Коммит e953a1a530
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
30 изменённых файлов: 532 добавлений и 239 удалений

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

@ -16,7 +16,7 @@ namespace GitHub.SampleData.Dialog.Clone
}
public string Path { get; set; }
public string PathError { get; set; }
public string PathWarning { get; set; }
public int SelectedTabIndex { get; set; }
public string Title => null;
public IObservable<object> Done => null;

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

@ -29,9 +29,13 @@ namespace GitHub.Services
this.showDialog = showDialog;
}
public async Task<CloneDialogResult> ShowCloneDialog(IConnection connection)
public async Task<CloneDialogResult> ShowCloneDialog(IConnection connection, string url = null)
{
var viewModel = factory.CreateViewModel<IRepositoryCloneViewModel>();
if (url != null)
{
viewModel.UrlTab.Url = url;
}
if (connection != null)
{

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

@ -34,6 +34,7 @@ namespace GitHub.Services
readonly IOperatingSystem operatingSystem;
readonly string defaultClonePath;
readonly IVSGitServices vsGitServices;
readonly ITeamExplorerServices teamExplorerServices;
readonly IGraphQLClientFactory graphqlFactory;
readonly IUsageTracker usageTracker;
ICompiledQuery<ViewerRepositoriesModel> readViewerRepositories;
@ -42,11 +43,13 @@ namespace GitHub.Services
public RepositoryCloneService(
IOperatingSystem operatingSystem,
IVSGitServices vsGitServices,
ITeamExplorerServices teamExplorerServices,
IGraphQLClientFactory graphqlFactory,
IUsageTracker usageTracker)
{
this.operatingSystem = operatingSystem;
this.vsGitServices = vsGitServices;
this.teamExplorerServices = teamExplorerServices;
this.graphqlFactory = graphqlFactory;
this.usageTracker = usageTracker;
@ -103,6 +106,54 @@ namespace GitHub.Services
return result;
}
/// <inheritdoc/>
public async Task CloneOrOpenRepository(
CloneDialogResult cloneDialogResult,
object progress = null)
{
Guard.ArgumentNotNull(cloneDialogResult, nameof(cloneDialogResult));
var repositoryPath = cloneDialogResult.Path;
var url = cloneDialogResult.Url;
if (DestinationFileExists(repositoryPath))
{
throw new InvalidOperationException("Can't clone or open a repository because a file exists at: " + repositoryPath);
}
var repositoryUrl = url.ToRepositoryUrl();
var isDotCom = HostAddress.IsGitHubDotComUri(repositoryUrl);
if (DestinationDirectoryExists(repositoryPath))
{
teamExplorerServices.OpenRepository(repositoryPath);
if (isDotCom)
{
await usageTracker.IncrementCounter(x => x.NumberOfGitHubOpens);
}
else
{
await usageTracker.IncrementCounter(x => x.NumberOfEnterpriseOpens);
}
}
else
{
var cloneUrl = repositoryUrl.ToString();
await CloneRepository(cloneUrl, repositoryPath, progress).ConfigureAwait(true);
if (isDotCom)
{
await usageTracker.IncrementCounter(x => x.NumberOfGitHubClones);
}
else
{
await usageTracker.IncrementCounter(x => x.NumberOfEnterpriseClones);
}
}
teamExplorerServices.ShowHomePage();
}
/// <inheritdoc/>
public async Task CloneRepository(
string cloneUrl,
@ -121,19 +172,12 @@ namespace GitHub.Services
try
{
await vsGitServices.Clone(cloneUrl, repositoryPath, true, progress);
await usageTracker.IncrementCounter(x => x.NumberOfClones);
var repositoryUrl = new UriString(cloneUrl).ToRepositoryUrl();
var isDotCom = HostAddress.IsGitHubDotComUri(repositoryUrl);
if (isDotCom)
if (repositoryPath.StartsWith(DefaultClonePath, StringComparison.OrdinalIgnoreCase))
{
await usageTracker.IncrementCounter(x => x.NumberOfGitHubClones);
}
else
{
// If it isn't a GitHub URL, assume it's an Enterprise URL
await usageTracker.IncrementCounter(x => x.NumberOfEnterpriseClones);
// Count the number of times users clone into the Default Repository Location
await usageTracker.IncrementCounter(x => x.NumberOfClonesToDefaultClonePath);
}
}
catch (Exception ex)
@ -144,7 +188,10 @@ namespace GitHub.Services
}
/// <inheritdoc/>
public bool DestinationExists(string path) => Directory.Exists(path) || File.Exists(path);
public bool DestinationDirectoryExists(string path) => operatingSystem.Directory.DirectoryExists(path);
/// <inheritdoc/>
public bool DestinationFileExists(string path) => operatingSystem.File.Exists(path);
string GetLocalClonePathFromGitProvider(string fallbackPath)
{

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

@ -25,7 +25,7 @@ namespace GitHub.Services
CannotDropFolder,
CannotDropFolderUnauthorizedAccess,
ClipboardFailed,
ClonedFailed,
CloneOrOpenFailed,
CloneFailedNotLoggedIn,
CommitCreateFailed,
CommitRevertFailed,
@ -123,7 +123,7 @@ namespace GitHub.Services
},
{ ErrorType.ClipboardFailed, Map(Defaults("Failed to copy text to the clipboard.")) },
{
ErrorType.ClonedFailed, Map(Defaults("Failed to clone the repository '{0}'", "Email support@github.com if you continue to have problems."),
ErrorType.CloneOrOpenFailed, Map(Defaults("Failed to clone or open the repository '{0}'", "Email support@github.com if you continue to have problems."),
new[]
{
new Translation(@"fatal: bad config file line (\d+) in (.+)", "Failed to clone the repository '{0}'", @"The config file '$2' is corrupted at line $1. You may need to open the file and try to fix any errors."),

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

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Globalization;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using GitHub.App;
using GitHub.Extensions;
using GitHub.Logging;
using GitHub.Models;
@ -23,12 +23,13 @@ namespace GitHub.ViewModels.Dialog.Clone
readonly IOperatingSystem os;
readonly IConnectionManager connectionManager;
readonly IRepositoryCloneService service;
readonly IGitService gitService;
readonly IUsageService usageService;
readonly IUsageTracker usageTracker;
readonly IReadOnlyList<IRepositoryCloneTabViewModel> tabs;
string path;
IRepositoryModel previousRepository;
ObservableAsPropertyHelper<string> pathError;
ObservableAsPropertyHelper<string> pathWarning;
int selectedTabIndex;
[ImportingConstructor]
@ -36,6 +37,7 @@ namespace GitHub.ViewModels.Dialog.Clone
IOperatingSystem os,
IConnectionManager connectionManager,
IRepositoryCloneService service,
IGitService gitService,
IUsageService usageService,
IUsageTracker usageTracker,
IRepositorySelectViewModel gitHubTab,
@ -45,6 +47,7 @@ namespace GitHub.ViewModels.Dialog.Clone
this.os = os;
this.connectionManager = connectionManager;
this.service = service;
this.gitService = gitService;
this.usageService = usageService;
this.usageTracker = usageTracker;
@ -59,22 +62,27 @@ namespace GitHub.ViewModels.Dialog.Clone
Path = service.DefaultClonePath;
repository.Subscribe(x => UpdatePath(x));
pathError = Observable.CombineLatest(
pathWarning = Observable.CombineLatest(
repository,
this.WhenAnyValue(x => x.Path),
ValidatePath)
.ToProperty(this, x => x.PathError);
ValidatePathWarning)
.ToProperty(this, x => x.PathWarning);
var canClone = Observable.CombineLatest(
repository,
this.WhenAnyValue(x => x.PathError),
(repo, error) => (repo, error))
.Select(x => x.repo != null && x.error == null);
repository, this.WhenAnyValue(x => x.Path),
(repo, path) => repo != null && !service.DestinationFileExists(path) && !service.DestinationDirectoryExists(path));
var canOpen = Observable.CombineLatest(
repository, this.WhenAnyValue(x => x.Path),
(repo, path) => repo != null && !service.DestinationFileExists(path) && service.DestinationDirectoryExists(path));
Browse = ReactiveCommand.Create().OnExecuteCompleted(_ => BrowseForDirectory());
Clone = ReactiveCommand.CreateAsyncObservable(
canClone,
_ => repository.Select(x => new CloneDialogResult(Path, x)));
_ => repository.Select(x => new CloneDialogResult(Path, x?.CloneUrl)));
Open = ReactiveCommand.CreateAsyncObservable(
canOpen,
_ => repository.Select(x => new CloneDialogResult(Path, x?.CloneUrl)));
}
public IRepositorySelectViewModel GitHubTab { get; }
@ -87,7 +95,7 @@ namespace GitHub.ViewModels.Dialog.Clone
set => this.RaiseAndSetIfChanged(ref path, value);
}
public string PathError => pathError.Value;
public string PathWarning => pathWarning.Value;
public int SelectedTabIndex
{
@ -95,14 +103,16 @@ namespace GitHub.ViewModels.Dialog.Clone
set => this.RaiseAndSetIfChanged(ref selectedTabIndex, value);
}
public string Title => Resources.CloneTitle;
public string Title => Resources.OpenFromGitHubTitle;
public IObservable<object> Done => Clone;
public IObservable<object> Done => Observable.Merge(Clone, Open);
public ReactiveCommand<object> Browse { get; }
public ReactiveCommand<CloneDialogResult> Clone { get; }
public ReactiveCommand<CloneDialogResult> Open { get; }
public async Task InitializeAsync(IConnection connection)
{
var connections = await connectionManager.GetLoadedConnections().ConfigureAwait(false);
@ -228,13 +238,39 @@ namespace GitHub.ViewModels.Dialog.Clone
}
}
string ValidatePath(IRepositoryModel repository, string path)
string ValidatePathWarning(IRepositoryModel repositoryModel, string path)
{
if (repository != null)
if (repositoryModel != null)
{
return service.DestinationExists(path) ?
Resources.DestinationAlreadyExists :
null;
if (service.DestinationFileExists(path))
{
return Resources.DestinationAlreadyExists;
}
if (service.DestinationDirectoryExists(path))
{
using (var repository = gitService.GetRepository(path))
{
if (repository == null)
{
return Resources.CantFindARepositoryAtLocalPath;
}
var localUrl = gitService.GetRemoteUri(repository)?.ToRepositoryUrl();
if (localUrl == null)
{
return Resources.LocalRepositoryDoesntHaveARemoteOrigin;
}
var targetUrl = repositoryModel.CloneUrl?.ToRepositoryUrl();
if (localUrl != targetUrl)
{
return string.Format(CultureInfo.CurrentCulture, Resources.LocalRepositoryHasARemoteOf, localUrl);
}
return Resources.YouHaveAlreadyClonedToThisLocation;
}
}
}
return null;

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

@ -34,13 +34,36 @@ namespace GitHub.Services
object progress = null);
/// <summary>
/// Checks whether the specified destination path already exists.
/// Clones the specified repository into the specified directory or opens it if the directory already exists.
/// </summary>
/// <param name="cloneDialogResult">The URL and path of the repository to clone or open.</param>
/// <param name="progress">
/// An object through which to report progress. This must be of type
/// System.IProgress&lt;Microsoft.VisualStudio.Shell.ServiceProgressData&gt;, but
/// as that type is only available in VS2017+ it is typed as <see cref="object"/> here.
/// </param>
/// <returns></returns>
Task CloneOrOpenRepository(
CloneDialogResult cloneDialogResult,
object progress = null);
/// <summary>
/// Checks whether the specified destination directory already exists.
/// </summary>
/// <param name="path">The destination path.</param>
/// <returns>
/// true if a file or directory is already present at <paramref name="path"/>; otherwise false.
/// true if a directory is already present at <paramref name="path"/>; otherwise false.
/// </returns>
bool DestinationExists(string path);
bool DestinationDirectoryExists(string path);
/// <summary>
/// Checks whether the specified destination file already exists.
/// </summary>
/// <param name="path">The destination file.</param>
/// <returns>
/// true if a file is already present at <paramref name="path"/>; otherwise false.
/// </returns>
bool DestinationFileExists(string path);
Task<ViewerRepositoriesModel> ReadViewerRepositories(HostAddress address);
}

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

@ -5,7 +5,7 @@ using ReactiveUI;
namespace GitHub.ViewModels.Dialog.Clone
{
/// <summary>
/// ViewModel for the the Clone Repository dialog
/// ViewModel for the Clone Repository dialog
/// </summary>
public interface IRepositoryCloneViewModel : IDialogContentViewModel, IConnectionInitializedViewModel
{
@ -30,9 +30,9 @@ namespace GitHub.ViewModels.Dialog.Clone
string Path { get; set; }
/// <summary>
/// Gets an error message that explains why <see cref="Path"/> is not valid.
/// Gets a warning message that explains why <see cref="Path"/> is suspect.
/// </summary>
string PathError { get; }
string PathWarning { get; }
/// <summary>
/// Gets the index of the selected tab.

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

@ -1,4 +1,5 @@
using System;
using GitHub.Primitives;
namespace GitHub.Models
{
@ -12,10 +13,10 @@ namespace GitHub.Models
/// </summary>
/// <param name="path">The path to clone the repository to.</param>
/// <param name="repository">The selected repository.</param>
public CloneDialogResult(string path, IRepositoryModel repository)
public CloneDialogResult(string path, UriString cloneUrl)
{
Path = path;
Repository = repository;
Url = cloneUrl;
}
/// <summary>
@ -24,8 +25,8 @@ namespace GitHub.Models
public string Path { get; }
/// <summary>
/// Gets the repository selected by the user.
/// Gets the url selected by the user.
/// </summary>
public IRepositoryModel Repository { get; }
public UriString Url { get; }
}
}

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

@ -91,6 +91,9 @@ namespace GitHub.Models
public int NumberOfCloneViewUrlTab { get; set; }
public int NumberOfGitHubClones { get; set; }
public int NumberOfEnterpriseClones { get; set; }
public int NumberOfGitHubOpens { get; set; }
public int NumberOfEnterpriseOpens { get; set; }
public int NumberOfClonesToDefaultClonePath { get; set; }
}
}
}

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

@ -16,11 +16,14 @@ namespace GitHub.Services
/// The connection to use. If null, the first connection will be used, or the user promted
/// to log in if there are no connections.
/// </param>
/// <param name="url">
/// The URL to prepopulate URL field with or null.
/// </param>
/// <returns>
/// A task that returns an instance of <see cref="CloneDialogResult"/> on success,
/// or null if the dialog was cancelled.
/// </returns>
Task<CloneDialogResult> ShowCloneDialog(IConnection connection);
Task<CloneDialogResult> ShowCloneDialog(IConnection connection, string url = null);
/// <summary>
/// Shows the re-clone dialog.

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

@ -5,7 +5,9 @@ namespace GitHub.Services
public interface ITeamExplorerServices : INotificationService
{
void ShowConnectPage();
void ShowHomePage();
void ShowPublishSection();
void ClearNotifications();
void OpenRepository(string repositoryPath);
}
}

47
src/GitHub.Resources/Resources.Designer.cs сгенерированный
Просмотреть файл

@ -249,6 +249,15 @@ namespace GitHub {
}
}
/// <summary>
/// Looks up a localized string similar to There is already a directory at this location, but it doesn&apos;t contain a repository..
/// </summary>
public static string CantFindARepositoryAtLocalPath {
get {
return ResourceManager.GetString("CantFindARepositoryAtLocalPath", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Changes ({0}).
/// </summary>
@ -505,7 +514,7 @@ namespace GitHub {
}
/// <summary>
/// Looks up a localized string similar to The destination already exists..
/// Looks up a localized string similar to A file exists at the destination path..
/// </summary>
public static string DestinationAlreadyExists {
get {
@ -873,6 +882,24 @@ namespace GitHub {
}
}
/// <summary>
/// Looks up a localized string similar to A repository already exists at this location, but it doesn&apos;t have a remote named &quot;origin&quot;..
/// </summary>
public static string LocalRepositoryDoesntHaveARemoteOrigin {
get {
return ResourceManager.GetString("LocalRepositoryDoesntHaveARemoteOrigin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A repository already exists at this location, but it has a remote of {0}..
/// </summary>
public static string LocalRepositoryHasARemoteOf {
get {
return ResourceManager.GetString("LocalRepositoryHasARemoteOf", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Make sure to use your password and not a Personal Access token to sign in..
/// </summary>
@ -1188,6 +1215,15 @@ namespace GitHub {
}
}
/// <summary>
/// Looks up a localized string similar to Open from GitHub.
/// </summary>
public static string OpenFromGitHubTitle {
get {
return ResourceManager.GetString("OpenFromGitHubTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open in Browser.
/// </summary>
@ -2144,6 +2180,15 @@ namespace GitHub {
}
}
/// <summary>
/// Looks up a localized string similar to You have already cloned to this location. Click &apos;Open&apos; to open the local repository..
/// </summary>
public static string YouHaveAlreadyClonedToThisLocation {
get {
return ResourceManager.GetString("YouHaveAlreadyClonedToThisLocation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You must commit and push your changes to add a comment here..
/// </summary>

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

@ -123,6 +123,9 @@
<data name="CloneTitle" xml:space="preserve">
<value>Clone a Repository</value>
</data>
<data name="OpenFromGitHubTitle" xml:space="preserve">
<value>Open from GitHub</value>
</data>
<data name="CouldNotConnectToGitHub" xml:space="preserve">
<value>Could not connect to github.com</value>
</data>
@ -328,7 +331,7 @@ https://git-scm.com/download/win</value>
<value>Cancel Review</value>
</data>
<data name="DestinationAlreadyExists" xml:space="preserve">
<value>The destination already exists.</value>
<value>A file exists at the destination path.</value>
</data>
<data name="LogoutRequired" xml:space="preserve">
<value>Logout Required</value>
@ -821,4 +824,16 @@ https://git-scm.com/download/win</value>
<data name="NoResolveSameOwnerMessage" xml:space="preserve">
<value>Couldn't find target URL in current repository. Try again after doing a fetch.</value>
</data>
<data name="CantFindARepositoryAtLocalPath" xml:space="preserve">
<value>There is already a directory at this location, but it doesn't contain a repository.</value>
</data>
<data name="LocalRepositoryDoesntHaveARemoteOrigin" xml:space="preserve">
<value>A repository already exists at this location, but it doesn't have a remote named "origin".</value>
</data>
<data name="LocalRepositoryHasARemoteOf" xml:space="preserve">
<value>A repository already exists at this location, but it has a remote of {0}.</value>
</data>
<data name="YouHaveAlreadyClonedToThisLocation" xml:space="preserve">
<value>You have already cloned to this location. Click 'Open' to open the local repository.</value>
</data>
</root>

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

@ -123,6 +123,9 @@
<data name="CloneTitle" xml:space="preserve">
<value>克隆一个 版本库</value>
</data>
<data name="OpenFromGitHubTitle" xml:space="preserve">
<value>Open from GitHub</value>
</data>
<data name="CouldNotConnectToGitHub" xml:space="preserve">
<value>无法连接到github.com</value>
</data>
@ -327,10 +330,10 @@
<value>取消评审</value>
</data>
<data name="DestinationAlreadyExists" xml:space="preserve">
<value>The destination already exists.</value>
<value>A file exists at the destination path.</value>
</data>
<data name="LogoutRequired" xml:space="preserve">
<value>Logout Required</value>
<value>需要注销</value>
</data>
<data name="day" xml:space="preserve">
<value>{0:N0} 天前</value>
@ -345,7 +348,7 @@
<value>{0:N0} 小时前</value>
</data>
<data name="JustNow" xml:space="preserve">
<value>just now</value>
<value>就现在</value>
</data>
<data name="minute" xml:space="preserve">
<value>{0:N0} 分钟前</value>
@ -820,4 +823,16 @@
<data name="NoResolveSameOwnerMessage" xml:space="preserve">
<value>无法在当前存储库中找到目标URL。在进行提取后再试一次。</value>
</data>
<data name="CantFindARepositoryAtLocalPath" xml:space="preserve">
<value>There is already a directory at this location, but it doesn't contain a repository.</value>
</data>
<data name="LocalRepositoryDoesntHaveARemoteOrigin" xml:space="preserve">
<value>A repository already exists at this location, but it doesn't have a remote named "origin".</value>
</data>
<data name="LocalRepositoryHasARemoteOf" xml:space="preserve">
<value>A repository already exists at this location, but it has a remote of {0}.</value>
</data>
<data name="YouHaveAlreadyClonedToThisLocation" xml:space="preserve">
<value>You have already cloned to this location. Click 'Open' to open the local repository.</value>
</data>
</root>

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

@ -70,11 +70,12 @@ namespace GitHub.StartPage
if (request == null)
return null;
var uri = request.Repository.CloneUrl.ToRepositoryUrl();
var uri = request.Url.ToRepositoryUrl();
var repositoryName = request.Url.RepositoryName;
return new CodeContainer(
localProperties: new CodeContainerLocalProperties(request.Path, CodeContainerType.Folder,
new CodeContainerSourceControlProperties(request.Repository.Name, request.Path, new Guid(Guids.GitSccProviderId))),
remote: new RemoteCodeContainer(request.Repository.Name,
new CodeContainerSourceControlProperties(repositoryName, request.Path, new Guid(Guids.GitSccProviderId))),
remote: new RemoteCodeContainer(repositoryName,
new Guid(Guids.CodeContainerProviderId),
uri,
new Uri(uri.ToString().TrimSuffix(".git")),
@ -133,7 +134,7 @@ namespace GitHub.StartPage
if (basePath != null)
{
var path = Path.Combine(basePath, repository.Name);
result = new CloneDialogResult(path, repository);
result = new CloneDialogResult(path, repository.CloneUrl);
}
}
@ -141,17 +142,13 @@ namespace GitHub.StartPage
{
try
{
await cloneService.CloneRepository(
result.Repository.CloneUrl,
result.Path,
progress);
await cloneService.CloneOrOpenRepository(result, progress);
usageTracker.IncrementCounter(x => x.NumberOfStartPageClones).Forget();
}
catch
{
var teServices = gitHubServiceProvider.TryGetService<ITeamExplorerServices>();
teServices.ShowError($"Failed to clone the repository '{result.Repository.Name}'");
teServices.ShowError($"Failed to clone the repository '{result.Url.RepositoryName}'");
result = null;
}
}

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

@ -151,16 +151,14 @@ namespace GitHub.VisualStudio.TeamExplorer.Connect
{
ServiceProvider.GitServiceProvider = TEServiceProvider;
var cloneService = ServiceProvider.GetService<IRepositoryCloneService>();
await cloneService.CloneRepository(
result.Repository.CloneUrl,
result.Path);
await cloneService.CloneOrOpenRepository(result);
usageTracker.IncrementCounter(x => x.NumberOfGitHubConnectSectionClones).Forget();
}
catch (Exception e)
{
var teServices = ServiceProvider.TryGetService<ITeamExplorerServices>();
teServices.ShowError(e.GetUserFriendlyErrorMessage(ErrorType.ClonedFailed, result.Repository.Name));
teServices.ShowError(e.GetUserFriendlyErrorMessage(ErrorType.CloneOrOpenFailed, result.Url.RepositoryName));
}
}
}

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

@ -52,6 +52,10 @@
</PropertyGroup>
<Import Project="$(SolutionDir)\src\common\signing.props" />
<ItemGroup>
<Reference Include="EnvDTE, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\..\packages\EnvDTE.8.0.2\lib\net10\EnvDTE.dll</HintPath>
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="LibGit2Sharp, Version=0.23.1.0, Culture=neutral, PublicKeyToken=7cbde695407f0333, processorArchitecture=MSIL">
<HintPath>..\..\packages\LibGit2Sharp.0.23.1\lib\net40\LibGit2Sharp.dll</HintPath>
<Private>True</Private>
@ -131,6 +135,10 @@
<HintPath>..\..\packages\Stateless.2.5.56.0\lib\portable-net40+sl50+win+wp80+MonoAndroid10+xamarinios10+MonoTouch10\Stateless.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="stdole, Version=7.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\..\packages\stdole.7.0.3302\lib\net10\stdole.dll</HintPath>
<EmbedInteropTypes>True</EmbedInteropTypes>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Core" />

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

@ -2,10 +2,9 @@ using System;
using System.ComponentModel.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Input;
using GitHub.Extensions;
using EnvDTE;
using GitHub.VisualStudio.TeamExplorer.Sync;
using Microsoft.TeamFoundation.Controls;
using Microsoft.VisualStudio.Shell;
namespace GitHub.Services
{
@ -29,12 +28,28 @@ namespace GitHub.Services
this.serviceProvider = serviceProvider;
}
public void OpenRepository(string repositoryPath)
{
#if TEAMEXPLORER14
var vsServices = serviceProvider.GetService<IVSServices>();
vsServices.TryOpenRepository(repositoryPath);
#else
OpenFolder(repositoryPath);
#endif
}
public void ShowConnectPage()
{
var te = serviceProvider.TryGetService<ITeamExplorer>();
te.NavigateToPage(new Guid(TeamExplorerPageIds.Connect), null);
}
public void ShowHomePage()
{
var te = serviceProvider.TryGetService<ITeamExplorer>();
te.NavigateToPage(new Guid(TeamExplorerPageIds.Home), null);
}
public void ShowPublishSection()
{
var te = serviceProvider.TryGetService<ITeamExplorer>();
@ -89,5 +104,11 @@ namespace GitHub.Services
manager = serviceProvider.GetService<ITeamExplorer, ITeamExplorerNotificationManager>();
return manager?.IsNotificationVisible(guid) ?? false;
}
void OpenFolder(string repositoryPath)
{
var dte = serviceProvider.TryGetService<DTE>();
dte?.ExecuteCommand("File.OpenFolder", repositoryPath);
}
}
}

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

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EnvDTE" version="8.0.2" targetFramework="net461" />
<package id="LibGit2Sharp" version="0.23.1" targetFramework="net461" />
<package id="LibGit2Sharp.NativeBinaries" version="1.0.164" targetFramework="net461" />
<package id="Microsoft.VisualStudio.ComponentModelHost" version="14.0.25424" targetFramework="net461" />
@ -18,4 +19,5 @@
<package id="Serilog" version="2.5.0" targetFramework="net461" />
<package id="SerilogAnalyzer" version="0.12.0.0" targetFramework="net461" />
<package id="Stateless" version="2.5.56.0" targetFramework="net461" />
<package id="stdole" version="7.0.3302" targetFramework="net461" />
</packages>

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

@ -52,6 +52,10 @@
</PropertyGroup>
<Import Project="$(SolutionDir)\src\common\signing.props" />
<ItemGroup>
<Reference Include="EnvDTE, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\..\packages\EnvDTE.8.0.2\lib\net10\EnvDTE.dll</HintPath>
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="LibGit2Sharp, Version=0.23.1.0, Culture=neutral, PublicKeyToken=7cbde695407f0333, processorArchitecture=MSIL">
<HintPath>..\..\packages\LibGit2Sharp.0.23.1\lib\net40\LibGit2Sharp.dll</HintPath>
<Private>True</Private>
@ -150,6 +154,10 @@
<HintPath>..\..\packages\Stateless.2.5.56.0\lib\portable-net40+sl50+win+wp80+MonoAndroid10+xamarinios10+MonoTouch10\Stateless.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="stdole, Version=7.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\..\packages\stdole.7.0.3302\lib\net10\stdole.dll</HintPath>
<EmbedInteropTypes>True</EmbedInteropTypes>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Core" />

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

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EnvDTE" version="8.0.2" targetFramework="net461" />
<package id="LibGit2Sharp" version="0.23.1" targetFramework="net461" />
<package id="LibGit2Sharp.NativeBinaries" version="1.0.164" targetFramework="net461" />
<package id="Microsoft.VisualStudio.CoreUtility" version="15.4.27004" targetFramework="net461" />
@ -24,4 +25,5 @@
<package id="Serilog" version="2.5.0" targetFramework="net461" />
<package id="SerilogAnalyzer" version="0.12.0.0" targetFramework="net461" />
<package id="Stateless" version="2.5.56.0" targetFramework="net461" />
<package id="stdole" version="7.0.3302" targetFramework="net461" />
</packages>

11
src/GitHub.UI/TestAutomation/AutomationIDs.Designer.cs сгенерированный
Просмотреть файл

@ -19,7 +19,7 @@ namespace GitHub.UI.TestAutomation {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class AutomationIDs {
@ -717,6 +717,15 @@ namespace GitHub.UI.TestAutomation {
}
}
/// <summary>
/// Looks up a localized string similar to OpenRepositoryButton.
/// </summary>
public static string OpenRepositoryButton {
get {
return ResourceManager.GetString("OpenRepositoryButton", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerfulCollaborationTextBlock.
/// </summary>

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

@ -516,4 +516,7 @@
<data name="TwoFactorAuthenticationInputStackPanel" xml:space="preserve">
<value>TwoFactorAuthenticationInputStackPanel</value>
</data>
<data name="OpenRepositoryButton" xml:space="preserve">
<value>OpenRepositoryButton</value>
</data>
</root>

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

@ -1,30 +1,18 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.ComponentModel.Composition;
using GitHub.Commands;
using GitHub.Services;
using GitHub.ViewModels.GitHubPane;
using GitHub.Services.Vssdk.Commands;
using EnvDTE;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Task = System.Threading.Tasks.Task;
using SVsServiceProvider = Microsoft.VisualStudio.Shell.SVsServiceProvider;
using GitHub.VisualStudio.UI;
namespace GitHub.VisualStudio.Commands
{
[Export(typeof(IOpenFromUrlCommand))]
public class OpenFromUrlCommand : VsCommand<string>, IOpenFromUrlCommand
{
readonly Lazy<IDialogService> dialogService;
readonly Lazy<IGitHubContextService> gitHubContextService;
readonly Lazy<IRepositoryCloneService> repositoryCloneService;
readonly Lazy<ITeamExplorerContext> teamExplorerContext;
readonly Lazy<IGitHubToolWindowManager> gitHubToolWindowManager;
readonly Lazy<DTE> dte;
readonly IServiceProvider serviceProvider;
/// <summary>
/// Gets the GUID of the group the command belongs to.
@ -38,19 +26,14 @@ namespace GitHub.VisualStudio.Commands
[ImportingConstructor]
public OpenFromUrlCommand(
Lazy<IDialogService> dialogService,
Lazy<IGitHubContextService> gitHubContextService,
Lazy<IRepositoryCloneService> repositoryCloneService,
Lazy<ITeamExplorerContext> teamExplorerContext,
[Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) :
Lazy<IRepositoryCloneService> repositoryCloneService) :
base(CommandSet, CommandId)
{
this.dialogService = dialogService;
this.gitHubContextService = gitHubContextService;
this.repositoryCloneService = repositoryCloneService;
this.teamExplorerContext = teamExplorerContext;
this.serviceProvider = serviceProvider;
dte = new Lazy<DTE>(() => (DTE)serviceProvider.GetService(typeof(DTE)));
gitHubToolWindowManager = new Lazy<IGitHubToolWindowManager>(
() => (IGitHubToolWindowManager)serviceProvider.GetService(typeof(IGitHubToolWindowManager)));
// See https://code.msdn.microsoft.com/windowsdesktop/AllowParams-2005-9442298f
ParametersDescription = "u"; // accept a single url
@ -58,132 +41,17 @@ namespace GitHub.VisualStudio.Commands
public override async Task Execute(string url)
{
var context = string.IsNullOrEmpty(url) ? null : gitHubContextService.Value.FindContextFromUrl(url);
if (context == null)
if (string.IsNullOrEmpty(url))
{
context = gitHubContextService.Value.FindContextFromClipboard();
var clipboardContext = gitHubContextService.Value.FindContextFromClipboard();
url = clipboardContext?.Url;
}
if (context == null)
var cloneDialogResult = await dialogService.Value.ShowCloneDialog(null, url);
if (cloneDialogResult != null)
{
// Couldn't find a URL to open
return;
await repositoryCloneService.Value.CloneOrOpenRepository(cloneDialogResult);
}
var activeDir = teamExplorerContext.Value.ActiveRepository?.LocalPath;
if (activeDir != null)
{
// Try opening file in current context
if (gitHubContextService.Value.TryOpenFile(activeDir, context))
{
return;
}
}
// Keep repos in unique dir while testing
var defaultSubPath = "GitHubCache";
var cloneUrl = gitHubContextService.Value.ToRepositoryUrl(context).ToString();
var targetDir = Path.Combine(repositoryCloneService.Value.DefaultClonePath, defaultSubPath, context.Owner);
var repositoryDirName = context.RepositoryName;
var repositoryDir = Path.Combine(targetDir, repositoryDirName);
if (!Directory.Exists(repositoryDir))
{
var result = ShowInfoMessage($"Clone {cloneUrl} to '{repositoryDir}'?");
switch (result)
{
case VSConstants.MessageBoxResult.IDYES:
await repositoryCloneService.Value.CloneRepository(cloneUrl, repositoryDir);
// Open the cloned repository
dte.Value.ExecuteCommand("File.OpenFolder", repositoryDir);
dte.Value.ExecuteCommand("View.TfsTeamExplorer");
break;
case VSConstants.MessageBoxResult.IDNO:
// Target the current solution
repositoryDir = FindSolutionDirectory(dte.Value.Solution);
if (repositoryDir == null)
{
// No current solution to use
return;
}
break;
case VSConstants.MessageBoxResult.IDCANCEL:
return;
}
}
var solutionDir = FindSolutionDirectory(dte.Value.Solution);
if (solutionDir == null || !ContainsDirectory(repositoryDir, solutionDir))
{
var result = ShowInfoMessage(string.Format(Resources.OpenRepositoryAtDir, repositoryDir));
switch (result)
{
case VSConstants.MessageBoxResult.IDYES:
// Open if current solution isn't in repository directory
dte.Value.ExecuteCommand("File.OpenFolder", repositoryDir);
dte.Value.ExecuteCommand("View.TfsTeamExplorer");
break;
case VSConstants.MessageBoxResult.IDNO:
break;
case VSConstants.MessageBoxResult.IDCANCEL:
return;
}
}
await TryOpenPullRequest(context);
gitHubContextService.Value.TryOpenFile(repositoryDir, context);
}
VSConstants.MessageBoxResult ShowInfoMessage(string message)
{
return (VSConstants.MessageBoxResult)VsShellUtilities.ShowMessageBox(serviceProvider, message, null,
OLEMSGICON.OLEMSGICON_QUERY, OLEMSGBUTTON.OLEMSGBUTTON_YESNOCANCEL, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}
static bool ContainsDirectory(string repositoryDir, string solutionDir)
{
if (solutionDir.Equals(repositoryDir, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (solutionDir.StartsWith(repositoryDir + '\\', StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
static string FindSolutionDirectory(Solution solution)
{
var solutionPath = solution.FileName;
if (File.Exists(solutionPath))
{
return Path.GetDirectoryName(solutionPath);
}
if (Directory.Exists(solutionPath))
{
return solutionPath;
}
return null;
}
async Task<bool> TryOpenPullRequest(GitHubContext context)
{
var pullRequest = context.PullRequest;
if (pullRequest == null)
{
return false;
}
var host = await gitHubToolWindowManager.Value.ShowGitHubPane();
await host.ShowPullRequest(context.Owner, context.RepositoryName, pullRequest.Value);
return true;
}
}
}

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

@ -362,11 +362,9 @@
</CommandPlacement>
<!-- Open from GitHub command -->
<!-- Removing from `File > Open` menu until we have a proper UI
<CommandPlacement guid="guidGitHubCmdSet" id="openFromUrlCommand" priority="0x0100">
<Parent guid="guidSHLMainMenu" id="IDG_VS_FILE_OPENSCC_CASCADE"/>
</CommandPlacement>
-->
<!-- Add Connection (Team Explorer) command -->
<CommandPlacement guid="guidGitHubCmdSet" id="addConnectionCommand" priority="0x0100">

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

@ -21,9 +21,8 @@
</UserControl.Resources>
<DockPanel>
<ghfvs:InfoPanel Message="{Binding PathError}"/>
<ghfvs:OcticonCircleButton DockPanel.Dock="Bottom"
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" HorizontalAlignment="Center">
<ghfvs:OcticonCircleButton
Margin="12"
HorizontalAlignment="Center"
Icon="check"
@ -32,6 +31,16 @@
Content="Clone"
AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.CloneRepositoryButton}"/>
<ghfvs:OcticonCircleButton
Margin="12"
HorizontalAlignment="Center"
Icon="check"
IsDefault="True"
Command="{Binding Open}"
Content="Open"
AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.OpenRepositoryButton}"/>
</StackPanel>
<DockPanel DockPanel.Dock="Bottom"
Margin="16">
<Label DockPanel.Dock="Left" Content="{x:Static ghfvs:Resources.localPathText}"/>
@ -44,6 +53,10 @@
<TextBox Text="{Binding Path, UpdateSourceTrigger=PropertyChanged}"/>
</DockPanel>
<ghfvs:InfoPanel Message="{Binding PathWarning}"
DockPanel.Dock="Bottom"
Margin="0"/>
<TabControl SelectedIndex="{Binding SelectedTabIndex}"
Style="{StaticResource LightModalViewTabControl}">
<TabItem Header="GitHub.com"

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

@ -28,19 +28,20 @@ public class RepositoryCloneServiceTests
}
[TestCase("https://github.com/foo/bar", 1, nameof(UsageModel.MeasuresModel.NumberOfClones))]
[TestCase("https://github.com/foo/bar", 1, nameof(UsageModel.MeasuresModel.NumberOfGitHubClones))]
[TestCase("https://github.com/foo/bar", 0, nameof(UsageModel.MeasuresModel.NumberOfGitHubClones))]
[TestCase("https://github.com/foo/bar", 0, nameof(UsageModel.MeasuresModel.NumberOfEnterpriseClones))]
[TestCase("https://enterprise.com/foo/bar", 1, nameof(UsageModel.MeasuresModel.NumberOfClones))]
[TestCase("https://enterprise.com/foo/bar", 1, nameof(UsageModel.MeasuresModel.NumberOfEnterpriseClones))]
[TestCase("https://enterprise.com/foo/bar", 0, nameof(UsageModel.MeasuresModel.NumberOfEnterpriseClones))]
[TestCase("https://enterprise.com/foo/bar", 0, nameof(UsageModel.MeasuresModel.NumberOfGitHubClones))]
public async Task UpdatesMetricsWhenRepositoryClonedAsync(string cloneUrl, int numberOfCalls, string counterName)
{
var serviceProvider = Substitutes.ServiceProvider;
var operatingSystem = serviceProvider.GetOperatingSystem();
var vsGitServices = serviceProvider.GetVSGitServices();
var teamExplorerServices = Substitute.For<ITeamExplorerServices>();
var graphqlFactory = Substitute.For<IGraphQLClientFactory>();
var usageTracker = Substitute.For<IUsageTracker>();
var cloneService = new RepositoryCloneService(operatingSystem, vsGitServices, graphqlFactory, usageTracker);
var cloneService = new RepositoryCloneService(operatingSystem, vsGitServices, teamExplorerServices, graphqlFactory, usageTracker);
await cloneService.CloneRepository(cloneUrl, @"c:\dev\bar");
var model = UsageModel.Create(Guid.NewGuid());
@ -49,5 +50,59 @@ public class RepositoryCloneServiceTests
Arg.Is<Expression<Func<UsageModel.MeasuresModel, int>>>(x =>
((MemberExpression)x.Body).Member.Name == counterName));
}
[TestCase("https://github.com/foo/bar", false, 1, nameof(UsageModel.MeasuresModel.NumberOfClones))]
[TestCase("https://github.com/foo/bar", false, 1, nameof(UsageModel.MeasuresModel.NumberOfGitHubClones))]
[TestCase("https://github.com/foo/bar", false, 0, nameof(UsageModel.MeasuresModel.NumberOfEnterpriseClones))]
[TestCase("https://enterprise.com/foo/bar", false, 1, nameof(UsageModel.MeasuresModel.NumberOfClones))]
[TestCase("https://enterprise.com/foo/bar", false, 1, nameof(UsageModel.MeasuresModel.NumberOfEnterpriseClones))]
[TestCase("https://enterprise.com/foo/bar", false, 0, nameof(UsageModel.MeasuresModel.NumberOfGitHubClones))]
[TestCase("https://github.com/foo/bar", true, 1, nameof(UsageModel.MeasuresModel.NumberOfGitHubOpens))]
[TestCase("https://github.com/foo/bar", true, 0, nameof(UsageModel.MeasuresModel.NumberOfEnterpriseOpens))]
[TestCase("https://enterprise.com/foo/bar", true, 1, nameof(UsageModel.MeasuresModel.NumberOfEnterpriseOpens))]
[TestCase("https://enterprise.com/foo/bar", true, 0, nameof(UsageModel.MeasuresModel.NumberOfGitHubOpens))]
public async Task UpdatesMetricsWhenCloneOrOpenRepositoryAsync(string cloneUrl, bool dirExists, int numberOfCalls, string counterName)
{
var repositoryPath = @"c:\dev\bar";
var cloneDialogResult = new CloneDialogResult(repositoryPath, cloneUrl);
var serviceProvider = Substitutes.ServiceProvider;
var operatingSystem = serviceProvider.GetOperatingSystem();
operatingSystem.Directory.DirectoryExists(repositoryPath).Returns(dirExists);
var vsGitServices = serviceProvider.GetVSGitServices();
var teamExplorerServices = Substitute.For<ITeamExplorerServices>();
var graphqlFactory = Substitute.For<IGraphQLClientFactory>();
var usageTracker = Substitute.For<IUsageTracker>();
var cloneService = new RepositoryCloneService(operatingSystem, vsGitServices, teamExplorerServices,
graphqlFactory, usageTracker);
await cloneService.CloneOrOpenRepository(cloneDialogResult);
await usageTracker.Received(numberOfCalls).IncrementCounter(
Arg.Is<Expression<Func<UsageModel.MeasuresModel, int>>>(x =>
((MemberExpression)x.Body).Member.Name == counterName));
}
[TestCase(@"c:\default\repo", @"c:\default", 1, nameof(UsageModel.MeasuresModel.NumberOfClonesToDefaultClonePath))]
[TestCase(@"c:\not_default\repo", @"c:\default", 0, nameof(UsageModel.MeasuresModel.NumberOfClonesToDefaultClonePath))]
public async Task UpdatesMetricsWhenDefaultClonePath(string targetPath, string defaultPath, int numberOfCalls, string counterName)
{
var serviceProvider = Substitutes.ServiceProvider;
var operatingSystem = serviceProvider.GetOperatingSystem();
var vsGitServices = serviceProvider.GetVSGitServices();
var teamExplorerServices = Substitute.For<ITeamExplorerServices>();
vsGitServices.GetLocalClonePathFromGitProvider().Returns(defaultPath);
var graphqlFactory = Substitute.For<IGraphQLClientFactory>();
var usageTracker = Substitute.For<IUsageTracker>();
var cloneService = new RepositoryCloneService(operatingSystem, vsGitServices, teamExplorerServices,
graphqlFactory, usageTracker);
await cloneService.CloneRepository("https://github.com/foo/bar", targetPath);
var model = UsageModel.Create(Guid.NewGuid());
await usageTracker.Received(numberOfCalls).IncrementCounter(
Arg.Is<Expression<Func<UsageModel.MeasuresModel, int>>>(x =>
((MemberExpression)x.Body).Member.Name == counterName));
}
}
}

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

@ -31,7 +31,7 @@ namespace UnitTests
}
// public static IGitRepositoriesExt IGitRepositoriesExt { get { return Substitute.For<IGitRepositoriesExt>(); } }
// public static IGitRepositoriesExt IGitRepositoriesExt { get { return Substitute.For<IGitRepositoriesExt>(); } }
public static IGitService IGitService { get { return Substitute.For<IGitService>(); } }
public static IVSGitServices IVSGitServices
@ -72,7 +72,7 @@ namespace UnitTests
/// RepositoryCloneService and RepositoryCreationService, which are real
/// instances.
/// </summary>
public static IGitHubServiceProvider ServiceProvider { get { return GetServiceProvider(); } }
public static IGitHubServiceProvider ServiceProvider { get { return GetServiceProvider(); } }
/// <summary>
/// This returns a service provider with mocked IRepositoryCreationService and
@ -111,7 +111,8 @@ namespace UnitTests
var os = OperatingSystem;
var vsgit = IVSGitServices;
var clone = cloneService ?? new RepositoryCloneService(os, vsgit, Substitute.For<IGraphQLClientFactory>(), Substitute.For<IUsageTracker>());
var clone = cloneService ?? new RepositoryCloneService(os, vsgit, Substitute.For<ITeamExplorerServices>(),
Substitute.For<IGraphQLClientFactory>(), Substitute.For<IUsageTracker>());
var create = creationService ?? new RepositoryCreationService(clone);
avatarProvider = avatarProvider ?? Substitute.For<IAvatarProvider>();
//ret.GetService(typeof(IGitRepositoriesExt)).Returns(IGitRepositoriesExt);

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

@ -1,14 +1,16 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Globalization;
using System.IO;
using System.Linq.Expressions;
using System.Numerics;
using System.Reactive.Linq;
using System.Threading.Tasks;
using GitHub.Extensions;
using GitHub.Models;
using GitHub.Primitives;
using GitHub.Services;
using GitHub.ViewModels.Dialog.Clone;
using LibGit2Sharp;
using NSubstitute;
using NUnit.Framework;
using Rothko;
@ -17,6 +19,10 @@ namespace GitHub.App.UnitTests.ViewModels.Dialog.Clone
{
public class RepositoryCloneViewModelTests
{
const string directoryExists = "d:\\exists\\directory";
const string fileExists = "d:\\exists\\file";
const string defaultPath = "d:\\default\\path";
[Test]
public async Task GitHubPage_Is_Initialized()
{
@ -95,7 +101,7 @@ namespace GitHub.App.UnitTests.ViewModels.Dialog.Clone
var cm = CreateConnectionManager("https://github.com", "https://enterprise.com");
var target = CreateTarget(connectionManager: cm);
await target.InitializeAsync(null);
await target.InitializeAsync(cm.Connections[0]);
await target.GitHubTab.Received(1).Activate();
await target.EnterpriseTab.DidNotReceive().Activate();
@ -146,37 +152,97 @@ namespace GitHub.App.UnitTests.ViewModels.Dialog.Clone
{
var target = CreateTarget();
Assert.That(target.Path, Is.EqualTo("d:\\efault\\path"));
Assert.That(target.Path, Is.EqualTo(defaultPath));
}
[Test]
public async Task Owner_And_Repository_Name_Is_Appended_To_Base_Path()
{
var owner = "owner";
var repo = "repo";
var target = CreateTarget();
var expectPath = Path.Combine(defaultPath, owner, repo);
SetRepository(target.GitHubTab, CreateRepositoryModel("owner", "repo"));
SetRepository(target.GitHubTab, CreateRepositoryModel(owner, repo));
Assert.That(target.Path, Is.EqualTo("d:\\efault\\path\\owner\\repo"));
Assert.That(target.Path, Is.EqualTo(expectPath));
}
[Test]
public async Task PathError_Is_Not_Set_When_No_Repository_Selected()
public async Task PathWarning_Is_Not_Set_When_No_Repository_Selected()
{
var target = CreateTarget();
target.Path = "d:\\exists";
target.Path = directoryExists;
Assert.That(target.PathError, Is.Null);
Assert.That(target.PathWarning, Is.Null);
}
[Test]
public async Task PathError_Is_Set_For_Existing_Destination()
public async Task PathWarning_Is_Set_For_Existing_File_At_Destination()
{
var target = CreateTarget();
SetRepository(target.GitHubTab, CreateRepositoryModel("owner", "repo"));
target.Path = "d:\\exists";
target.Path = fileExists;
Assert.That(target.PathError, Is.EqualTo(Resources.DestinationAlreadyExists));
Assert.That(target.PathWarning, Is.EqualTo(Resources.DestinationAlreadyExists));
}
[Test]
public async Task PathWarning_Is_Set_For_Existing_Clone_At_Destination()
{
var owner = "owner";
var repo = "repo";
var remoteUrl = CreateGitHubUrl("owner", "repo");
var gitService = CreateGitService(true, remoteUrl);
var target = CreateTarget(gitService: gitService);
SetRepository(target.GitHubTab, CreateRepositoryModel(owner, repo));
target.Path = directoryExists;
Assert.That(target.PathWarning, Is.EqualTo(Resources.YouHaveAlreadyClonedToThisLocation));
}
[Test]
public async Task PathWarning_Is_Set_For_Repository_With_No_Origin()
{
var owner = "owner";
var repo = "repo";
var gitService = CreateGitService(true, null);
var target = CreateTarget(gitService: gitService);
SetRepository(target.GitHubTab, CreateRepositoryModel(owner, repo));
target.Path = directoryExists;
Assert.That(target.PathWarning, Is.EqualTo(Resources.LocalRepositoryDoesntHaveARemoteOrigin));
}
[Test]
public async Task PathWarning_Is_Set_For_Directory_With_No_Repository()
{
var owner = "owner";
var repo = "repo";
var gitService = CreateGitService(false, null);
var target = CreateTarget(gitService: gitService);
SetRepository(target.GitHubTab, CreateRepositoryModel(owner, repo));
target.Path = directoryExists;
Assert.That(target.PathWarning, Is.EqualTo(Resources.CantFindARepositoryAtLocalPath));
}
[Test]
public async Task PathWarning_Is_Set_For_Existing_Repository_At_Destination_With_Different_Remote()
{
var originalOwner = "original_Owner";
var forkedOwner = "forked_owner";
var repo = "repo";
var forkedUrl = CreateGitHubUrl(forkedOwner, repo);
var expectMessage = string.Format(CultureInfo.CurrentCulture, Resources.LocalRepositoryHasARemoteOf, forkedUrl);
var gitService = CreateGitService(true, CreateGitHubUrl(forkedOwner, repo));
var target = CreateTarget(gitService: gitService);
SetRepository(target.GitHubTab, CreateRepositoryModel(originalOwner, repo));
target.Path = directoryExists;
Assert.That(target.PathWarning, Is.EqualTo(expectMessage));
}
[Test]
@ -249,7 +315,7 @@ namespace GitHub.App.UnitTests.ViewModels.Dialog.Clone
}
[Test]
public async Task Clone_Is_Disabled_When_Has_PathError()
public async Task Clone_Is_Disabled_When_Path_DirectoryExists()
{
var target = CreateTarget();
@ -258,11 +324,25 @@ namespace GitHub.App.UnitTests.ViewModels.Dialog.Clone
SetRepository(target.GitHubTab, CreateRepositoryModel());
Assert.That(target.Clone.CanExecute(null), Is.True);
target.Path = "d:\\exists";
target.Path = directoryExists;
Assert.That(target.Clone.CanExecute(null), Is.False);
}
[Test]
public async Task Open_Is_Enabled_When_Path_DirectoryExists()
{
var target = CreateTarget();
await target.InitializeAsync(null);
Assert.That(target.Open.CanExecute(null), Is.False);
SetRepository(target.GitHubTab, CreateRepositoryModel());
target.Path = directoryExists;
Assert.That(target.Open.CanExecute(null), Is.True);
}
static void SetRepository(IRepositoryCloneTabViewModel vm, IRepositoryModel repository)
{
vm.Repository.Returns(repository);
@ -305,7 +385,10 @@ namespace GitHub.App.UnitTests.ViewModels.Dialog.Clone
{
var result = Substitute.For<IRepositoryCloneService>();
result.DefaultClonePath.Returns(defaultClonePath);
result.DestinationExists("d:\\exists").Returns(true);
result.DestinationDirectoryExists(directoryExists).Returns(true);
result.DestinationFileExists(directoryExists).Returns(false);
result.DestinationDirectoryExists(fileExists).Returns(false);
result.DestinationFileExists(fileExists).Returns(true);
return result;
}
@ -317,8 +400,9 @@ namespace GitHub.App.UnitTests.ViewModels.Dialog.Clone
IUsageTracker usageTracker = null,
IRepositorySelectViewModel gitHubTab = null,
IRepositorySelectViewModel enterpriseTab = null,
IGitService gitService = null,
IRepositoryUrlViewModel urlTab = null,
string defaultClonePath = "d:\\efault\\path")
string defaultClonePath = defaultPath)
{
os = os ?? Substitute.For<IOperatingSystem>();
connectionManager = connectionManager ?? CreateConnectionManager("https://github.com");
@ -327,12 +411,14 @@ namespace GitHub.App.UnitTests.ViewModels.Dialog.Clone
usageTracker = usageTracker ?? Substitute.For<IUsageTracker>();
gitHubTab = gitHubTab ?? CreateSelectViewModel();
enterpriseTab = enterpriseTab ?? CreateSelectViewModel();
urlTab = urlTab ?? Substitute.For<IRepositoryUrlViewModel>();
gitService = gitService ?? CreateGitService(true, "https://github.com/owner/repo");
urlTab = urlTab ?? CreateRepositoryUrlViewModel();
return new RepositoryCloneViewModel(
os,
connectionManager,
service,
gitService,
usageService,
usageTracker,
gitHubTab,
@ -340,6 +426,21 @@ namespace GitHub.App.UnitTests.ViewModels.Dialog.Clone
urlTab);
}
private static IGitService CreateGitService(bool repositoryExists, UriString remoteUrl)
{
var gitService = Substitute.For<IGitService>();
IRepository repository = null;
if (repositoryExists)
{
repository = Substitute.For<IRepository>();
gitService.GetRemoteUri(repository).Returns(remoteUrl);
}
gitService.GetRepository(directoryExists).Returns(repository);
return gitService;
}
static IUsageService CreateUsageService(bool isGroupA = false)
{
var usageService = Substitute.For<IUsageService>();
@ -362,7 +463,21 @@ namespace GitHub.App.UnitTests.ViewModels.Dialog.Clone
var repository = Substitute.For<IRepositoryModel>();
repository.Owner.Returns(owner);
repository.Name.Returns(name);
repository.CloneUrl.Returns(CreateGitHubUrl(owner, name));
return repository;
}
static UriString CreateGitHubUrl(string owner, string repo)
{
return new UriString($"https://github.com/{owner}/{repo}");
}
static IRepositoryUrlViewModel CreateRepositoryUrlViewModel()
{
var repositoryUrlViewModel = Substitute.For<IRepositoryUrlViewModel>();
repositoryUrlViewModel.Repository.Returns(null as IRepositoryModel);
repositoryUrlViewModel.Url.Returns(string.Empty);
return repositoryUrlViewModel;
}
}
}

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

@ -31,7 +31,7 @@ namespace UnitTests
}
// public static IGitRepositoriesExt IGitRepositoriesExt { get { return Substitute.For<IGitRepositoriesExt>(); } }
// public static IGitRepositoriesExt IGitRepositoriesExt { get { return Substitute.For<IGitRepositoriesExt>(); } }
public static IGitService IGitService { get { return Substitute.For<IGitService>(); } }
public static IVSGitServices IVSGitServices
@ -72,7 +72,7 @@ namespace UnitTests
/// RepositoryCloneService and RepositoryCreationService, which are real
/// instances.
/// </summary>
public static IGitHubServiceProvider ServiceProvider { get { return GetServiceProvider(); } }
public static IGitHubServiceProvider ServiceProvider { get { return GetServiceProvider(); } }
/// <summary>
/// This returns a service provider with mocked IRepositoryCreationService and
@ -111,7 +111,8 @@ namespace UnitTests
var os = OperatingSystem;
var vsgit = IVSGitServices;
var clone = cloneService ?? new RepositoryCloneService(os, vsgit, Substitute.For<IGraphQLClientFactory>(), Substitute.For<IUsageTracker>());
var clone = cloneService ?? new RepositoryCloneService(os, vsgit, Substitute.For<ITeamExplorerServices>(),
Substitute.For<IGraphQLClientFactory>(), Substitute.For<IUsageTracker>());
var create = creationService ?? new RepositoryCreationService(clone);
avatarProvider = avatarProvider ?? Substitute.For<IAvatarProvider>();
//ret.GetService(typeof(IGitRepositoriesExt)).Returns(IGitRepositoriesExt);