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

Support hierarchical (with owner) or flat repository directory layout #2430

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c5c959c
Remember folder layout preference between edits
jcansdale Oct 15, 2019
58fb315
Reset bad paths
jcansdale Oct 15, 2019
e07a025
Fix package reference in PackageSettingsGen.tt
jcansdale Oct 15, 2019
b1d89e1
Add default repository location/layout settings
jcansdale Oct 16, 2019
7c57637
Consolidate default clone path responsibilities
jcansdale Oct 16, 2019
f8f03d1
Set default clone path after cloning
jcansdale Oct 16, 2019
48491d0
Remember user's preferred repository layout
jcansdale Oct 16, 2019
72f631d
Remove dead code
jcansdale Oct 16, 2019
62207e8
Factor out repository layout logic
jcansdale Oct 18, 2019
129154f
Remove layout logic from RepositoryCloneService
jcansdale Oct 18, 2019
09a84d9
Use RepositoryLayout.Default when unitialized
jcansdale Oct 18, 2019
c9973de
Add tests fro RepositoryLayoutUtilities
jcansdale Oct 22, 2019
dfbf96a
Add tests for GetRepositoryLayout
jcansdale Oct 25, 2019
ae3c092
Add tests for GetDefaultRepositoryPath
jcansdale Oct 25, 2019
81cb9be
Do case sensitive match of owner
jcansdale Oct 25, 2019
6c77d12
Use same capitalization as underlying file system
jcansdale Oct 25, 2019
372988a
Merge branch 'master' into fixes/2428-flexible-repository-dir-layout
jcansdale Oct 25, 2019
13aa808
Exclude "GitHub" dirs from being a possible owner
jcansdale Nov 2, 2019
3fa718d
Revert "Use same capitalization as underlying file system"
jcansdale Nov 2, 2019
c7881e2
Merge branch 'master' into fixes/2428-flexible-repository-dir-layout
jcansdale Nov 12, 2019
eb1b2e1
Merge branch 'master' into fixes/2428-flexible-repository-dir-layout
jcansdale Mar 13, 2020
a564d16
Merge branch 'master' into fixes/2428-flexible-repository-dir-layout
jcansdale Mar 15, 2020
939d43a
Merge branch 'master' into fixes/2428-flexible-repository-dir-layout
jcansdale Mar 17, 2020
c2abdd0
Merge branch 'master' into fixes/2428-flexible-repository-dir-layout
jcansdale Jun 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions src/GitHub.App/Services/RepositoryCloneService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using GitHub.Logging;
using GitHub.Models;
using GitHub.Primitives;
using GitHub.Settings;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Threading;
using Octokit.GraphQL;
Expand Down Expand Up @@ -39,6 +40,7 @@ public class RepositoryCloneService : IRepositoryCloneService
readonly IGraphQLClientFactory graphqlFactory;
readonly IGitHubContextService gitHubContextService;
readonly IUsageTracker usageTracker;
readonly Lazy<IPackageSettings> packageSettings;
readonly Lazy<EnvDTE.DTE> dte;
ICompiledQuery<ViewerRepositoriesModel> readViewerRepositories;

Expand All @@ -51,6 +53,7 @@ public RepositoryCloneService(
IGitHubContextService gitHubContextService,
IUsageTracker usageTracker,
IGitHubServiceProvider sp,
Lazy<IPackageSettings> packageSettings,
[Import(AllowDefault = true)] JoinableTaskContext joinableTaskContext)
{
this.operatingSystem = operatingSystem;
Expand All @@ -59,10 +62,9 @@ public RepositoryCloneService(
this.graphqlFactory = graphqlFactory;
this.gitHubContextService = gitHubContextService;
this.usageTracker = usageTracker;
this.packageSettings = packageSettings;
dte = new Lazy<EnvDTE.DTE>(() => sp.GetService<EnvDTE.DTE>());
JoinableTaskContext = joinableTaskContext ?? ThreadHelper.JoinableTaskContext;

defaultClonePath = GetLocalClonePathFromGitProvider(operatingSystem.Environment.GetUserRepositoriesPath());
}

/// <inheritdoc/>
Expand Down Expand Up @@ -225,6 +227,8 @@ public async Task CloneRepository(
// Count the number of times users clone into the Default Repository Location
await usageTracker.IncrementCounter(x => x.NumberOfClonesToDefaultClonePath);
}

SetDefaultClonePath(repositoryPath, cloneUrl);
}
catch (Exception ex)
{
Expand All @@ -251,13 +255,38 @@ string GetLocalClonePathFromGitProvider(string fallbackPath)
: fallbackPath;
}

public string DefaultClonePath { get { return defaultClonePath; } }
public void SetDefaultClonePath(string repositoryPath, UriString cloneUrl)
{
var (defaultPath, repositoryLayout) = RepositoryLayoutUtilities.GetDefaultPathAndLayout(repositoryPath, cloneUrl);

JoinableTaskContext JoinableTaskContext { get; }
log.Information("Setting DefaultRepositoryLocation to {Location}", defaultPath);
packageSettings.Value.DefaultRepositoryLocation = defaultPath;

log.Information("Setting DefaultRepositoryLayout to {Layout}", repositoryLayout);
packageSettings.Value.DefaultRepositoryLayout = repositoryLayout.ToString();

class OrganizationAdapter
packageSettings.Value.Save();
}

public RepositoryLayout DefaultRepositoryLayout
{
public IReadOnlyList<RepositoryListItemModel> Repositories { get; set; }
get => RepositoryLayoutUtilities.GetRepositoryLayout(packageSettings.Value.DefaultRepositoryLayout);
}

public string DefaultClonePath
{
get
{
var defaultPath = packageSettings.Value.DefaultRepositoryLocation;
if (!string.IsNullOrEmpty(defaultPath))
{
return defaultPath;
}

return GetLocalClonePathFromGitProvider(operatingSystem.Environment.GetUserRepositoriesPath());
}
}

JoinableTaskContext JoinableTaskContext { get; }
}
}
49 changes: 49 additions & 0 deletions src/GitHub.App/Services/RepositoryLayoutUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.IO;
using GitHub.Primitives;

namespace GitHub.Services
{
public static class RepositoryLayoutUtilities
{
// Exclude directories with a case sensitive name of "GitHub" from being a possible owner.
// GitHub Desktop uses "GitHub" as its default directory and this isn't a user or organization name.
const string GitHubDirectoryName = "GitHub";

public static string GetDefaultRepositoryPath(UriString cloneUrl, string defaultPath, RepositoryLayout repositoryLayout)
{
switch (repositoryLayout)
{
case RepositoryLayout.Name:
return Path.Combine(defaultPath, cloneUrl.RepositoryName);
case RepositoryLayout.Default:
case RepositoryLayout.OwnerName:
return Path.Combine(defaultPath, cloneUrl.Owner, cloneUrl.RepositoryName);
default:
throw new ArgumentException($"Unknown repository layout: {repositoryLayout}");

}
}

public static RepositoryLayout GetRepositoryLayout(string repositoryLayoutSetting)
{
return Enum.TryParse(repositoryLayoutSetting, out RepositoryLayout repositoryLayout) ?
repositoryLayout : RepositoryLayout.Default;
}

public static (string, RepositoryLayout) GetDefaultPathAndLayout(string repositoryPath, UriString cloneUrl)
{
var possibleOwnerPath = Path.GetDirectoryName(repositoryPath);
var possibleOwner = Path.GetFileName(possibleOwnerPath);
if (string.Equals(possibleOwner, cloneUrl.Owner, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(possibleOwner, GitHubDirectoryName, StringComparison.Ordinal))
{
return (Path.GetDirectoryName(possibleOwnerPath), RepositoryLayout.OwnerName);
}
else
{
return (possibleOwnerPath, RepositoryLayout.Name);
}
}
}
}
56 changes: 14 additions & 42 deletions src/GitHub.App/ViewModels/Dialog/Clone/RepositoryCloneViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,55 +210,27 @@ void UpdatePath(RepositoryModel repository)
{
if (repository != null)
{
var basePath = GetUpdatedBasePath(Path);
previousRepository = repository;
Path = System.IO.Path.Combine(basePath, repository.Owner, repository.Name);
}
}

string GetUpdatedBasePath(string path)
{
if (string.IsNullOrEmpty(path))
{
return service.DefaultClonePath;
}

if (previousRepository == null)
{
return path;
}

if (FindDirWithout(path, previousRepository?.Owner, 2) is string dirWithoutOwner)
{
return dirWithoutOwner;
}

if (FindDirWithout(path, previousRepository?.Name, 1) is string dirWithoutRepo)
{
return dirWithoutRepo;
}

return path;

string FindDirWithout(string dir, string match, int levels)
{
string dirWithout = null;
for (var i = 0; i < 2; i++)
try
{
if (string.IsNullOrEmpty(dir))
if (previousRepository != null && !string.IsNullOrEmpty(Path))
{
break;
var (path, layout) = RepositoryLayoutUtilities.GetDefaultPathAndLayout(Path, previousRepository.CloneUrl);
Path = RepositoryLayoutUtilities.GetDefaultRepositoryPath(repository.CloneUrl, path, layout);
}

var name = System.IO.Path.GetFileName(dir);
dir = System.IO.Path.GetDirectoryName(dir);
if (name == match)
else
{
dirWithout = dir;
Path = RepositoryLayoutUtilities.GetDefaultRepositoryPath(repository.CloneUrl,
service.DefaultClonePath, service.DefaultRepositoryLayout);
}
}
catch (Exception)
{
// Reset illegal paths
Path = RepositoryLayoutUtilities.GetDefaultRepositoryPath(repository.CloneUrl,
service.DefaultClonePath, service.DefaultRepositoryLayout);
}

return dirWithout;
previousRepository = repository;
}
}

Expand Down
13 changes: 11 additions & 2 deletions src/GitHub.Exports.Reactive/Services/IRepositoryCloneService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,20 @@ namespace GitHub.Services
public interface IRepositoryCloneService
{
/// <summary>
/// Default path to clone things to, used as fallback if we can't find the correct path
/// from VS.
/// Default path to clone things to
/// </summary>
string DefaultClonePath { get; }

/// <summary>
/// Default layout of repository directories.
/// </summary>
RepositoryLayout DefaultRepositoryLayout { get; }

/// <summary>
/// Infer the default clone path and layout from an example repository path and clone URL.
/// </summary>
void SetDefaultClonePath(string repositoryPath, UriString cloneUrl);

/// <summary>
/// Clones the specificed repository into the specified directory.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions src/GitHub.Exports.Reactive/Services/RepositoryLayout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace GitHub.Services
{
public enum RepositoryLayout
{
Default, // Layout hasn't been specified
Name, // Layout repositories by name
OwnerName // Layout repositories by owner and name
}
}
2 changes: 2 additions & 0 deletions src/GitHub.Exports/Settings/generated/IPackageSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ public interface IPackageSettings : INotifyPropertyChanged
UIState UIState { get; set; }
bool HideTeamExplorerWelcomeMessage { get; set; }
bool EnableTraceLogging { get; set; }
string DefaultRepositoryLocation { get; set; }
string DefaultRepositoryLayout { get; set; }
}
}
30 changes: 29 additions & 1 deletion src/GitHub.VisualStudio/Settings/generated/PackageSettingsGen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{
"name": "CollectMetrics",
"type": "bool",
"default": 'true'
"default": "true"
},
{
"name": "UIState",
Expand All @@ -22,6 +22,16 @@
"name": "EnableTraceLogging",
"type": "bool",
"default": "false"
},
{
"name": "DefaultRepositoryLocation",
"type": "string",
"default": "null"
},
{
"name": "DefaultRepositoryLayout",
"type": "string",
"default": "null"
}
]
}
Expand Down Expand Up @@ -71,13 +81,29 @@ public bool EnableTraceLogging
set { enableTraceLogging = value; this.RaisePropertyChange(); }
}

string defaultRepositoryLocation;
public string DefaultRepositoryLocation
{
get { return defaultRepositoryLocation; }
set { defaultRepositoryLocation = value; this.RaisePropertyChange(); }
}

string defaultRepositoryLayout;
public string DefaultRepositoryLayout
{
get { return defaultRepositoryLayout; }
set { defaultRepositoryLayout = value; this.RaisePropertyChange(); }
}


void LoadSettings()
{
CollectMetrics = (bool)settingsStore.Read("CollectMetrics", true);
UIState = SimpleJson.DeserializeObject<UIState>((string)settingsStore.Read("UIState", "{}"));
HideTeamExplorerWelcomeMessage = (bool)settingsStore.Read("HideTeamExplorerWelcomeMessage", false);
EnableTraceLogging = (bool)settingsStore.Read("EnableTraceLogging", false);
DefaultRepositoryLocation = (string)settingsStore.Read("DefaultRepositoryLocation", null);
DefaultRepositoryLayout = (string)settingsStore.Read("DefaultRepositoryLayout", null);
}

void SaveSettings()
Expand All @@ -86,6 +112,8 @@ void SaveSettings()
settingsStore.Write("UIState", SimpleJson.SerializeObject(UIState));
settingsStore.Write("HideTeamExplorerWelcomeMessage", HideTeamExplorerWelcomeMessage);
settingsStore.Write("EnableTraceLogging", EnableTraceLogging);
settingsStore.Write("DefaultRepositoryLocation", DefaultRepositoryLocation);
settingsStore.Write("DefaultRepositoryLayout", DefaultRepositoryLayout);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ assembly name="$(PackageDir)\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll" #>
<#@ assembly name="$(USERPROFILE)\.nuget\packages\Newtonsoft.Json\6.0.8\lib\net45\Newtonsoft.Json.dll" #>
<#@ import namespace="Newtonsoft.Json.Linq" #>
<#@ output extension=".cs" #>
<#
Expand Down
12 changes: 11 additions & 1 deletion src/common/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"name": "CollectMetrics",
"type": "bool",
"default": 'true'
"default": "true"
},
{
"name": "UIState",
Expand All @@ -20,6 +20,16 @@
"name": "EnableTraceLogging",
"type": "bool",
"default": "false"
},
{
"name": "DefaultRepositoryLocation",
"type": "string",
"default": "null"
},
{
"name": "DefaultRepositoryLayout",
"type": "string",
"default": "null"
}
]
}
Loading