diff --git a/src/GitHub.App/Controllers/UIController.cs b/src/GitHub.App/Controllers/UIController.cs index aa4d1ce3ae..00351ac46b 100644 --- a/src/GitHub.App/Controllers/UIController.cs +++ b/src/GitHub.App/Controllers/UIController.cs @@ -420,7 +420,7 @@ void ConfigureLogicStates() ConfigureSingleViewLogic(UIControllerFlow.PullRequestList, UIViewType.PRList); ConfigureSingleViewLogic(UIControllerFlow.PullRequestDetail, UIViewType.PRDetail); ConfigureSingleViewLogic(UIControllerFlow.PullRequestCreation, UIViewType.PRCreation); - ConfigureSingleViewLogic(UIControllerFlow.StartPageClone, UIViewType.StartPageClone); + ConfigureSingleViewLogic(UIControllerFlow.ReClone, UIViewType.StartPageClone); } void ConfigureSingleViewLogic(UIControllerFlow flow, UIViewType type) diff --git a/src/GitHub.App/GitHub.App.csproj b/src/GitHub.App/GitHub.App.csproj index f5b0da547f..426f6b410e 100644 --- a/src/GitHub.App/GitHub.App.csproj +++ b/src/GitHub.App/GitHub.App.csproj @@ -160,6 +160,7 @@ + diff --git a/src/GitHub.App/SampleData/SampleViewModels.cs b/src/GitHub.App/SampleData/SampleViewModels.cs index a4980f8f62..95856c5b8e 100644 --- a/src/GitHub.App/SampleData/SampleViewModels.cs +++ b/src/GitHub.App/SampleData/SampleViewModels.cs @@ -18,6 +18,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; +using System.Threading.Tasks; namespace GitHub.SampleData { @@ -353,7 +354,7 @@ public RepositoryCloneViewModelDesigner() .IfPathNotRooted("Please enter a valid path"); } - public IReactiveCommand CloneCommand + public IReactiveCommand CloneCommand { get; private set; @@ -484,10 +485,6 @@ public ObservableCollection Repositories get; set; } - public void DoClone() - { - } - public void DoCreate() { } @@ -506,6 +503,7 @@ public bool OpenRepository() } public IConnection SectionConnection { get; } + public ICommand Clone { get; } } public class InfoPanelDesigner diff --git a/src/GitHub.App/SampleData/StartPageCloneViewModelDesigner.cs b/src/GitHub.App/SampleData/StartPageCloneViewModelDesigner.cs index d5232f9b16..87e1d5a376 100644 --- a/src/GitHub.App/SampleData/StartPageCloneViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/StartPageCloneViewModelDesigner.cs @@ -13,7 +13,7 @@ public class StartPageCloneViewModelDesigner : BaseViewModel, IBaseCloneViewMode public string BaseRepositoryPath { get; set; } public ReactivePropertyValidator BaseRepositoryPathValidator { get; } public ICommand BrowseForDirectory { get; } - public IReactiveCommand CloneCommand { get; } + public IReactiveCommand CloneCommand { get; } public IRepositoryModel SelectedRepository { get; set; } } } diff --git a/src/GitHub.App/Services/DialogService.cs b/src/GitHub.App/Services/DialogService.cs new file mode 100644 index 0000000000..9d937c4e54 --- /dev/null +++ b/src/GitHub.App/Services/DialogService.cs @@ -0,0 +1,72 @@ +using System; +using System.ComponentModel.Composition; +using System.Threading.Tasks; +using GitHub.Models; +using GitHub.UI; +using GitHub.ViewModels; +using NullGuard; + +namespace GitHub.Services +{ + [Export(typeof(IDialogService))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class DialogService : IDialogService + { + readonly IUIProvider uiProvider; + + [ImportingConstructor] + public DialogService(IUIProvider uiProvider) + { + this.uiProvider = uiProvider; + } + + public Task ShowCloneDialog([AllowNull] IConnection connection) + { + var controller = uiProvider.Configure(UIControllerFlow.Clone, connection); + var basePath = default(string); + var repository = default(IRepositoryModel); + + controller.TransitionSignal.Subscribe(x => + { + var vm = x.View.ViewModel as IBaseCloneViewModel; + + x.View.Done.Subscribe(_ => + { + basePath = vm?.BaseRepositoryPath; + repository = vm?.SelectedRepository; + }); + }); + + uiProvider.RunInDialog(controller); + + var result = repository != null && basePath != null ? + new CloneDialogResult(basePath, repository) : null; + return Task.FromResult(result); + } + + public Task ShowReCloneDialog(IRepositoryModel repository) + { + var controller = uiProvider.Configure(UIControllerFlow.ReClone); + var basePath = default(string); + + controller.TransitionSignal.Subscribe(x => + { + var vm = x.View.ViewModel as IBaseCloneViewModel; + + if (vm != null) + { + vm.SelectedRepository = repository; + } + + x.View.Done.Subscribe(_ => + { + basePath = vm?.BaseRepositoryPath; + }); + }); + + uiProvider.RunInDialog(controller); + + return Task.FromResult(basePath); + } + } +} diff --git a/src/GitHub.App/Services/RepositoryCloneService.cs b/src/GitHub.App/Services/RepositoryCloneService.cs index f9a361c1aa..7b8d30d37e 100644 --- a/src/GitHub.App/Services/RepositoryCloneService.cs +++ b/src/GitHub.App/Services/RepositoryCloneService.cs @@ -8,6 +8,7 @@ using NLog; using Rothko; using GitHub.Helpers; +using Task = System.Threading.Tasks.Task; namespace GitHub.Services { @@ -25,45 +26,50 @@ public class RepositoryCloneService : IRepositoryCloneService readonly IOperatingSystem operatingSystem; readonly string defaultClonePath; readonly IVSGitServices vsGitServices; + readonly IUsageTracker usageTracker; [ImportingConstructor] - public RepositoryCloneService(IOperatingSystem operatingSystem, IVSGitServices vsGitServices) + public RepositoryCloneService( + IOperatingSystem operatingSystem, + IVSGitServices vsGitServices, + IUsageTracker usageTracker) { this.operatingSystem = operatingSystem; this.vsGitServices = vsGitServices; + this.usageTracker = usageTracker; defaultClonePath = GetLocalClonePathFromGitProvider(operatingSystem.Environment.GetUserRepositoriesPath()); } - public IObservable CloneRepository(string cloneUrl, string repositoryName, string repositoryPath) + /// + public async Task CloneRepository( + string cloneUrl, + string repositoryName, + string repositoryPath, + object progress = null) { Guard.ArgumentNotEmptyString(cloneUrl, nameof(cloneUrl)); Guard.ArgumentNotEmptyString(repositoryName, nameof(repositoryName)); Guard.ArgumentNotEmptyString(repositoryPath, nameof(repositoryPath)); - return Observable.StartAsync(async () => - { - string path = Path.Combine(repositoryPath, repositoryName); - - operatingSystem.Directory.CreateDirectory(path); + string path = Path.Combine(repositoryPath, repositoryName); - // Once we've done IO switch to the main thread to call vsGitServices.Clone() as this must be - // called on the main thread. - await ThreadingHelper.SwitchToMainThreadAsync(); + // 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(path); + await ThreadingHelper.SwitchToMainThreadAsync(); - try - { - // this will throw if it can't find it - vsGitServices.Clone(cloneUrl, path, true); - } - catch (Exception ex) - { - log.Error("Could not clone {0} to {1}. {2}", cloneUrl, path, ex); - throw; - } - - return Unit.Default; - }); + try + { + await vsGitServices.Clone(cloneUrl, path, true, progress); + await usageTracker.IncrementCloneCount(); + } + catch (Exception ex) + { + log.Error("Could not clone {0} to {1}. {2}", cloneUrl, path, ex); + throw; + } } string GetLocalClonePathFromGitProvider(string fallbackPath) diff --git a/src/GitHub.App/ViewModels/RepositoryCloneViewModel.cs b/src/GitHub.App/ViewModels/RepositoryCloneViewModel.cs index fc8dbce2dc..0e609f356c 100644 --- a/src/GitHub.App/ViewModels/RepositoryCloneViewModel.cs +++ b/src/GitHub.App/ViewModels/RepositoryCloneViewModel.cs @@ -33,10 +33,7 @@ public class RepositoryCloneViewModel : BaseViewModel, IRepositoryCloneViewModel static readonly Logger log = LogManager.GetCurrentClassLogger(); readonly IRepositoryHost repositoryHost; - readonly IRepositoryCloneService cloneService; readonly IOperatingSystem operatingSystem; - readonly INotificationService notificationService; - readonly IUsageTracker usageTracker; readonly ReactiveCommand browseForDirectoryCommand = ReactiveCommand.Create(); bool isLoading; bool noRepositoriesFound; @@ -48,25 +45,18 @@ public class RepositoryCloneViewModel : BaseViewModel, IRepositoryCloneViewModel RepositoryCloneViewModel( IConnectionRepositoryHostMap connectionRepositoryHostMap, IRepositoryCloneService repositoryCloneService, - IOperatingSystem operatingSystem, - INotificationService notificationService, - IUsageTracker usageTracker) - : this(connectionRepositoryHostMap.CurrentRepositoryHost, repositoryCloneService, operatingSystem, notificationService, usageTracker) + IOperatingSystem operatingSystem) + : this(connectionRepositoryHostMap.CurrentRepositoryHost, repositoryCloneService, operatingSystem) { } public RepositoryCloneViewModel( IRepositoryHost repositoryHost, IRepositoryCloneService cloneService, - IOperatingSystem operatingSystem, - INotificationService notificationService, - IUsageTracker usageTracker) + IOperatingSystem operatingSystem) { this.repositoryHost = repositoryHost; - this.cloneService = cloneService; this.operatingSystem = operatingSystem; - this.notificationService = notificationService; - this.usageTracker = usageTracker; Title = string.Format(CultureInfo.CurrentCulture, Resources.CloneTitle, repositoryHost.Title); @@ -121,7 +111,7 @@ public RepositoryCloneViewModel( x => x.BaseRepositoryPathValidator.ValidationResult.IsValid, (x, y) => x.Value != null && y.Value); canClone = canCloneObservable.ToProperty(this, x => x.CanClone); - CloneCommand = ReactiveCommand.CreateAsyncObservable(canCloneObservable, OnCloneRepository); + CloneCommand = ReactiveCommand.Create(canCloneObservable); browseForDirectoryCommand.Subscribe(_ => ShowBrowseForDirectoryDialog()); this.WhenAny(x => x.BaseRepositoryPathValidator.ValidationResult, x => x.Value) @@ -158,38 +148,6 @@ bool FilterRepository(IRemoteRepositoryModel repo, int position, IList OnCloneRepository(object state) - { - return Observable.Start(() => - { - var repository = SelectedRepository; - Debug.Assert(repository != null, "Should not be able to attempt to clone a repo when it's null"); - if (repository == null) - { - notificationService.ShowError(Resources.RepositoryCloneFailedNoSelectedRepo); - return Observable.Return(Unit.Default); - } - - // The following is a noop if the directory already exists. - operatingSystem.Directory.CreateDirectory(BaseRepositoryPath); - - return cloneService.CloneRepository(repository.CloneUrl, repository.Name, BaseRepositoryPath) - .ContinueAfter(() => - { - usageTracker.IncrementCloneCount().Forget(); - return Observable.Return(Unit.Default); - }); - }) - .SelectMany(_ => _) - .Catch(e => - { - var repository = SelectedRepository; - Debug.Assert(repository != null, "Should not be able to attempt to clone a repo when it's null"); - notificationService.ShowError(e.GetUserFriendlyErrorMessage(ErrorType.ClonedFailed, repository.Name)); - return Observable.Return(Unit.Default); - }); - } - bool IsAlreadyRepoAtPath(string path) { Debug.Assert(path != null, "RepositoryCloneViewModel.IsAlreadyRepoAtPath cannot be passed null as a path parameter."); @@ -244,9 +202,9 @@ public string BaseRepositoryPath } /// - /// Fires off the cloning process + /// Signals that the user clicked the clone button. /// - public IReactiveCommand CloneCommand { get; private set; } + public IReactiveCommand CloneCommand { get; private set; } TrackingCollection repositories; public ObservableCollection Repositories diff --git a/src/GitHub.App/ViewModels/StartPageCloneViewModel.cs b/src/GitHub.App/ViewModels/StartPageCloneViewModel.cs index 76c4a63423..c7d49a9e65 100644 --- a/src/GitHub.App/ViewModels/StartPageCloneViewModel.cs +++ b/src/GitHub.App/ViewModels/StartPageCloneViewModel.cs @@ -1,17 +1,13 @@ using System; -using System.Collections.Generic; using System.ComponentModel.Composition; using System.Diagnostics; using System.Globalization; using System.IO; -using System.Linq; using System.Reactive; using System.Reactive.Linq; -using System.Text.RegularExpressions; using System.Windows.Input; using GitHub.App; using GitHub.Exports; -using GitHub.Extensions; using GitHub.Models; using GitHub.Services; using GitHub.Validation; @@ -19,10 +15,6 @@ using NullGuard; using ReactiveUI; using Rothko; -using System.Collections.ObjectModel; -using GitHub.Collections; -using GitHub.UI; -using GitHub.Extensions.Reactive; namespace GitHub.ViewModels { @@ -32,10 +24,7 @@ public class StartPageCloneViewModel : BaseViewModel, IBaseCloneViewModel { static readonly Logger log = LogManager.GetCurrentClassLogger(); - readonly IRepositoryCloneService cloneService; readonly IOperatingSystem operatingSystem; - readonly INotificationService notificationService; - readonly IUsageTracker usageTracker; readonly ReactiveCommand browseForDirectoryCommand = ReactiveCommand.Create(); readonly ObservableAsPropertyHelper canClone; string baseRepositoryPath; @@ -44,23 +33,16 @@ public class StartPageCloneViewModel : BaseViewModel, IBaseCloneViewModel StartPageCloneViewModel( IConnectionRepositoryHostMap connectionRepositoryHostMap, IRepositoryCloneService repositoryCloneService, - IOperatingSystem operatingSystem, - INotificationService notificationService, - IUsageTracker usageTracker) - : this(connectionRepositoryHostMap.CurrentRepositoryHost, repositoryCloneService, operatingSystem, notificationService, usageTracker) + IOperatingSystem operatingSystem) + : this(connectionRepositoryHostMap.CurrentRepositoryHost, repositoryCloneService, operatingSystem) { } public StartPageCloneViewModel( IRepositoryHost repositoryHost, IRepositoryCloneService cloneService, - IOperatingSystem operatingSystem, - INotificationService notificationService, - IUsageTracker usageTracker) + IOperatingSystem operatingSystem) { - this.cloneService = cloneService; this.operatingSystem = operatingSystem; - this.notificationService = notificationService; - this.usageTracker = usageTracker; Title = string.Format(CultureInfo.CurrentCulture, Resources.CloneTitle, repositoryHost.Title); @@ -81,7 +63,7 @@ public StartPageCloneViewModel( x => x.BaseRepositoryPathValidator.ValidationResult.IsValid, (x, y) => x.Value != null && y.Value); canClone = canCloneObservable.ToProperty(this, x => x.CanClone); - CloneCommand = ReactiveCommand.CreateAsyncObservable(canCloneObservable, OnCloneRepository); + CloneCommand = ReactiveCommand.Create(canCloneObservable); browseForDirectoryCommand.Subscribe(_ => ShowBrowseForDirectoryDialog()); this.WhenAny(x => x.BaseRepositoryPathValidator.ValidationResult, x => x.Value) @@ -89,39 +71,6 @@ public StartPageCloneViewModel( BaseRepositoryPath = cloneService.DefaultClonePath; } - - IObservable OnCloneRepository(object state) - { - return Observable.Start(() => - { - var repository = SelectedRepository; - Debug.Assert(repository != null, "Should not be able to attempt to clone a repo when it's null"); - if (repository == null) - { - notificationService.ShowError(Resources.RepositoryCloneFailedNoSelectedRepo); - return Observable.Return(Unit.Default); - } - - // The following is a noop if the directory already exists. - operatingSystem.Directory.CreateDirectory(BaseRepositoryPath); - - return cloneService.CloneRepository(repository.CloneUrl, repository.Name, BaseRepositoryPath) - .ContinueAfter(() => - { - usageTracker.IncrementCloneCount().Forget(); - return Observable.Return(Unit.Default); - }); - }) - .SelectMany(_ => _) - .Catch(e => - { - var repository = SelectedRepository; - Debug.Assert(repository != null, "Should not be able to attempt to clone a repo when it's null"); - notificationService.ShowError(e.GetUserFriendlyErrorMessage(ErrorType.ClonedFailed, repository.Name)); - return Observable.Return(Unit.Default); - }); - } - bool IsAlreadyRepoAtPath(string path) { Debug.Assert(path != null, "RepositoryCloneViewModel.IsAlreadyRepoAtPath cannot be passed null as a path parameter."); @@ -176,9 +125,9 @@ public string BaseRepositoryPath } /// - /// Fires off the cloning process + /// Signals that the user clicked the clone button. /// - public IReactiveCommand CloneCommand { get; private set; } + public IReactiveCommand CloneCommand { get; private set; } IRepositoryModel selectedRepository; /// diff --git a/src/GitHub.Exports.Reactive/Services/IRepositoryCloneService.cs b/src/GitHub.Exports.Reactive/Services/IRepositoryCloneService.cs index f871a2c607..f3b7a8ec0c 100644 --- a/src/GitHub.Exports.Reactive/Services/IRepositoryCloneService.cs +++ b/src/GitHub.Exports.Reactive/Services/IRepositoryCloneService.cs @@ -1,5 +1,6 @@ using System; using System.Reactive; +using System.Threading.Tasks; namespace GitHub.Services { @@ -20,7 +21,16 @@ public interface IRepositoryCloneService /// The url of the repository to clone. /// The name of the repository to clone. /// The directory that will contain the repository directory. + /// + /// An object through which to report progress. This must be of type + /// , but + /// as that type is only available in VS2017+ it is typed as here. + /// /// - IObservable CloneRepository(string cloneUrl, string repositoryName, string repositoryPath); + Task CloneRepository( + string cloneUrl, + string repositoryName, + string repositoryPath, + object progress = null); } } diff --git a/src/GitHub.Exports.Reactive/ViewModels/IBaseCloneViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/IBaseCloneViewModel.cs index 27b9f9833a..07d5f79c2c 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/IBaseCloneViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/IBaseCloneViewModel.cs @@ -12,9 +12,9 @@ namespace GitHub.ViewModels public interface IBaseCloneViewModel : IViewModel, IRepositoryCreationTarget { /// - /// Command to clone the currently selected repository. + /// Signals that the user clicked the clone button. /// - IReactiveCommand CloneCommand { get; } + IReactiveCommand CloneCommand { get; } IRepositoryModel SelectedRepository { get; set; } } diff --git a/src/GitHub.Exports/GitHub.Exports.csproj b/src/GitHub.Exports/GitHub.Exports.csproj index 88c9692304..c88b19acd2 100644 --- a/src/GitHub.Exports/GitHub.Exports.csproj +++ b/src/GitHub.Exports/GitHub.Exports.csproj @@ -152,6 +152,7 @@ + @@ -169,6 +170,7 @@ + diff --git a/src/GitHub.Exports/Models/CloneDialogResult.cs b/src/GitHub.Exports/Models/CloneDialogResult.cs new file mode 100644 index 0000000000..05da3d10cd --- /dev/null +++ b/src/GitHub.Exports/Models/CloneDialogResult.cs @@ -0,0 +1,32 @@ +using System; +using GitHub.Services; + +namespace GitHub.Models +{ + /// + /// Holds the result of a call to . + /// + public class CloneDialogResult + { + /// + /// Initializes a new instance of the class. + /// + /// The selected base path for the clone. + /// The selected repository. + public CloneDialogResult(string basePath, IRepositoryModel repository) + { + BasePath = basePath; + Repository = repository; + } + + /// + /// Gets the filesystem path to which the user wants to clone. + /// + public string BasePath { get; } + + /// + /// Gets the repository selected by the user. + /// + public IRepositoryModel Repository { get; } + } +} diff --git a/src/GitHub.Exports/Services/IDialogService.cs b/src/GitHub.Exports/Services/IDialogService.cs new file mode 100644 index 0000000000..d05f1abd7f --- /dev/null +++ b/src/GitHub.Exports/Services/IDialogService.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using GitHub.Models; + +namespace GitHub.Services +{ + /// + /// Services for showing dialogs. + /// + public interface IDialogService + { + /// + /// Shows the clone dialog. + /// + /// The connection to use. + /// + /// A task that returns an instance of on success, + /// or null if the dialog was cancelled. + /// + Task ShowCloneDialog(IConnection connection); + + /// + /// Shows the re-clone dialog. + /// + /// The repository to clone. + /// + /// A task that returns the base path for the clone on success, or null if the dialog was + /// cancelled. + /// + /// + /// The re-clone dialog is shown from the VS2017+ start page when the user wants to check + /// out a repository that was previously checked out on another machine. + /// + Task ShowReCloneDialog(IRepositoryModel repository); + } +} diff --git a/src/GitHub.Exports/Services/IVSGitServices.cs b/src/GitHub.Exports/Services/IVSGitServices.cs index 2b5bf44914..ad56579d84 100644 --- a/src/GitHub.Exports/Services/IVSGitServices.cs +++ b/src/GitHub.Exports/Services/IVSGitServices.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Threading.Tasks; using GitHub.Models; namespace GitHub.Services @@ -6,7 +8,24 @@ namespace GitHub.Services public interface IVSGitServices { string GetLocalClonePathFromGitProvider(); - void Clone(string cloneUrl, string clonePath, bool recurseSubmodules); + + /// + /// Clones a repository via Team Explorer. + /// + /// The URL of the repository to clone. + /// The path to clone the repository to. + /// Whether to recursively clone submodules. + /// + /// An object through which to report progress. This must be of type + /// , but + /// as that type is only available in VS2017+ it is typed as here. + /// + Task Clone( + string cloneUrl, + string clonePath, + bool recurseSubmodules, + object progress = null); + string GetActiveRepoPath(); LibGit2Sharp.IRepository GetActiveRepo(); IEnumerable GetKnownRepositories(); diff --git a/src/GitHub.Exports/UI/IUIController.cs b/src/GitHub.Exports/UI/IUIController.cs index 5d538371d1..e2377612d6 100644 --- a/src/GitHub.Exports/UI/IUIController.cs +++ b/src/GitHub.Exports/UI/IUIController.cs @@ -33,7 +33,7 @@ public enum UIControllerFlow Gist, LogoutRequired, Home, - StartPageClone, + ReClone, PullRequestList, PullRequestDetail, PullRequestCreation, diff --git a/src/GitHub.Exports/ViewModels/IGitHubConnectSection.cs b/src/GitHub.Exports/ViewModels/IGitHubConnectSection.cs index 19d0a41711..f4a01060a5 100644 --- a/src/GitHub.Exports/ViewModels/IGitHubConnectSection.cs +++ b/src/GitHub.Exports/ViewModels/IGitHubConnectSection.cs @@ -1,15 +1,15 @@ using GitHub.Models; -using System.Collections.ObjectModel; +using System.Windows.Input; namespace GitHub.VisualStudio.TeamExplorer.Connect { public interface IGitHubConnectSection { void DoCreate(); - void DoClone(); void SignOut(); void Login(); bool OpenRepository(); IConnection SectionConnection { get; } + ICommand Clone { get; } } } diff --git a/src/GitHub.StartPage/StartPagePackage.cs b/src/GitHub.StartPage/StartPagePackage.cs index 754fec16e8..52a18249a0 100644 --- a/src/GitHub.StartPage/StartPagePackage.cs +++ b/src/GitHub.StartPage/StartPagePackage.cs @@ -52,14 +52,13 @@ public async Task AcquireCodeContainerAsync(RemoteCodeContainer o async Task RunAcquisition(IProgress downloadProgress, CancellationToken cancellationToken, IRepositoryModel repository) { - CloneRequest request = null; + CloneDialogResult request = null; try { var uiProvider = await Task.Run(() => Package.GetGlobalService(typeof(IGitHubServiceProvider)) as IGitHubServiceProvider); - var cm = uiProvider.TryGetService(); - var gitRepositories = await GetGitRepositoriesExt(uiProvider); - request = ShowCloneDialog(uiProvider, gitRepositories, repository); + await ShowTeamExplorerPage(uiProvider); + request = await ShowCloneDialog(uiProvider, downloadProgress, repository); } catch { @@ -83,13 +82,7 @@ async Task RunAcquisition(IProgress download lastAccessed: DateTimeOffset.UtcNow); } - async Task GetGitRepositoriesExt(IGitHubServiceProvider gitHubServiceProvider) - { - var page = await GetTeamExplorerPage(gitHubServiceProvider); - return page?.GetService(); - } - - async Task GetTeamExplorerPage(IGitHubServiceProvider gitHubServiceProvider) + async Task ShowTeamExplorerPage(IGitHubServiceProvider gitHubServiceProvider) { var te = gitHubServiceProvider?.GetService(typeof(ITeamExplorer)) as ITeamExplorer; @@ -115,59 +108,51 @@ async Task GetTeamExplorerPage(IGitHubServiceProvider gitHubS page = await tcs.Task; } - - return page; - } - else - { - // TODO: Log - return null; } } - CloneRequest ShowCloneDialog(IGitHubServiceProvider gitHubServiceProvider, IGitRepositoriesExt gitRepositories, IRepositoryModel repository = null) + async Task ShowCloneDialog( + IGitHubServiceProvider gitHubServiceProvider, + IProgress progress, + IRepositoryModel repository = null) { - string basePath = null; - - gitHubServiceProvider.AddService(this, gitRepositories); - var uiProvider = gitHubServiceProvider.GetService(); - var controller = uiProvider.Configure(repository == null ? UIControllerFlow.Clone : UIControllerFlow.StartPageClone, - null //TODO: set the connection corresponding to the repository if the repository is not null - ); - controller.TransitionSignal.Subscribe(x => + var dialogService = gitHubServiceProvider.GetService(); + var cloneService = gitHubServiceProvider.GetService(); + CloneDialogResult result = null; + + if (repository == null) + { + result = await dialogService.ShowCloneDialog(null); + } + else { - if ((repository == null && x.Data.ViewType == Exports.UIViewType.Clone) || // fire the normal clone dialog - (repository != null && x.Data.ViewType == Exports.UIViewType.StartPageClone) // fire the clone dialog for re-acquiring a repo - ) + var basePath = await dialogService.ShowReCloneDialog(repository); + + if (basePath != null) { - var vm = x.View.ViewModel as IBaseCloneViewModel; - if (repository != null) - vm.SelectedRepository = repository; - x.View.Done.Subscribe(_ => - { - basePath = vm.BaseRepositoryPath; - if (repository == null) - repository = vm.SelectedRepository; - }); + result = new CloneDialogResult(basePath, repository); } - }); - - uiProvider.RunInDialog(controller); - gitHubServiceProvider.RemoveService(typeof(IGitRepositoriesExt), this); - - return repository != null && basePath != null ? new CloneRequest(basePath, repository) : null; - } - - class CloneRequest - { - public CloneRequest(string basePath, IRepositoryModel repository) + } + + if (result != null) { - BasePath = basePath; - Repository = repository; + try + { + await cloneService.CloneRepository( + result.Repository.CloneUrl, + result.Repository.Name, + result.BasePath, + progress); + } + catch + { + var teServices = gitHubServiceProvider.TryGetService(); + teServices.ShowError($"Failed to clone the repository '{result.Repository.Name}'"); + result = null; + } } - public string BasePath { get; } - public IRepositoryModel Repository { get; } + return result; } } } diff --git a/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection.cs b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection.cs index fae132e388..d93de67ea2 100644 --- a/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection.cs +++ b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection.cs @@ -20,6 +20,9 @@ using GitHub.VisualStudio.UI; using GitHub.Primitives; using GitHub.Settings; +using System.Windows.Input; +using System.Reactive.Threading.Tasks; +using System.Reactive.Subjects; namespace GitHub.VisualStudio.TeamExplorer.Connect { @@ -28,6 +31,8 @@ public class GitHubConnectSection : TeamExplorerSectionBase, IGitHubConnectSecti readonly IPackageSettings packageSettings; readonly IVSServices vsServices; readonly int sectionIndex; + readonly IDialogService dialogService; + readonly IRepositoryCloneService cloneService; bool isCloning; bool isCreating; @@ -87,6 +92,8 @@ public ILocalRepositoryModel SelectedRepository set { selectedRepository = value; this.RaisePropertyChange(); } } + public ICommand Clone { get; } + internal ITeamExplorerServiceHolder Holder => holder; public GitHubConnectSection(IGitHubServiceProvider serviceProvider, @@ -95,6 +102,8 @@ public GitHubConnectSection(IGitHubServiceProvider serviceProvider, IConnectionManager manager, IPackageSettings packageSettings, IVSServices vsServices, + IRepositoryCloneService cloneService, + IDialogService dialogService, int index) : base(serviceProvider, apiFactory, holder, manager) { @@ -106,12 +115,38 @@ public GitHubConnectSection(IGitHubServiceProvider serviceProvider, this.packageSettings = packageSettings; this.vsServices = vsServices; + this.cloneService = cloneService; + this.dialogService = dialogService; + + Clone = CreateAsyncCommandHack(DoClone); connectionManager.Connections.CollectionChanged += RefreshConnections; PropertyChanged += OnPropertyChange; UpdateConnection(); } + async Task DoClone() + { + var result = await dialogService.ShowCloneDialog(SectionConnection); + + if (result != null) + { + try + { + ServiceProvider.GitServiceProvider = TEServiceProvider; + await cloneService.CloneRepository( + result.Repository.CloneUrl, + result.Repository.Name, + result.BasePath); + } + catch (Exception e) + { + var teServices = ServiceProvider.TryGetService(); + teServices.ShowError(e.GetUserFriendlyErrorMessage(ErrorType.ClonedFailed, result.Repository.Name)); + } + } + } + void RefreshConnections(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -330,11 +365,6 @@ public void DoCreate() StartFlow(UIControllerFlow.Create); } - public void DoClone() - { - StartFlow(UIControllerFlow.Clone); - } - public void SignOut() { SectionConnection.Logout(); @@ -409,6 +439,31 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + /// + /// Creates a ReactiveCommand that works like a command created via + /// but that does not hang when the async + /// task shows a modal dialog. + /// + /// Method that creates the task to run. + /// A reactive command. + /// + /// The command needs to be disabled while a clone operation is in + /// progress but also needs to display a modal dialog. For some reason using + /// causes a weird UI hang in this situation + /// where the UI runs but WhenAny no longer responds to property changed notifications. + /// + static ReactiveCommand CreateAsyncCommandHack(Func executeAsync) + { + var enabled = new BehaviorSubject(true); + var command = ReactiveCommand.Create(enabled); + command.Subscribe(async _ => + { + enabled.OnNext(false); + try { await executeAsync(); } + finally { enabled.OnNext(true); } + }); + return command; + } class SectionStateTracker { diff --git a/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection0.cs b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection0.cs index c7f0c033cf..05ec3a53e6 100644 --- a/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection0.cs +++ b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection0.cs @@ -19,8 +19,10 @@ public GitHubConnectSection0(IGitHubServiceProvider serviceProvider, ITeamExplorerServiceHolder holder, IConnectionManager manager, IPackageSettings settings, - IVSServices vsServices) - : base(serviceProvider, apiFactory, holder, manager, settings, vsServices, 0) + IVSServices vsServices, + IRepositoryCloneService cloneService, + IDialogService dialogService) + : base(serviceProvider, apiFactory, holder, manager, settings, vsServices, cloneService, dialogService, 0) { } } diff --git a/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection1.cs b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection1.cs index f6020e6edb..910c28567f 100644 --- a/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection1.cs +++ b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection1.cs @@ -19,8 +19,10 @@ public GitHubConnectSection1(IGitHubServiceProvider serviceProvider, ITeamExplorerServiceHolder holder, IConnectionManager manager, IPackageSettings settings, - IVSServices vsServices) - : base(serviceProvider, apiFactory, holder, manager, settings, vsServices, 1) + IVSServices vsServices, + IRepositoryCloneService cloneService, + IDialogService dialogService) + : base(serviceProvider, apiFactory, holder, manager, settings, vsServices, cloneService, dialogService, 0) { } } diff --git a/src/GitHub.TeamFoundation.14/Services/VSGitServices.cs b/src/GitHub.TeamFoundation.14/Services/VSGitServices.cs index 9a85107d16..3b083008d0 100644 --- a/src/GitHub.TeamFoundation.14/Services/VSGitServices.cs +++ b/src/GitHub.TeamFoundation.14/Services/VSGitServices.cs @@ -1,19 +1,19 @@ using System; -using System.Collections; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Globalization; using System.Linq; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; using GitHub.Extensions; using GitHub.Models; -using GitHub.VisualStudio; -using Microsoft.VisualStudio; -using Microsoft.VisualStudio.Shell.Interop; using GitHub.TeamFoundation; +using GitHub.VisualStudio; using Microsoft.TeamFoundation.Git.Controls.Extensibility; +using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; -using Microsoft.VisualStudio.Shell; -using System.Threading; +using ReactiveUI; namespace GitHub.Services { @@ -59,19 +59,29 @@ public string GetLocalClonePathFromGitProvider() return ret; } - public void Clone(string cloneUrl, string clonePath, bool recurseSubmodules) + /// + public async Task Clone( + string cloneUrl, + string clonePath, + bool recurseSubmodules, + object progress = null) { #if TEAMEXPLORER14 var gitExt = serviceProvider.GetService(); gitExt.Clone(cloneUrl, clonePath, recurseSubmodules ? CloneOptions.RecurseSubmodule : CloneOptions.None); + + // The operation will have completed when CanClone goes false and then true again. + await gitExt.WhenAnyValue(x => x.CanClone).Where(x => !x).Take(1); + await gitExt.WhenAnyValue(x => x.CanClone).Where(x => x).Take(1); #else var gitExt = serviceProvider.GetService(); - var progress = new Progress(); + var typedProgress = ((Progress)progress) ?? + new Progress(); - ThreadHelper.JoinableTaskFactory.RunAsync(async () => + await Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.RunAsync(async () => { - progress.ProgressChanged += (s, e) => statusBar.SetText(e.ProgressText); - await gitExt.CloneAsync(cloneUrl, clonePath, recurseSubmodules, default(CancellationToken), progress); + typedProgress.ProgressChanged += (s, e) => statusBar.SetText(e.ProgressText); + await gitExt.CloneAsync(cloneUrl, clonePath, recurseSubmodules, default(CancellationToken), typedProgress); }); #endif } diff --git a/src/GitHub.VisualStudio.UI/UI/Views/GitHubConnectContent.xaml b/src/GitHub.VisualStudio.UI/UI/Views/GitHubConnectContent.xaml index f4654690ab..b41f2844a4 100644 --- a/src/GitHub.VisualStudio.UI/UI/Views/GitHubConnectContent.xaml +++ b/src/GitHub.VisualStudio.UI/UI/Views/GitHubConnectContent.xaml @@ -146,7 +146,7 @@ -