Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 7d355e4

Browse files
committed
Merge remote-tracking branch 'upstream/master' into fixes/1666-filter-pr-solution-explorer
2 parents 7d1a254 + 34b1fe8 commit 7d355e4

File tree

30 files changed

+532
-239
lines changed

30 files changed

+532
-239
lines changed

src/GitHub.App/SampleData/Dialog/Clone/RepositoryCloneViewModelDesigner.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public RepositoryCloneViewModelDesigner()
1616
}
1717

1818
public string Path { get; set; }
19-
public string PathError { get; set; }
19+
public string PathWarning { get; set; }
2020
public int SelectedTabIndex { get; set; }
2121
public string Title => null;
2222
public IObservable<object> Done => null;

src/GitHub.App/Services/DialogService.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ public DialogService(
2929
this.showDialog = showDialog;
3030
}
3131

32-
public async Task<CloneDialogResult> ShowCloneDialog(IConnection connection)
32+
public async Task<CloneDialogResult> ShowCloneDialog(IConnection connection, string url = null)
3333
{
3434
var viewModel = factory.CreateViewModel<IRepositoryCloneViewModel>();
35+
if (url != null)
36+
{
37+
viewModel.UrlTab.Url = url;
38+
}
3539

3640
if (connection != null)
3741
{

src/GitHub.App/Services/RepositoryCloneService.cs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class RepositoryCloneService : IRepositoryCloneService
3434
readonly IOperatingSystem operatingSystem;
3535
readonly string defaultClonePath;
3636
readonly IVSGitServices vsGitServices;
37+
readonly ITeamExplorerServices teamExplorerServices;
3738
readonly IGraphQLClientFactory graphqlFactory;
3839
readonly IUsageTracker usageTracker;
3940
ICompiledQuery<ViewerRepositoriesModel> readViewerRepositories;
@@ -42,11 +43,13 @@ public class RepositoryCloneService : IRepositoryCloneService
4243
public RepositoryCloneService(
4344
IOperatingSystem operatingSystem,
4445
IVSGitServices vsGitServices,
46+
ITeamExplorerServices teamExplorerServices,
4547
IGraphQLClientFactory graphqlFactory,
4648
IUsageTracker usageTracker)
4749
{
4850
this.operatingSystem = operatingSystem;
4951
this.vsGitServices = vsGitServices;
52+
this.teamExplorerServices = teamExplorerServices;
5053
this.graphqlFactory = graphqlFactory;
5154
this.usageTracker = usageTracker;
5255

@@ -103,6 +106,54 @@ public async Task<ViewerRepositoriesModel> ReadViewerRepositories(HostAddress ad
103106
return result;
104107
}
105108

109+
/// <inheritdoc/>
110+
public async Task CloneOrOpenRepository(
111+
CloneDialogResult cloneDialogResult,
112+
object progress = null)
113+
{
114+
Guard.ArgumentNotNull(cloneDialogResult, nameof(cloneDialogResult));
115+
116+
var repositoryPath = cloneDialogResult.Path;
117+
var url = cloneDialogResult.Url;
118+
119+
if (DestinationFileExists(repositoryPath))
120+
{
121+
throw new InvalidOperationException("Can't clone or open a repository because a file exists at: " + repositoryPath);
122+
}
123+
124+
var repositoryUrl = url.ToRepositoryUrl();
125+
var isDotCom = HostAddress.IsGitHubDotComUri(repositoryUrl);
126+
if (DestinationDirectoryExists(repositoryPath))
127+
{
128+
teamExplorerServices.OpenRepository(repositoryPath);
129+
130+
if (isDotCom)
131+
{
132+
await usageTracker.IncrementCounter(x => x.NumberOfGitHubOpens);
133+
}
134+
else
135+
{
136+
await usageTracker.IncrementCounter(x => x.NumberOfEnterpriseOpens);
137+
}
138+
}
139+
else
140+
{
141+
var cloneUrl = repositoryUrl.ToString();
142+
await CloneRepository(cloneUrl, repositoryPath, progress).ConfigureAwait(true);
143+
144+
if (isDotCom)
145+
{
146+
await usageTracker.IncrementCounter(x => x.NumberOfGitHubClones);
147+
}
148+
else
149+
{
150+
await usageTracker.IncrementCounter(x => x.NumberOfEnterpriseClones);
151+
}
152+
}
153+
154+
teamExplorerServices.ShowHomePage();
155+
}
156+
106157
/// <inheritdoc/>
107158
public async Task CloneRepository(
108159
string cloneUrl,
@@ -121,19 +172,12 @@ public async Task CloneRepository(
121172
try
122173
{
123174
await vsGitServices.Clone(cloneUrl, repositoryPath, true, progress);
124-
125175
await usageTracker.IncrementCounter(x => x.NumberOfClones);
126176

127-
var repositoryUrl = new UriString(cloneUrl).ToRepositoryUrl();
128-
var isDotCom = HostAddress.IsGitHubDotComUri(repositoryUrl);
129-
if (isDotCom)
130-
{
131-
await usageTracker.IncrementCounter(x => x.NumberOfGitHubClones);
132-
}
133-
else
177+
if (repositoryPath.StartsWith(DefaultClonePath, StringComparison.OrdinalIgnoreCase))
134178
{
135-
// If it isn't a GitHub URL, assume it's an Enterprise URL
136-
await usageTracker.IncrementCounter(x => x.NumberOfEnterpriseClones);
179+
// Count the number of times users clone into the Default Repository Location
180+
await usageTracker.IncrementCounter(x => x.NumberOfClonesToDefaultClonePath);
137181
}
138182
}
139183
catch (Exception ex)
@@ -144,7 +188,10 @@ public async Task CloneRepository(
144188
}
145189

146190
/// <inheritdoc/>
147-
public bool DestinationExists(string path) => Directory.Exists(path) || File.Exists(path);
191+
public bool DestinationDirectoryExists(string path) => operatingSystem.Directory.DirectoryExists(path);
192+
193+
/// <inheritdoc/>
194+
public bool DestinationFileExists(string path) => operatingSystem.File.Exists(path);
148195

149196
string GetLocalClonePathFromGitProvider(string fallbackPath)
150197
{

src/GitHub.App/Services/StandardUserErrors.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public enum ErrorType
2525
CannotDropFolder,
2626
CannotDropFolderUnauthorizedAccess,
2727
ClipboardFailed,
28-
ClonedFailed,
28+
CloneOrOpenFailed,
2929
CloneFailedNotLoggedIn,
3030
CommitCreateFailed,
3131
CommitRevertFailed,
@@ -123,7 +123,7 @@ public static class StandardUserErrors
123123
},
124124
{ ErrorType.ClipboardFailed, Map(Defaults("Failed to copy text to the clipboard.")) },
125125
{
126-
ErrorType.ClonedFailed, Map(Defaults("Failed to clone the repository '{0}'", "Email [email protected] if you continue to have problems."),
126+
ErrorType.CloneOrOpenFailed, Map(Defaults("Failed to clone or open the repository '{0}'", "Email [email protected] if you continue to have problems."),
127127
new[]
128128
{
129129
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."),

src/GitHub.App/ViewModels/Dialog/Clone/RepositoryCloneViewModel.cs

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
using System;
22
using System.Collections.Generic;
33
using System.ComponentModel.Composition;
4+
using System.Globalization;
45
using System.Linq;
56
using System.Reactive.Linq;
67
using System.Threading.Tasks;
7-
using GitHub.App;
88
using GitHub.Extensions;
99
using GitHub.Logging;
1010
using GitHub.Models;
@@ -23,19 +23,21 @@ public class RepositoryCloneViewModel : ViewModelBase, IRepositoryCloneViewModel
2323
readonly IOperatingSystem os;
2424
readonly IConnectionManager connectionManager;
2525
readonly IRepositoryCloneService service;
26+
readonly IGitService gitService;
2627
readonly IUsageService usageService;
2728
readonly IUsageTracker usageTracker;
2829
readonly IReadOnlyList<IRepositoryCloneTabViewModel> tabs;
2930
string path;
3031
IRepositoryModel previousRepository;
31-
ObservableAsPropertyHelper<string> pathError;
32+
ObservableAsPropertyHelper<string> pathWarning;
3233
int selectedTabIndex;
3334

3435
[ImportingConstructor]
3536
public RepositoryCloneViewModel(
3637
IOperatingSystem os,
3738
IConnectionManager connectionManager,
3839
IRepositoryCloneService service,
40+
IGitService gitService,
3941
IUsageService usageService,
4042
IUsageTracker usageTracker,
4143
IRepositorySelectViewModel gitHubTab,
@@ -45,6 +47,7 @@ public RepositoryCloneViewModel(
4547
this.os = os;
4648
this.connectionManager = connectionManager;
4749
this.service = service;
50+
this.gitService = gitService;
4851
this.usageService = usageService;
4952
this.usageTracker = usageTracker;
5053

@@ -59,22 +62,27 @@ public RepositoryCloneViewModel(
5962
Path = service.DefaultClonePath;
6063
repository.Subscribe(x => UpdatePath(x));
6164

62-
pathError = Observable.CombineLatest(
65+
pathWarning = Observable.CombineLatest(
6366
repository,
6467
this.WhenAnyValue(x => x.Path),
65-
ValidatePath)
66-
.ToProperty(this, x => x.PathError);
68+
ValidatePathWarning)
69+
.ToProperty(this, x => x.PathWarning);
6770

6871
var canClone = Observable.CombineLatest(
69-
repository,
70-
this.WhenAnyValue(x => x.PathError),
71-
(repo, error) => (repo, error))
72-
.Select(x => x.repo != null && x.error == null);
72+
repository, this.WhenAnyValue(x => x.Path),
73+
(repo, path) => repo != null && !service.DestinationFileExists(path) && !service.DestinationDirectoryExists(path));
74+
75+
var canOpen = Observable.CombineLatest(
76+
repository, this.WhenAnyValue(x => x.Path),
77+
(repo, path) => repo != null && !service.DestinationFileExists(path) && service.DestinationDirectoryExists(path));
7378

7479
Browse = ReactiveCommand.Create().OnExecuteCompleted(_ => BrowseForDirectory());
7580
Clone = ReactiveCommand.CreateAsyncObservable(
7681
canClone,
77-
_ => repository.Select(x => new CloneDialogResult(Path, x)));
82+
_ => repository.Select(x => new CloneDialogResult(Path, x?.CloneUrl)));
83+
Open = ReactiveCommand.CreateAsyncObservable(
84+
canOpen,
85+
_ => repository.Select(x => new CloneDialogResult(Path, x?.CloneUrl)));
7886
}
7987

8088
public IRepositorySelectViewModel GitHubTab { get; }
@@ -87,22 +95,24 @@ public string Path
8795
set => this.RaiseAndSetIfChanged(ref path, value);
8896
}
8997

90-
public string PathError => pathError.Value;
98+
public string PathWarning => pathWarning.Value;
9199

92100
public int SelectedTabIndex
93101
{
94102
get => selectedTabIndex;
95103
set => this.RaiseAndSetIfChanged(ref selectedTabIndex, value);
96104
}
97105

98-
public string Title => Resources.CloneTitle;
106+
public string Title => Resources.OpenFromGitHubTitle;
99107

100-
public IObservable<object> Done => Clone;
108+
public IObservable<object> Done => Observable.Merge(Clone, Open);
101109

102110
public ReactiveCommand<object> Browse { get; }
103111

104112
public ReactiveCommand<CloneDialogResult> Clone { get; }
105113

114+
public ReactiveCommand<CloneDialogResult> Open { get; }
115+
106116
public async Task InitializeAsync(IConnection connection)
107117
{
108118
var connections = await connectionManager.GetLoadedConnections().ConfigureAwait(false);
@@ -228,13 +238,39 @@ string FindDirWithout(string dir, string match, int levels)
228238
}
229239
}
230240

231-
string ValidatePath(IRepositoryModel repository, string path)
241+
string ValidatePathWarning(IRepositoryModel repositoryModel, string path)
232242
{
233-
if (repository != null)
243+
if (repositoryModel != null)
234244
{
235-
return service.DestinationExists(path) ?
236-
Resources.DestinationAlreadyExists :
237-
null;
245+
if (service.DestinationFileExists(path))
246+
{
247+
return Resources.DestinationAlreadyExists;
248+
}
249+
250+
if (service.DestinationDirectoryExists(path))
251+
{
252+
using (var repository = gitService.GetRepository(path))
253+
{
254+
if (repository == null)
255+
{
256+
return Resources.CantFindARepositoryAtLocalPath;
257+
}
258+
259+
var localUrl = gitService.GetRemoteUri(repository)?.ToRepositoryUrl();
260+
if (localUrl == null)
261+
{
262+
return Resources.LocalRepositoryDoesntHaveARemoteOrigin;
263+
}
264+
265+
var targetUrl = repositoryModel.CloneUrl?.ToRepositoryUrl();
266+
if (localUrl != targetUrl)
267+
{
268+
return string.Format(CultureInfo.CurrentCulture, Resources.LocalRepositoryHasARemoteOf, localUrl);
269+
}
270+
271+
return Resources.YouHaveAlreadyClonedToThisLocation;
272+
}
273+
}
238274
}
239275

240276
return null;

src/GitHub.Exports.Reactive/Services/IRepositoryCloneService.cs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,36 @@ Task CloneRepository(
3434
object progress = null);
3535

3636
/// <summary>
37-
/// Checks whether the specified destination path already exists.
37+
/// Clones the specified repository into the specified directory or opens it if the directory already exists.
38+
/// </summary>
39+
/// <param name="cloneDialogResult">The URL and path of the repository to clone or open.</param>
40+
/// <param name="progress">
41+
/// An object through which to report progress. This must be of type
42+
/// System.IProgress&lt;Microsoft.VisualStudio.Shell.ServiceProgressData&gt;, but
43+
/// as that type is only available in VS2017+ it is typed as <see cref="object"/> here.
44+
/// </param>
45+
/// <returns></returns>
46+
Task CloneOrOpenRepository(
47+
CloneDialogResult cloneDialogResult,
48+
object progress = null);
49+
50+
/// <summary>
51+
/// Checks whether the specified destination directory already exists.
3852
/// </summary>
3953
/// <param name="path">The destination path.</param>
4054
/// <returns>
41-
/// true if a file or directory is already present at <paramref name="path"/>; otherwise false.
55+
/// true if a directory is already present at <paramref name="path"/>; otherwise false.
56+
/// </returns>
57+
bool DestinationDirectoryExists(string path);
58+
59+
/// <summary>
60+
/// Checks whether the specified destination file already exists.
61+
/// </summary>
62+
/// <param name="path">The destination file.</param>
63+
/// <returns>
64+
/// true if a file is already present at <paramref name="path"/>; otherwise false.
4265
/// </returns>
43-
bool DestinationExists(string path);
66+
bool DestinationFileExists(string path);
4467

4568
Task<ViewerRepositoriesModel> ReadViewerRepositories(HostAddress address);
4669
}

src/GitHub.Exports.Reactive/ViewModels/Dialog/Clone/IRepositoryCloneViewModel.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace GitHub.ViewModels.Dialog.Clone
66
{
77
/// <summary>
8-
/// ViewModel for the the Clone Repository dialog
8+
/// ViewModel for the Clone Repository dialog
99
/// </summary>
1010
public interface IRepositoryCloneViewModel : IDialogContentViewModel, IConnectionInitializedViewModel
1111
{
@@ -30,9 +30,9 @@ public interface IRepositoryCloneViewModel : IDialogContentViewModel, IConnectio
3030
string Path { get; set; }
3131

3232
/// <summary>
33-
/// Gets an error message that explains why <see cref="Path"/> is not valid.
33+
/// Gets a warning message that explains why <see cref="Path"/> is suspect.
3434
/// </summary>
35-
string PathError { get; }
35+
string PathWarning { get; }
3636

3737
/// <summary>
3838
/// Gets the index of the selected tab.

src/GitHub.Exports/Models/CloneDialogResult.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using GitHub.Primitives;
23

34
namespace GitHub.Models
45
{
@@ -12,10 +13,10 @@ public class CloneDialogResult
1213
/// </summary>
1314
/// <param name="path">The path to clone the repository to.</param>
1415
/// <param name="repository">The selected repository.</param>
15-
public CloneDialogResult(string path, IRepositoryModel repository)
16+
public CloneDialogResult(string path, UriString cloneUrl)
1617
{
1718
Path = path;
18-
Repository = repository;
19+
Url = cloneUrl;
1920
}
2021

2122
/// <summary>
@@ -24,8 +25,8 @@ public CloneDialogResult(string path, IRepositoryModel repository)
2425
public string Path { get; }
2526

2627
/// <summary>
27-
/// Gets the repository selected by the user.
28+
/// Gets the url selected by the user.
2829
/// </summary>
29-
public IRepositoryModel Repository { get; }
30+
public UriString Url { get; }
3031
}
3132
}

0 commit comments

Comments
 (0)