diff --git a/src/GitHub.App/SampleData/Dialog/Clone/RepositoryCloneViewModelDesigner.cs b/src/GitHub.App/SampleData/Dialog/Clone/RepositoryCloneViewModelDesigner.cs index 4b2c599fe9..0d66eb9528 100644 --- a/src/GitHub.App/SampleData/Dialog/Clone/RepositoryCloneViewModelDesigner.cs +++ b/src/GitHub.App/SampleData/Dialog/Clone/RepositoryCloneViewModelDesigner.cs @@ -2,6 +2,7 @@ using System.Reactive; using System.Threading.Tasks; using GitHub.Models; +using GitHub.Primitives; using GitHub.ViewModels; using GitHub.ViewModels.Dialog.Clone; using ReactiveUI; @@ -17,13 +18,13 @@ public RepositoryCloneViewModelDesigner() } public string Path { get; set; } + public UriString Url { get; set; } public string PathWarning { get; set; } public int SelectedTabIndex { get; set; } public string Title => null; public IObservable Done => null; public IRepositorySelectViewModel GitHubTab { get; } public IRepositorySelectViewModel EnterpriseTab { get; } - public IRepositoryUrlViewModel UrlTab { get; } public ReactiveCommand Browse { get; } public ReactiveCommand Clone { get; } diff --git a/src/GitHub.App/Services/DialogService.cs b/src/GitHub.App/Services/DialogService.cs index bc4d189fe1..5eeae61c74 100644 --- a/src/GitHub.App/Services/DialogService.cs +++ b/src/GitHub.App/Services/DialogService.cs @@ -51,7 +51,7 @@ public async Task ShowCloneDialog(IConnection connection, str var viewModel = factory.CreateViewModel(); if (url != null) { - viewModel.UrlTab.Url = url; + viewModel.Url = url; } if (connection != null) diff --git a/src/GitHub.App/ViewModels/Dialog/Clone/RepositoryCloneViewModel.cs b/src/GitHub.App/ViewModels/Dialog/Clone/RepositoryCloneViewModel.cs index b96b6dfbe9..585f57c5cb 100644 --- a/src/GitHub.App/ViewModels/Dialog/Clone/RepositoryCloneViewModel.cs +++ b/src/GitHub.App/ViewModels/Dialog/Clone/RepositoryCloneViewModel.cs @@ -9,6 +9,7 @@ using GitHub.Extensions; using GitHub.Logging; using GitHub.Models; +using GitHub.Primitives; using GitHub.Services; using ReactiveUI; using Rothko; @@ -20,15 +21,14 @@ namespace GitHub.ViewModels.Dialog.Clone [PartCreationPolicy(CreationPolicy.NonShared)] public class RepositoryCloneViewModel : ViewModelBase, IRepositoryCloneViewModel { - static readonly ILogger log = LogManager.ForContext(); readonly IOperatingSystem os; readonly IConnectionManager connectionManager; readonly IRepositoryCloneService service; readonly IGitService gitService; - readonly IUsageService usageService; readonly IUsageTracker usageTracker; readonly IReadOnlyList tabs; string path; + UriString url; RepositoryModel previousRepository; ObservableAsPropertyHelper pathWarning; int selectedTabIndex; @@ -39,23 +39,19 @@ public RepositoryCloneViewModel( IConnectionManager connectionManager, IRepositoryCloneService service, IGitService gitService, - IUsageService usageService, IUsageTracker usageTracker, IRepositorySelectViewModel gitHubTab, - IRepositorySelectViewModel enterpriseTab, - IRepositoryUrlViewModel urlTab) + IRepositorySelectViewModel enterpriseTab) { this.os = os; this.connectionManager = connectionManager; this.service = service; this.gitService = gitService; - this.usageService = usageService; this.usageTracker = usageTracker; GitHubTab = gitHubTab; EnterpriseTab = enterpriseTab; - UrlTab = urlTab; - tabs = new IRepositoryCloneTabViewModel[] { GitHubTab, EnterpriseTab, UrlTab }; + tabs = new IRepositoryCloneTabViewModel[] { GitHubTab, EnterpriseTab }; var repository = this.WhenAnyValue(x => x.SelectedTabIndex) .SelectMany(x => tabs[x].WhenAnyValue(tab => tab.Repository)); @@ -88,7 +84,6 @@ public RepositoryCloneViewModel( public IRepositorySelectViewModel GitHubTab { get; } public IRepositorySelectViewModel EnterpriseTab { get; } - public IRepositoryUrlViewModel UrlTab { get; } public string Path { @@ -96,6 +91,12 @@ public string Path set => this.RaiseAndSetIfChanged(ref path, value); } + public UriString Url + { + get => url; + set => this.RaiseAndSetIfChanged(ref url, value); + } + public string PathWarning => pathWarning.Value; public int SelectedTabIndex @@ -135,26 +136,21 @@ public async Task InitializeAsync(IConnection connection) SelectedTabIndex = 1; } - this.WhenAnyValue(x => x.SelectedTabIndex).Subscribe(x => tabs[x].Activate().Forget()); - - // When a clipboard URL has been set, show the URL tab by default - if (!string.IsNullOrEmpty(UrlTab.Url)) + if (Url?.Host is string host && HostAddress.Create(host) is HostAddress hostAddress) { - SelectedTabIndex = 2; + if (hostAddress == gitHubConnection?.HostAddress) + { + GitHubTab.Filter = Url; + SelectedTabIndex = 0; + } + else if (hostAddress == enterpriseConnection?.HostAddress) + { + EnterpriseTab.Filter = Url; + SelectedTabIndex = 1; + } } - switch (SelectedTabIndex) - { - case 0: - usageTracker.IncrementCounter(model => model.NumberOfCloneViewGitHubTab).Forget(); - break; - case 1: - usageTracker.IncrementCounter(model => model.NumberOfCloneViewEnterpriseTab).Forget(); - break; - case 2: - usageTracker.IncrementCounter(model => model.NumberOfCloneViewUrlTab).Forget(); - break; - } + this.WhenAnyValue(x => x.SelectedTabIndex).Subscribe(x => tabs[x].Activate().Forget()); } void BrowseForDirectory() diff --git a/src/GitHub.App/ViewModels/Dialog/Clone/RepositorySelectViewModel.cs b/src/GitHub.App/ViewModels/Dialog/Clone/RepositorySelectViewModel.cs index 6a9034bc8a..1c68a134d9 100644 --- a/src/GitHub.App/ViewModels/Dialog/Clone/RepositorySelectViewModel.cs +++ b/src/GitHub.App/ViewModels/Dialog/Clone/RepositorySelectViewModel.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using System.Windows.Data; +using GitHub.Exports; using GitHub.Extensions; using GitHub.Logging; using GitHub.Models; @@ -23,6 +24,7 @@ public class RepositorySelectViewModel : ViewModelBase, IRepositorySelectViewMod { static readonly ILogger log = LogManager.ForContext(); readonly IRepositoryCloneService service; + readonly IGitHubContextService gitHubContextService; IConnection connection; Exception error; string filter; @@ -35,15 +37,26 @@ public class RepositorySelectViewModel : ViewModelBase, IRepositorySelectViewMod IRepositoryItemViewModel selectedItem; [ImportingConstructor] - public RepositorySelectViewModel(IRepositoryCloneService service) + public RepositorySelectViewModel(IRepositoryCloneService service, IGitHubContextService gitHubContextService) { Guard.ArgumentNotNull(service, nameof(service)); + Guard.ArgumentNotNull(service, nameof(gitHubContextService)); this.service = service; + this.gitHubContextService = gitHubContextService; - repository = this.WhenAnyValue(x => x.SelectedItem) - .Select(CreateRepository) + var selectedRepository = this.WhenAnyValue(x => x.SelectedItem) + .Select(CreateRepository); + + var filterRepository = this.WhenAnyValue(x => x.Filter) + .Select(f => gitHubContextService.FindContextFromUrl(f)) + .Where(c => c?.LinkType == LinkType.Repository) + .Select(c => new RepositoryModel(c.RepositoryName, c.Url)); + + repository = selectedRepository + .Merge(filterRepository) .ToProperty(this, x => x.Repository); + this.WhenAnyValue(x => x.Filter).Subscribe(_ => ItemsView?.Refresh()); } @@ -166,7 +179,14 @@ bool FilterItem(object obj) { if (obj is IRepositoryItemViewModel item && !string.IsNullOrWhiteSpace(Filter)) { - return item.Caption.Contains(Filter, StringComparison.CurrentCultureIgnoreCase); + 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); } return true; diff --git a/src/GitHub.App/ViewModels/Dialog/Clone/RepositoryUrlViewModel.cs b/src/GitHub.App/ViewModels/Dialog/Clone/RepositoryUrlViewModel.cs deleted file mode 100644 index 0c2d69caee..0000000000 --- a/src/GitHub.App/ViewModels/Dialog/Clone/RepositoryUrlViewModel.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.ComponentModel.Composition; -using System.Threading.Tasks; -using GitHub.Extensions; -using GitHub.Models; -using GitHub.Primitives; -using GitHub.ViewModels; -using GitHub.ViewModels.Dialog.Clone; -using ReactiveUI; - -namespace GitHub.App.ViewModels.Dialog.Clone -{ - [Export(typeof(IRepositoryUrlViewModel))] - [PartCreationPolicy(CreationPolicy.NonShared)] - public class RepositoryUrlViewModel : ViewModelBase, IRepositoryUrlViewModel - { - ObservableAsPropertyHelper repository; - string url; - - public RepositoryUrlViewModel() - { - repository = this.WhenAnyValue(x => x.Url, ParseUrl).ToProperty(this, x => x.Repository); - } - - public string Url - { - get => url; - set => this.RaiseAndSetIfChanged(ref url, value); - } - - public bool IsEnabled => true; - - public RepositoryModel Repository => repository.Value; - - public Task Activate() => Task.CompletedTask; - - RepositoryModel ParseUrl(string s) - { - if (s != null) - { - var uri = new UriString(s); - - if (string.IsNullOrWhiteSpace(uri.Owner) || !string.IsNullOrWhiteSpace(uri.RepositoryName)) - { - var parts = s.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length == 2) - { - uri = UriString.ToUriString( - HostAddress.GitHubDotComHostAddress.WebUri - .Append(parts[0]) - .Append(parts[1])); - } - } - - if (!string.IsNullOrWhiteSpace(uri.Owner) && !string.IsNullOrWhiteSpace(uri.RepositoryName)) - { - return new RepositoryModel(uri.RepositoryName, uri); - } - } - - return null; - } - } -} diff --git a/src/GitHub.Exports.Reactive/ViewModels/Dialog/Clone/IRepositoryCloneViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/Dialog/Clone/IRepositoryCloneViewModel.cs index 0b8eb754fc..ba42f011eb 100644 --- a/src/GitHub.Exports.Reactive/ViewModels/Dialog/Clone/IRepositoryCloneViewModel.cs +++ b/src/GitHub.Exports.Reactive/ViewModels/Dialog/Clone/IRepositoryCloneViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Reactive; using GitHub.Models; +using GitHub.Primitives; using ReactiveUI; namespace GitHub.ViewModels.Dialog.Clone @@ -21,9 +22,9 @@ public interface IRepositoryCloneViewModel : IDialogContentViewModel, IConnectio IRepositorySelectViewModel EnterpriseTab { get; } /// - /// Gets the view model for the URL tab. + /// Initial URL for the dialog. /// - IRepositoryUrlViewModel UrlTab { get; } + UriString Url { get; set; } /// /// Gets the path to clone the repository to. diff --git a/src/GitHub.Exports.Reactive/ViewModels/Dialog/Clone/IRepositoryUrlViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/Dialog/Clone/IRepositoryUrlViewModel.cs deleted file mode 100644 index 57701e2948..0000000000 --- a/src/GitHub.Exports.Reactive/ViewModels/Dialog/Clone/IRepositoryUrlViewModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace GitHub.ViewModels.Dialog.Clone -{ - public interface IRepositoryUrlViewModel : IRepositoryCloneTabViewModel - { - string Url { get; set; } - } -} diff --git a/src/GitHub.Exports/Models/UsageModel.cs b/src/GitHub.Exports/Models/UsageModel.cs index 24c651feb9..f91ffd7448 100644 --- a/src/GitHub.Exports/Models/UsageModel.cs +++ b/src/GitHub.Exports/Models/UsageModel.cs @@ -90,9 +90,6 @@ public class MeasuresModel public int ExecuteToggleInlineCommentMarginCommand { get; set; } public int NumberOfPullRequestFileMarginToggleInlineCommentMargin { get; set; } public int NumberOfPullRequestFileMarginViewChanges { get; set; } - public int NumberOfCloneViewGitHubTab { get; set; } - public int NumberOfCloneViewEnterpriseTab { get; set; } - public int NumberOfCloneViewUrlTab { get; set; } public int NumberOfGitHubClones { get; set; } public int NumberOfEnterpriseClones { get; set; } public int NumberOfGitHubOpens { get; set; } diff --git a/src/GitHub.VisualStudio.UI/Views/Dialog/Clone/RepositoryCloneView.xaml b/src/GitHub.VisualStudio.UI/Views/Dialog/Clone/RepositoryCloneView.xaml index 70d0e25f24..407589f563 100644 --- a/src/GitHub.VisualStudio.UI/Views/Dialog/Clone/RepositoryCloneView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/Dialog/Clone/RepositoryCloneView.xaml @@ -10,7 +10,7 @@ - + @@ -70,21 +70,6 @@ Visibility="{Binding EnterpriseTab.IsEnabled, Converter={ghfvs:BooleanToVisibilityConverter}}"> - - - - Repository URL or GitHub username and repository - - (hubot/cool-repo) - - - - diff --git a/src/GitHub.VisualStudio.UI/Views/Dialog/Clone/SelectPageView.xaml b/src/GitHub.VisualStudio.UI/Views/Dialog/Clone/SelectPageView.xaml index 4a79d4ee75..9dd656640f 100644 --- a/src/GitHub.VisualStudio.UI/Views/Dialog/Clone/SelectPageView.xaml +++ b/src/GitHub.VisualStudio.UI/Views/Dialog/Clone/SelectPageView.xaml @@ -23,7 +23,7 @@ diff --git a/test/GitHub.App.UnitTests/ViewModels/Dialog/Clone/RepositoryCloneViewModelTests.cs b/test/GitHub.App.UnitTests/ViewModels/Dialog/Clone/RepositoryCloneViewModelTests.cs index 3f1d41e357..8d3b26f210 100644 --- a/test/GitHub.App.UnitTests/ViewModels/Dialog/Clone/RepositoryCloneViewModelTests.cs +++ b/test/GitHub.App.UnitTests/ViewModels/Dialog/Clone/RepositoryCloneViewModelTests.cs @@ -2,8 +2,6 @@ using System.ComponentModel; using System.Globalization; using System.IO; -using System.Linq.Expressions; -using System.Reactive.Linq; using System.Threading.Tasks; using GitHub.Extensions; using GitHub.Models; @@ -23,6 +21,43 @@ public class RepositoryCloneViewModelTests const string fileExists = "d:\\exists\\file"; const string defaultPath = "d:\\default\\path"; + public class TheUrlProperty + { + [TestCase("https://github.com;https://enterprise.com", null, 0)] + [TestCase("https://github.com;https://enterprise.com", "https://github.com/foo/bar", 0)] + [TestCase("https://github.com;https://enterprise.com", "https://enterprise.com/foo/bar", 1)] + [TestCase("https://github.com;https://enterprise.com", "HTTPS://ENTERPRISE.COM/FOO/BAR", 1)] + [TestCase("https://github.com;https://enterprise.com", "https://unknown.com/foo/bar", 0)] + public async Task Select_Tab_For_Url(string addresses, string url, int expectTabIndex) + { + var cm = CreateConnectionManager(addresses.Split(';')); + var target = CreateTarget(connectionManager: cm); + target.Url = url; + + await target.InitializeAsync(null); + + Assert.That(target.SelectedTabIndex, Is.EqualTo(expectTabIndex)); + } + + [TestCase("https://github.com;https://enterprise.com", null, "", "")] + [TestCase("https://github.com;https://enterprise.com", "https://github.com/foo/bar", "https://github.com/foo/bar", "")] + [TestCase("https://github.com;https://enterprise.com", "https://enterprise.com/foo/bar", "", "https://enterprise.com/foo/bar")] + [TestCase("https://github.com;https://enterprise.com", "HTTPS://GITHUB.COM/FOO/BAR", "HTTPS://GITHUB.COM/FOO/BAR", "")] + [TestCase("https://github.com;https://enterprise.com", "HTTPS://ENTERPRISE.COM/FOO/BAR", "", "HTTPS://ENTERPRISE.COM/FOO/BAR")] + [TestCase("https://github.com;https://enterprise.com", "https://unknown.com/foo/bar", "", "")] + public async Task Set_Filter_For_Url(string addresses, string url, string expectGitHubFilter, string expectEnterpriseFilter) + { + var cm = CreateConnectionManager(addresses.Split(';')); + var target = CreateTarget(connectionManager: cm); + target.Url = url; + + await target.InitializeAsync(null); + + Assert.That(target.GitHubTab.Filter, Is.EqualTo(expectGitHubFilter)); + Assert.That(target.EnterpriseTab.Filter, Is.EqualTo(expectEnterpriseFilter)); + } + } + [Test] public async Task GitHubPage_Is_Initialized() { @@ -35,45 +70,6 @@ public async Task GitHubPage_Is_Initialized() target.EnterpriseTab.DidNotReceiveWithAnyArgs().Initialize(null); } - [TestCase("https://github.com", null, false, 0)] - [TestCase("https://enterprise.com", null, false, 1)] - [TestCase("https://github.com", null, true, 0, Description = "Show URL tab for GitHub connections")] - [TestCase("https://enterprise.com", null, true, 1, Description = "Show URL tab for Enterprise connections")] - [TestCase("https://github.com", "https://github.com/github/visualstudio", false, 2)] - [TestCase("https://enterprise.com", "https://enterprise.com/owner/repo", false, 2)] - public async Task Default_SelectedTabIndex_For_Group(string address, string clipboardUrl, bool isGroupA, int expectTabIndex) - { - var cm = CreateConnectionManager(address); - var connection = cm.Connections[0]; - var usageService = CreateUsageService(isGroupA); - var target = CreateTarget(connectionManager: cm, usageService: usageService); - target.UrlTab.Url = clipboardUrl; - - await target.InitializeAsync(connection); - - Assert.That(target.SelectedTabIndex, Is.EqualTo(expectTabIndex)); - } - - [TestCase("https://github.com", false, 1, nameof(UsageModel.MeasuresModel.NumberOfCloneViewGitHubTab))] - [TestCase("https://enterprise.com", false, 1, nameof(UsageModel.MeasuresModel.NumberOfCloneViewEnterpriseTab))] - [TestCase("https://github.com", true, 1, nameof(UsageModel.MeasuresModel.NumberOfCloneViewGitHubTab))] - [TestCase("https://enterprise.com", true, 1, nameof(UsageModel.MeasuresModel.NumberOfCloneViewEnterpriseTab))] - public async Task IncrementCounter_Showing_Default_Tab(string address, bool isGroupA, int numberOfCalls, string counterName) - { - var cm = CreateConnectionManager(address); - var connection = cm.Connections[0]; - var usageService = CreateUsageService(isGroupA); - var usageTracker = Substitute.For(); - var target = CreateTarget(connectionManager: cm, usageService: usageService, usageTracker: usageTracker); - usageTracker.IncrementCounter(null).ReturnsForAnyArgs(Task.CompletedTask); - - await target.InitializeAsync(connection).ConfigureAwait(false); - - await usageTracker.Received(numberOfCalls).IncrementCounter( - Arg.Is>>(x => - ((MemberExpression)x.Body).Member.Name == counterName)); - } - [Test] public async Task EnterprisePage_Is_Initialized() { @@ -399,34 +395,28 @@ static RepositoryCloneViewModel CreateTarget( IOperatingSystem os = null, IConnectionManager connectionManager = null, IRepositoryCloneService service = null, - IUsageService usageService = null, IUsageTracker usageTracker = null, IRepositorySelectViewModel gitHubTab = null, IRepositorySelectViewModel enterpriseTab = null, IGitService gitService = null, - IRepositoryUrlViewModel urlTab = null, string defaultClonePath = defaultPath) { os = os ?? Substitute.For(); connectionManager = connectionManager ?? CreateConnectionManager("https://github.com"); service = service ?? CreateRepositoryCloneService(defaultClonePath); - usageService = usageService ?? CreateUsageService(); usageTracker = usageTracker ?? Substitute.For(); gitHubTab = gitHubTab ?? CreateSelectViewModel(); enterpriseTab = enterpriseTab ?? CreateSelectViewModel(); gitService = gitService ?? CreateGitService(true, "https://github.com/owner/repo"); - urlTab = urlTab ?? CreateRepositoryUrlViewModel(); return new RepositoryCloneViewModel( os, connectionManager, service, gitService, - usageService, usageTracker, gitHubTab, - enterpriseTab, - urlTab); + enterpriseTab); } private static IGitService CreateGitService(bool repositoryExists, UriString remoteUrl) @@ -444,16 +434,6 @@ private static IGitService CreateGitService(bool repositoryExists, UriString rem return gitService; } - static IUsageService CreateUsageService(bool isGroupA = false) - { - var usageService = Substitute.For(); - var guidBytes = new byte[16]; - guidBytes[guidBytes.Length - 1] = (byte)(isGroupA ? 0 : 1); - var userGuid = new Guid(guidBytes); - usageService.GetUserGuid().Returns(userGuid); - return usageService; - } - static RepositoryModel CreateRepositoryModel(string repo = "owner/repo") { var split = repo.Split('/'); @@ -471,13 +451,5 @@ static UriString CreateGitHubUrl(string owner, string repo) { return new UriString($"https://github.com/{owner}/{repo}"); } - - static IRepositoryUrlViewModel CreateRepositoryUrlViewModel() - { - var repositoryUrlViewModel = Substitute.For(); - repositoryUrlViewModel.Repository.Returns(null as RepositoryModel); - repositoryUrlViewModel.Url.Returns(string.Empty); - return repositoryUrlViewModel; - } } } diff --git a/test/GitHub.App.UnitTests/ViewModels/Dialog/Clone/RepositorySelectViewModelTests.cs b/test/GitHub.App.UnitTests/ViewModels/Dialog/Clone/RepositorySelectViewModelTests.cs new file mode 100644 index 0000000000..c62129e080 --- /dev/null +++ b/test/GitHub.App.UnitTests/ViewModels/Dialog/Clone/RepositorySelectViewModelTests.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Data; +using GitHub.Models; +using GitHub.Primitives; +using GitHub.Services; +using GitHub.ViewModels.Dialog.Clone; +using NSubstitute; +using NUnit.Framework; + +public class RepositorySelectViewModelTests +{ + public class TheFilterProperty + { + [TestCase("unknown", "owner", "name", "https://github.com/owner/name", 0)] + [TestCase("", "owner", "name", "https://github.com/owner/name", 1)] + [TestCase("owner", "owner", "name", "https://github.com/owner/name", 1)] + [TestCase("name", "owner", "name", "https://github.com/owner/name", 1)] + [TestCase("owner/name", "owner", "name", "https://github.com/owner/name", 1)] + [TestCase("OWNER/NAME", "owner", "name", "https://github.com/owner/name", 1)] + [TestCase("https://github.com/owner/name", "owner", "name", "https://github.com/owner/name", 1)] + [TestCase("HTTPS://GITHUB.COM/OWNER/NAME", "owner", "name", "https://github.com/owner/name", 1)] + [TestCase("https://github.com/owner", "owner", "name", "https://github.com/owner/name", 1)] + [TestCase("https://github.com/jcansdale/TestDriven.Net", "owner", "name", "https://github.com/jcansdale/TestDriven.Net-issues", 1)] + [TestCase("https://github.com/owner/name/", "owner", "name", "https://github.com/owner/name", 1, Description = "Trailing slash")] + [TestCase("https://github.com/owner/name.git", "owner", "name", "https://github.com/owner/name", 1, Description = "Trailing .git")] + public async Task Filter(string filter, string owner, string name, string url, int expectCount) + { + var contributedToRepositories = new[] + { + new RepositoryListItemModel + { + Owner = owner, + Name = name, + Url = new Uri(url) + } + }; + var hostAddress = HostAddress.GitHubDotComHostAddress; + var connection = CreateConnection(hostAddress); + var repositoryCloneService = CreateRepositoryCloneService(contributedToRepositories, hostAddress); + var gitHubContextService = CreateGitHubContextService(); + var target = new RepositorySelectViewModel(repositoryCloneService, gitHubContextService); + target.Filter = filter; + target.Initialize(connection); + + await target.Activate(); + + var items = target.ItemsView.Groups + .Cast() + .SelectMany(g => g.Items) + .Cast(); + Assert.That(items.Count, Is.EqualTo(expectCount)); + } + + [TestCase("filter", null)] + [TestCase("https://github.com", null)] + [TestCase("https://github.com/github/VisualStudio", "https://github.com/github/VisualStudio")] + public void Set_Repository_When_Filter_Is_Url(string url, string expectUrl) + { + var expectCloneUrl = expectUrl != null ? new UriString(expectUrl) : null; + var repositoryCloneService = CreateRepositoryCloneService(); + var gitHubContextService = new GitHubContextService(Substitute.For(), + Substitute.For(), Substitute.For()); + var target = new RepositorySelectViewModel(repositoryCloneService, gitHubContextService); + + target.Filter = url; + + Assert.That(target.Repository?.CloneUrl, Is.EqualTo(expectCloneUrl)); + } + } + + static IGitHubContextService CreateGitHubContextService() + { + return Substitute.For(); + } + + static IConnection CreateConnection(HostAddress hostAddress) + { + var connection = Substitute.For(); + connection.HostAddress.Returns(hostAddress); + return connection; + } + + static IRepositoryCloneService CreateRepositoryCloneService( + IList contributedToRepositories = null, + HostAddress hostAddress = null) + { + contributedToRepositories = contributedToRepositories ?? Array.Empty(); + hostAddress = hostAddress ?? HostAddress.GitHubDotComHostAddress; + + var viewRepositoriesModel = CreateViewerRepositoriesModel(contributedToRepositories: contributedToRepositories); + var repositoryCloneService = Substitute.For(); + repositoryCloneService.ReadViewerRepositories(hostAddress).Returns(viewRepositoriesModel); + return repositoryCloneService; + } + + private static ViewerRepositoriesModel CreateViewerRepositoriesModel( + string owner = "owner", + IList repositories = null, + IList contributedToRepositories = null) + { + repositories = repositories ?? Array.Empty(); + contributedToRepositories = contributedToRepositories ?? Array.Empty(); + + return new ViewerRepositoriesModel + { + Owner = owner, + Repositories = CreateRepositoriesList(repositories), + ContributedToRepositories = CreateRepositoriesList(contributedToRepositories), + Organizations = CreateOrganizationsList() + }; + } + + static IReadOnlyList CreateRepositoriesList(IList repositories) + { + return repositories.ToList().AsReadOnly(); + } + + static IDictionary> CreateOrganizationsList() + { + return new Dictionary>(); + } +}