From 28e2cc3b91f6189b690b09103f0ae10c7d9bb562 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Wed, 24 Jan 2024 14:29:15 -0800 Subject: [PATCH 01/15] Building with new SDK. Have not tested --- .../Helpers/AzureRepositoryHierarchy.cs | 140 ++++++++++ .../Providers/DevHomeRepository.cs | 2 +- .../Providers/RepositoryProvider.cs | 244 +++++++++--------- 3 files changed, 267 insertions(+), 119 deletions(-) create mode 100644 src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs diff --git a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs new file mode 100644 index 00000000..6ec8be65 --- /dev/null +++ b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using DevHomeAzureExtension.Client; +using DevHomeAzureExtension.DeveloperId; +using Microsoft.TeamFoundation.Core.WebApi; +using Microsoft.VisualStudio.Services.Account.Client; +using Microsoft.VisualStudio.Services.Common; +using Microsoft.VisualStudio.Services.WebApi; + +// In the past, an organization was known as an account. Typedef to organization to make the code easier to read. +using Organization = Microsoft.VisualStudio.Services.Account.Account; + +namespace AzureExtension.Helpers; + +public class AzureRepositoryHierarchy +{ + private readonly DeveloperId _developerId; + + private readonly Task _buildingHierarchyTask; + + private List>> _organizationsToTheirProjects = new(); + + /// + /// Initializes a new instance of the class. + /// Class to handle searching organizations and projects within a server. + /// + /// A developerId to an ADO instance. + /// + /// A server can have multiple organizations and each organization can have multiple projects. + /// Additionally, organizations and projects need to be fetched from the network. + /// This calss handles fetching the data, caching it, and searching it. + /// + public AzureRepositoryHierarchy(DeveloperId developerId) + { + _developerId = developerId; + _buildingHierarchyTask = BuildHierarchy(); + } + + public List GetOrganizations(string? project = null) + { + Task.WaitAny(_buildingHierarchyTask); + + IEnumerable>> organizationsToReturn = _organizationsToTheirProjects; + + if (!string.IsNullOrEmpty(project)) + { + organizationsToReturn = organizationsToReturn.Where(x => x.Value.Any(y => y.Name.Contains(project))); + } + + return organizationsToReturn.Select(x => x.Key).ToList(); + } + + public List GetProjects(string? organization = null) + { + Task.WaitAny(_buildingHierarchyTask); + + IEnumerable>> projectsToReturn = _organizationsToTheirProjects; + + if (!string.IsNullOrEmpty(organization)) + { + projectsToReturn = projectsToReturn.Where(x => x.Key.AccountName.Equals(organization, StringComparison.OrdinalIgnoreCase)); + } + + List projectNames = new(); + projectsToReturn.ForEach(x => projectNames.AddRange(x.Value)); + + return projectNames; + } + + private async Task BuildHierarchy() + { + await QueryForOrganizations(); + + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = -1, + }; + + Parallel.ForEach(_organizationsToTheirProjects.Select(x => x.Key), options, async organization => + { + var projects = await QueryForProjects(organization); + + // Only add the organization and projects if the organization hasn't been added yet. + if (!_organizationsToTheirProjects.Select(x => x.Key).Contains(organization)) + { + _organizationsToTheirProjects.Add(new KeyValuePair>(organization, projects)); + } + }); + } + + private async Task> QueryForOrganizations() + { + // Establish a connection to get all organizations. + // The library calls these "accounts". + VssConnection accountConnection; + + try + { + accountConnection = AzureClientProvider.GetConnectionForLoggedInDeveloper(new Uri(@"https://app.vssps.visualstudio.com/"), _developerId); + } + catch + { + return new List(); + } + + var accountClient = accountConnection.GetClient(); + + try + { + return await accountClient.GetAccountsByMemberAsync( + memberId: accountConnection.AuthorizedIdentity.Id, new List { "organization" }); + } + catch + { + return new List(); + } + } + + private async Task> QueryForProjects(Organization organization) + { + try + { + var connection = AzureClientProvider.GetConnectionForLoggedInDeveloper(organization.AccountUri, _developerId); + + // connection can be null if the organization is disabled. + if (connection != null) + { + var projectClient = connection.GetClient(); + return (await projectClient.GetProjects()).ToList(); + } + } + catch (Exception e) + { + DevHomeAzureExtension.Client.Log.Logger()?.ReportError("DevHomeRepository", e); + } + + return new List(); + } +} diff --git a/src/AzureExtension/Providers/DevHomeRepository.cs b/src/AzureExtension/Providers/DevHomeRepository.cs index dfcdc31d..e5f14789 100644 --- a/src/AzureExtension/Providers/DevHomeRepository.cs +++ b/src/AzureExtension/Providers/DevHomeRepository.cs @@ -45,7 +45,7 @@ public DevHomeRepository(GitRepository gitRepository) } var repoInformation = new AzureUri(localUrl); - _owningAccountName = Path.Join(repoInformation.Organization, repoInformation.Repository); + _owningAccountName = Path.Join(repoInformation.Organization, repoInformation.Project); cloneUrl = localUrl; diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index fafc773d..5cff1ecd 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -3,9 +3,11 @@ using System.Security.Authentication; using System.Text; +using AzureExtension.Helpers; using DevHomeAzureExtension.Client; using DevHomeAzureExtension.DeveloperId; using DevHomeAzureExtension.Helpers; +using Microsoft.Azure.Pipelines.WebApi; using Microsoft.Identity.Client; using Microsoft.TeamFoundation.Core.WebApi; using Microsoft.TeamFoundation.SourceControl.WebApi; @@ -16,10 +18,25 @@ using Windows.Foundation; using Windows.Storage.Streams; +// In the past, an organization was known as an account. Typedef to organization to make the code easier to read. +using Organization = Microsoft.VisualStudio.Services.Account.Account; + namespace DevHomeAzureExtension.Providers; -public class RepositoryProvider : IRepositoryProvider +public class RepositoryProvider : IRepositoryProvider, IRepositoryProvider2, IRepositoryData { + private Dictionary> _organizationsToProjects; + + private AzureRepositoryHierarchy? _azureHierarchy; + + private const string _orgnization = "Organization"; + + private const string _project = "Project"; + + private string _organizationName = string.Empty; + + private string _projectName = string.Empty; + public string DisplayName => Resources.GetResource(@"RepositoryProviderDisplayName"); public IRandomAccessStreamReference Icon @@ -30,11 +47,12 @@ public IRandomAccessStreamReference Icon public RepositoryProvider(IRandomAccessStreamReference icon) { Icon = icon; + _organizationsToProjects = new Dictionary>(); } public RepositoryProvider() + : this(RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///AzureExtension/Assets/AzureExtensionDark.png"))) { - Icon = RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///AzureExtension/Assets/AzureExtensionDark.png")); } public IAsyncOperation IsUriSupportedAsync(Uri uri) @@ -62,33 +80,43 @@ public IAsyncOperation IsUriSupportedAsync(Uri uri, } /// - /// Get all projects for an organization. + /// Gets all repos from the project that the user did a pull request into /// - /// The org to look under. - /// The developerId to get the connection. - /// A list of projects the organization has. - private List GetProjects(Microsoft.VisualStudio.Services.Account.Account organization, DeveloperId.DeveloperId azureDeveloperId) + private List GetRepos(VssConnection connection, TeamProjectReference project, GitPullRequestSearchCriteria criteria) { + Log.Logger()?.ReportInfo("DevHomeRepository", $"Getting all repos for {project.Name}"); try { - var connection = AzureClientProvider.GetConnectionForLoggedInDeveloper(organization.AccountUri, azureDeveloperId); + var gitClient = connection.GetClient(); + var repos = gitClient.GetRepositoriesAsync(project.Id, true, true).Result.OrderBy(x => x.Name); - // connection can be null if the organization is disabled. - if (connection != null) + var reposToReturn = new List(); + foreach (var repo in repos) { - var projectClient = connection.GetClient(); - return projectClient.GetProjects().Result.ToList(); + try + { + var pullRequests = gitClient.GetPullRequestsAsync(repo.Id, criteria).Result; + + if (pullRequests.Count != 0) + { + reposToReturn.Add(new DevHomeRepository(repo)); + } + } + catch + { + continue; + } } + + return reposToReturn; } - catch (Exception e) + catch { - Providers.Log.Logger()?.ReportError("DevHomeRepository", e); + return new List(); } - - return new List(); } - IAsyncOperation IRepositoryProvider.GetRepositoriesAsync(IDeveloperId developerId) + public IAsyncOperation GetRepositoriesAsync(IDeveloperId developerId) { // Get access token for ADO API calls. if (developerId is not DeveloperId.DeveloperId azureDeveloperId) @@ -100,124 +128,35 @@ IAsyncOperation IRepositoryProvider.GetRepositoriesAsync(IDe }).AsAsyncOperation(); } - // Establish a connection to get all organizations. - // The library calls these organizations "accounts". - VssConnection accountConnection; - - try - { - accountConnection = AzureClientProvider.GetConnectionForLoggedInDeveloper(new Uri(@"https://app.vssps.visualstudio.com/"), azureDeveloperId); - } - catch (Exception e) - { - return Task.Run(() => - { - return new RepositoriesResult(e, "Could not establish a connection for this account"); - }).AsAsyncOperation(); - } - - var accountClient = accountConnection.GetClient(); - - // Get all organizations the current user belongs to. - var internalDevId = DeveloperIdProvider.GetInstance().GetDeveloperIdInternal(developerId); - IReadOnlyList? theseOrganizations; + _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); - try - { - theseOrganizations = accountClient.GetAccountsByMemberAsync( - memberId: accountConnection.AuthorizedIdentity.Id).Result; - } - catch (Exception e) - { - return Task.Run(() => - { - return new RepositoriesResult(e, "Could not get the member id for this user."); - }).AsAsyncOperation(); - } + var reposToReturn = new List(); - // Set up parallel options to get all projects for each organization. var options = new ParallelOptions() { - MaxDegreeOfParallelism = Environment.ProcessorCount, + // Considering these are API calls, having more degrees than Environment.CoreCount should be okay. + MaxDegreeOfParallelism = -1, }; - var projectsWithOrgName = new List<(TeamProjectReference, Microsoft.VisualStudio.Services.Account.Account)>(); - - try - { - Parallel.ForEach(theseOrganizations, options, organization => - { - var projects = GetProjects(organization, azureDeveloperId); - if (projects.Any()) - { - foreach (var project in projects) - { - projectsWithOrgName.Add((project, organization)); - } - } - }); - } - catch (AggregateException aggregateException) - { - var exceptionMessages = new StringBuilder(); - - foreach (var exceptionMessage in aggregateException.InnerExceptions) - { - exceptionMessages.AppendLine(exceptionMessage.Message); - } - - return Task.Run(() => - { - return new RepositoriesResult(aggregateException, exceptionMessages.ToString()); - }).AsAsyncOperation(); - } - catch (Exception e) + Parallel.ForEach(_azureHierarchy.GetOrganizations(), options, orgnization => { - return Task.Run(() => - { - return new RepositoriesResult(e, e.Message); - }).AsAsyncOperation(); - } - - projectsWithOrgName = projectsWithOrgName.OrderByDescending(x => x.Item1.LastUpdateTime).ToList(); - - // Get a list of the 200 most recently updated repos. - var reposToReturn = new List(); - foreach (var projectWithOrgName in projectsWithOrgName) - { - // Hard limit on 200. - // TODO: Figure out a better way to limit results, or, at least, pagination. - if (reposToReturn.Count >= 200) - { - break; - } - try { // Making a connection can throw. - var connection = AzureClientProvider.GetConnectionForLoggedInDeveloper(projectWithOrgName.Item2.AccountUri, azureDeveloperId); + var connection = AzureClientProvider.GetConnectionForLoggedInDeveloper(orgnization.AccountUri, azureDeveloperId); + var criteria = new GitPullRequestSearchCriteria(); + criteria.CreatorId = connection.AuthorizedIdentity.Id; - // Make the GitHttpClient inside try/catch because an exception happens if the project is disabled. - var gitClient = connection.GetClient(); - var repos = gitClient.GetRepositoriesAsync(projectWithOrgName.Item1.Id, false, false).Result; - foreach (var repo in repos) + Parallel.ForEach(_azureHierarchy.GetProjects(orgnization.AccountName), options, project => { - if (repo.IsDisabled.HasValue && !repo.IsDisabled.Value) - { - if (reposToReturn.Count >= 200) - { - break; - } - - reposToReturn.Add(new DevHomeRepository(repo)); - } - } + reposToReturn.AddRange(GetRepos(connection, project, criteria)); + }); } catch (Exception e) { Providers.Log.Logger()?.ReportError("DevHomeRepository", e); } - } + }); return Task.Run(() => { @@ -394,4 +333,73 @@ public void Dispose() { GC.SuppressFinalize(this); } + + public IEnumerable GetRepositoriesAsync(IDeveloperId developerId, IDictionary searchTerms) + { + if (searchTerms.ContainsKey(_orgnization)) + { + _organizationName = searchTerms[_organizationName]; + } + else + { + _organizationName = string.Empty; + } + + if (searchTerms.ContainsKey(_project)) + { + _projectName = searchTerms[_project]; + } + else + { + _projectName = string.Empty; + } + + var foundRepositoriesAwaiter = GetRepositoriesAsync(developerId).GetAwaiter(); + + while (!foundRepositoriesAwaiter.IsCompleted) + { + Thread.Sleep(1000); + } + + var foundRepositories = foundRepositoriesAwaiter.GetResult().Repositories; + return foundRepositories; + } + + public IReadOnlyList GetSearchFieldNames() + { + return new List { "Organization", "Project" }; + } + + public IReadOnlyList GetValuesBasedOn(IDictionary inputValues, string fieldNameToRetrieve, IDeveloperId developerId) + { + if (developerId is not DeveloperId.DeveloperId azureDeveloperId) + { + throw new NotSupportedException("Authenticated user is not an azure developer id"); + } + + _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); + + if (fieldNameToRetrieve.Equals(_orgnization, StringComparison.OrdinalIgnoreCase)) + { + var inputProject = string.Empty; + if (inputValues.ContainsKey(_project)) + { + inputProject = inputValues[_project]; + } + + return _azureHierarchy.GetOrganizations(inputProject).Select(x => x.AccountName).ToList(); + } + else if (fieldNameToRetrieve.Equals(_project, StringComparison.OrdinalIgnoreCase)) + { + var inputOrganization = string.Empty; + if (inputValues.ContainsKey(_orgnization)) + { + inputOrganization = inputValues[_orgnization]; + } + + return _azureHierarchy.GetProjects(inputOrganization).Select(x => x.Name).ToList(); + } + + return new List(); + } } From b30554c768e54b89ab8064098e684ab7a2378931 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Fri, 2 Feb 2024 15:23:19 -0800 Subject: [PATCH 02/15] WIP --- nuget.config | 1 + src/AzureExtension/AzureExtension.csproj | 2 +- .../Helpers/AzureRepositoryHierarchy.cs | 90 ++--- .../Providers/RepositoryProvider.cs | 346 +++++++++++++----- .../AzureExtensionServer.csproj | 2 +- src/Logging/DevHome.Logging.csproj | 2 +- src/Telemetry/AzureExtension.Telemetry.csproj | 2 +- .../AzureExtension/AzureExtension.Test.csproj | 4 +- .../Console/AzureExtension.TestConsole.csproj | 4 +- 9 files changed, 313 insertions(+), 140 deletions(-) diff --git a/nuget.config b/nuget.config index 6285fe02..a98931e2 100644 --- a/nuget.config +++ b/nuget.config @@ -8,6 +8,7 @@ + diff --git a/src/AzureExtension/AzureExtension.csproj b/src/AzureExtension/AzureExtension.csproj index b2efa0c7..769b9b21 100644 --- a/src/AzureExtension/AzureExtension.csproj +++ b/src/AzureExtension/AzureExtension.csproj @@ -57,7 +57,7 @@ - + diff --git a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs index 6ec8be65..db91ff9b 100644 --- a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs +++ b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs @@ -5,7 +5,7 @@ using DevHomeAzureExtension.DeveloperId; using Microsoft.TeamFoundation.Core.WebApi; using Microsoft.VisualStudio.Services.Account.Client; -using Microsoft.VisualStudio.Services.Common; +using Microsoft.VisualStudio.Services.Organization; using Microsoft.VisualStudio.Services.WebApi; // In the past, an organization was known as an account. Typedef to organization to make the code easier to read. @@ -17,9 +17,11 @@ public class AzureRepositoryHierarchy { private readonly DeveloperId _developerId; - private readonly Task _buildingHierarchyTask; + private readonly Dictionary> _organizationsAndProjects; - private List>> _organizationsToTheirProjects = new(); + private Task>? _queryOrganizationsTask; + + private Dictionary>> _organizationsAndProjectTask; /// /// Initializes a new instance of the class. @@ -34,62 +36,55 @@ public class AzureRepositoryHierarchy public AzureRepositoryHierarchy(DeveloperId developerId) { _developerId = developerId; - _buildingHierarchyTask = BuildHierarchy(); + _organizationsAndProjects = new Dictionary>(); + _organizationsAndProjectTask = new Dictionary>>(); } - public List GetOrganizations(string? project = null) + public async Task> GetOrganizationsAsync() { - Task.WaitAny(_buildingHierarchyTask); - - IEnumerable>> organizationsToReturn = _organizationsToTheirProjects; - - if (!string.IsNullOrEmpty(project)) + // if something is running the query, wait for it to finish. + if (_queryOrganizationsTask != null) { - organizationsToReturn = organizationsToReturn.Where(x => x.Value.Any(y => y.Name.Contains(project))); + await _queryOrganizationsTask; + return _organizationsAndProjects.Keys.ToList(); } - return organizationsToReturn.Select(x => x.Key).ToList(); - } - - public List GetProjects(string? organization = null) - { - Task.WaitAny(_buildingHierarchyTask); - - IEnumerable>> projectsToReturn = _organizationsToTheirProjects; - - if (!string.IsNullOrEmpty(organization)) + var organizations = QueryForOrganizations(); + foreach (var organization in organizations) { - projectsToReturn = projectsToReturn.Where(x => x.Key.AccountName.Equals(organization, StringComparison.OrdinalIgnoreCase)); + // Extra protection against duplicates if two threads run QueryForOrganizations at the same time. + _organizationsAndProjects.TryAdd(organization, new List()); } - List projectNames = new(); - projectsToReturn.ForEach(x => projectNames.AddRange(x.Value)); - - return projectNames; + return _organizationsAndProjects.Keys.ToList(); } - private async Task BuildHierarchy() + public async Task> GetProjectsAsync(Organization organization) { - await QueryForOrganizations(); - - var options = new ParallelOptions() + // if something is running the query, wait for it to finish. + if (_queryOrganizationsTask != null) { - MaxDegreeOfParallelism = -1, - }; + await _queryOrganizationsTask; + } - Parallel.ForEach(_organizationsToTheirProjects.Select(x => x.Key), options, async organization => + // if the task has been started + if (_organizationsAndProjectTask.ContainsKey(organization)) { - var projects = await QueryForProjects(organization); - - // Only add the organization and projects if the organization hasn't been added yet. - if (!_organizationsToTheirProjects.Select(x => x.Key).Contains(organization)) + if (_organizationsAndProjectTask[organization] != null) { - _organizationsToTheirProjects.Add(new KeyValuePair>(organization, projects)); + // Wait for it. + await _organizationsAndProjectTask[organization]; + return _organizationsAndProjects[organization]; } - }); + } + + var projects = QueryForProjects(organization); + _organizationsAndProjects[organization] = projects; + + return _organizationsAndProjects[organization]; } - private async Task> QueryForOrganizations() + private List QueryForOrganizations() { // Establish a connection to get all organizations. // The library calls these "accounts". @@ -108,8 +103,9 @@ private async Task> QueryForOrganizations() try { - return await accountClient.GetAccountsByMemberAsync( - memberId: accountConnection.AuthorizedIdentity.Id, new List { "organization" }); + _queryOrganizationsTask = accountClient.GetAccountsByMemberAsync( + memberId: accountConnection.AuthorizedIdentity.Id); + return _queryOrganizationsTask.Result; } catch { @@ -117,7 +113,7 @@ private async Task> QueryForOrganizations() } } - private async Task> QueryForProjects(Organization organization) + private List QueryForProjects(Organization organization) { try { @@ -127,7 +123,13 @@ private async Task> QueryForProjects(Organization org if (connection != null) { var projectClient = connection.GetClient(); - return (await projectClient.GetProjects()).ToList(); + var getProjectsTask = projectClient.GetProjects(); + + // Add the task if it does not yet exist. + _organizationsAndProjectTask.TryAdd(organization, getProjectsTask); + + // in both cases, wait for the task to finish. + return _organizationsAndProjectTask[organization].Result.ToList(); } } catch (Exception e) diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index 5cff1ecd..9b265582 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -1,17 +1,15 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. +using System.Drawing; using System.Security.Authentication; -using System.Text; using AzureExtension.Helpers; using DevHomeAzureExtension.Client; using DevHomeAzureExtension.DeveloperId; using DevHomeAzureExtension.Helpers; -using Microsoft.Azure.Pipelines.WebApi; using Microsoft.Identity.Client; using Microsoft.TeamFoundation.Core.WebApi; using Microsoft.TeamFoundation.SourceControl.WebApi; -using Microsoft.VisualStudio.Services.Account.Client; using Microsoft.VisualStudio.Services.Client; using Microsoft.VisualStudio.Services.WebApi; using Microsoft.Windows.DevHome.SDK; @@ -25,17 +23,19 @@ namespace DevHomeAzureExtension.Providers; public class RepositoryProvider : IRepositoryProvider, IRepositoryProvider2, IRepositoryData { - private Dictionary> _organizationsToProjects; - private AzureRepositoryHierarchy? _azureHierarchy; - private const string _orgnization = "Organization"; + private const string _server = "server"; + + private const string _organization = "organization"; + + private const string _project = "project"; - private const string _project = "Project"; + private string _selectedServer; - private string _organizationName = string.Empty; + private string _selectedOrganization; - private string _projectName = string.Empty; + private string _selectedProject; public string DisplayName => Resources.GetResource(@"RepositoryProviderDisplayName"); @@ -44,10 +44,14 @@ public IRandomAccessStreamReference Icon get; private set; } + string[] IRepositoryData.GetSearchFieldNames => new string[3] { _server, _organization, _project }; + public RepositoryProvider(IRandomAccessStreamReference icon) { Icon = icon; - _organizationsToProjects = new Dictionary>(); + _selectedServer = string.Empty; + _selectedOrganization = string.Empty; + _selectedProject = string.Empty; } public RepositoryProvider() @@ -82,7 +86,7 @@ public IAsyncOperation IsUriSupportedAsync(Uri uri, /// /// Gets all repos from the project that the user did a pull request into /// - private List GetRepos(VssConnection connection, TeamProjectReference project, GitPullRequestSearchCriteria criteria) + private List GetRepos(VssConnection connection, TeamProjectReference project, GitPullRequestSearchCriteria criteria, bool shouldCheckForPrs) { Log.Logger()?.ReportInfo("DevHomeRepository", $"Getting all repos for {project.Name}"); try @@ -95,9 +99,16 @@ private List GetRepos(VssConnection connection, TeamProjectReferenc { try { - var pullRequests = gitClient.GetPullRequestsAsync(repo.Id, criteria).Result; + if (shouldCheckForPrs) + { + var pullRequests = gitClient.GetPullRequestsAsync(repo.Id, criteria).Result; - if (pullRequests.Count != 0) + if (pullRequests.Count != 0) + { + reposToReturn.Add(new DevHomeRepository(repo)); + } + } + else { reposToReturn.Add(new DevHomeRepository(repo)); } @@ -118,50 +129,7 @@ private List GetRepos(VssConnection connection, TeamProjectReferenc public IAsyncOperation GetRepositoriesAsync(IDeveloperId developerId) { - // Get access token for ADO API calls. - if (developerId is not DeveloperId.DeveloperId azureDeveloperId) - { - return Task.Run(() => - { - var exception = new NotSupportedException("Authenticated user is not the an azure developer id."); - return new RepositoriesResult(exception, $"{exception.Message} HResult: {exception.HResult}"); - }).AsAsyncOperation(); - } - - _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); - - var reposToReturn = new List(); - - var options = new ParallelOptions() - { - // Considering these are API calls, having more degrees than Environment.CoreCount should be okay. - MaxDegreeOfParallelism = -1, - }; - - Parallel.ForEach(_azureHierarchy.GetOrganizations(), options, orgnization => - { - try - { - // Making a connection can throw. - var connection = AzureClientProvider.GetConnectionForLoggedInDeveloper(orgnization.AccountUri, azureDeveloperId); - var criteria = new GitPullRequestSearchCriteria(); - criteria.CreatorId = connection.AuthorizedIdentity.Id; - - Parallel.ForEach(_azureHierarchy.GetProjects(orgnization.AccountName), options, project => - { - reposToReturn.AddRange(GetRepos(connection, project, criteria)); - }); - } - catch (Exception e) - { - Providers.Log.Logger()?.ReportError("DevHomeRepository", e); - } - }); - - return Task.Run(() => - { - return new RepositoriesResult(reposToReturn); - }).AsAsyncOperation(); + return GetRepositoriesAsync(new Dictionary(), developerId); } public IAsyncOperation GetRepositoryFromUriAsync(Uri uri) @@ -334,72 +302,274 @@ public void Dispose() GC.SuppressFinalize(this); } - public IEnumerable GetRepositoriesAsync(IDeveloperId developerId, IDictionary searchTerms) + public IAsyncOperation GetRepositoriesAsync(IReadOnlyDictionary searchTerms, IDeveloperId developerId) { - if (searchTerms.ContainsKey(_orgnization)) + var serverToUse = searchTerms.ContainsKey(_server) ? searchTerms[_server] : string.Empty; + _selectedServer = serverToUse; + + var organizationToUse = searchTerms.ContainsKey(_organization) ? searchTerms[_organization] : string.Empty; + _selectedOrganization = organizationToUse; + + var projectToUse = searchTerms.ContainsKey(_project) ? searchTerms[_project] : string.Empty; + _selectedProject = projectToUse; + + // Get access token for ADO API calls. + if (developerId is not DeveloperId.DeveloperId azureDeveloperId) { - _organizationName = searchTerms[_organizationName]; + return Task.Run(() => + { + var exception = new NotSupportedException("Authenticated user is not the an azure developer id."); + return new RepositoriesResult(exception, $"{exception.Message} HResult: {exception.HResult}"); + }).AsAsyncOperation(); } - else + + _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); + + var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + if (!string.IsNullOrEmpty(_selectedOrganization)) { - _organizationName = string.Empty; +#pragma warning disable CA1309 // Use ordinal string comparison. Organization names should match exactly. + organizations = organizations.Where(x => x.AccountName.Equals(_selectedOrganization)).ToList(); +#pragma warning restore CA1309 // Use ordinal string comparison } - if (searchTerms.ContainsKey(_project)) + var options = new ParallelOptions() { - _projectName = searchTerms[_project]; - } - else + // Let the program decide + MaxDegreeOfParallelism = -1, + }; + + Dictionary> organizationsAndProjects = new Dictionary>(); + + Parallel.ForEach(organizations, options, orgnization => { - _projectName = string.Empty; - } + var projects = _azureHierarchy.GetProjectsAsync(orgnization).Result; + if (!string.IsNullOrEmpty(_selectedProject)) + { +#pragma warning disable CA1309 // Use ordinal string comparison. Organization names should match exactly. + projects = projects.Where(x => x.Name.Equals(_selectedProject)).ToList(); +#pragma warning restore CA1309 // Use ordinal string comparison + } - var foundRepositoriesAwaiter = GetRepositoriesAsync(developerId).GetAwaiter(); + if (projects.Count != 0) + { + organizationsAndProjects.Add(orgnization, projects); + } + }); - while (!foundRepositoriesAwaiter.IsCompleted) + var reposToReturn = new List(); + + // By now organizationAndProjects include all relevant information to get repos. + Parallel.ForEach(organizationsAndProjects, options, organizationAndProject => { - Thread.Sleep(1000); - } + try + { + // Making a connection can throw. + var connection = AzureClientProvider.GetConnectionForLoggedInDeveloper(organizationAndProject.Key.AccountUri, azureDeveloperId); + var criteria = new GitPullRequestSearchCriteria(); + criteria.CreatorId = connection.AuthorizedIdentity.Id; + + Parallel.ForEach(organizationAndProject.Value, options, project => + { + reposToReturn.AddRange(GetRepos(connection, project, criteria, true)); + }); + } + catch (Exception e) + { + Providers.Log.Logger()?.ReportError("DevHomeRepository", e); + } + }); - var foundRepositories = foundRepositoriesAwaiter.GetResult().Repositories; - return foundRepositories; + return Task.Run(() => + { + return new RepositoriesResult(reposToReturn); + }).AsAsyncOperation(); } public IReadOnlyList GetSearchFieldNames() { - return new List { "Organization", "Project" }; + return new List { _server, _organization, _project }; } - public IReadOnlyList GetValuesBasedOn(IDictionary inputValues, string fieldNameToRetrieve, IDeveloperId developerId) + public IAsyncOperation> GetValuesForField(string fieldName, IReadOnlyDictionary fieldValues, IDeveloperId developerId) { + // Get access token for ADO API calls. if (developerId is not DeveloperId.DeveloperId azureDeveloperId) { - throw new NotSupportedException("Authenticated user is not an azure developer id"); + return Task.Run(() => + { + return new List() as IList; + }).AsAsyncOperation(); } +#pragma warning disable IDE0059 // Unnecessary assignment of a value. Server isn't needed until users use on prem. + var serverToUse = fieldValues.ContainsKey(_server) ? fieldValues[_server] : string.Empty; +#pragma warning restore IDE0059 // Unnecessary assignment of a value + + var organizationToUse = fieldValues.ContainsKey(_organization) ? fieldValues[_organization] : string.Empty; + var projectToUse = fieldValues.ContainsKey(_project) ? fieldValues[_project] : string.Empty; + _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); - if (fieldNameToRetrieve.Equals(_orgnization, StringComparison.OrdinalIgnoreCase)) +#pragma warning disable CA1309 // Use ordinal string comparison. Make sure names match linguisticly. + if (fieldName.Equals(_organization)) { - var inputProject = string.Empty; - if (inputValues.ContainsKey(_project)) + var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + if (!string.IsNullOrEmpty(organizationToUse)) { - inputProject = inputValues[_project]; + organizations = organizations.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); } - return _azureHierarchy.GetOrganizations(inputProject).Select(x => x.AccountName).ToList(); + if (string.IsNullOrEmpty(projectToUse)) + { + return Task.Run(() => + { + return organizations.Select(x => x.AccountName).ToList() as IList; + }).AsAsyncOperation(); + } + else + { + var organizationsToReturn = new List(); + foreach (var organization in organizations) + { + var projects = _azureHierarchy.GetProjectsAsync(organization).Result.Where(x => x.Name.Equals(projectToUse)).ToList(); + if (projects.Count > 0) + { + organizationsToReturn.Add(organization.AccountName); + } + } + + return Task.Run(() => + { + return organizationsToReturn as IList; + }).AsAsyncOperation(); + } } - else if (fieldNameToRetrieve.Equals(_project, StringComparison.OrdinalIgnoreCase)) + else if (fieldName.Equals(_project)) { - var inputOrganization = string.Empty; - if (inputValues.ContainsKey(_orgnization)) + // Because projects exist in an organization searching behavior is different for each combonation. + var isOrganizationInInput = !string.IsNullOrEmpty(organizationToUse); + var isProjectInInput = !string.IsNullOrEmpty(projectToUse); + + if (isOrganizationInInput && !isProjectInInput) { - inputOrganization = inputValues[_orgnization]; + // get all projects in the organization. + var organizations = _azureHierarchy.GetOrganizationsAsync().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); + List projectNames = new List(); + foreach (var organization in organizations) + { + projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Select(x => x.Name)); + } + + return Task.Run(() => + { + return projectNames as IList; + }).AsAsyncOperation(); } - return _azureHierarchy.GetProjects(inputOrganization).Select(x => x.Name).ToList(); + if (isProjectInInput && !isOrganizationInInput) + { + // Get all projects with the same name in all organizations. + // This does mean the project drop down might have duplicate names. + var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + List projectNames = new List(); + foreach (var organization in organizations) + { + projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Where(x => x.Name.Equals(projectToUse)).Select(x => x.Name)); + } + + return Task.Run(() => + { + return projectNames as IList; + }).AsAsyncOperation(); + } + + if (isOrganizationInInput && isProjectInInput) + { + // Get the organization. If the organization does not exist, return nothing. + // If the organization has the project, return the project name. + // otherwise, return nothing. + var organizations = _azureHierarchy.GetOrganizationsAsync().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); + + List projectNames = new List(); + foreach (var organization in organizations) + { + projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Select(x => x.Name)); + } + + return Task.Run(() => + { + return projectNames as IList; + }).AsAsyncOperation(); + } + } + + return Task.Run(() => + { + return new List() as IList; + }).AsAsyncOperation(); +#pragma warning restore CA1309 // Use ordinal string comparison + } + + public string? GetFieldSearchValue(string field, IDeveloperId developerId) + { +#pragma warning disable CA1309 // Use ordinal string comparison. Make sure names match linguisticly. + if (field.Equals(_server)) + { + return _selectedServer; + } + else if (field.Equals(_organization)) + { + return _selectedOrganization; + } + else if (field.Equals(_project)) + { + return _selectedProject; + } + else + { + return null; + } +#pragma warning restore CA1309 // Use ordinal string comparison + } + + public string GetDefaultValueFor(string field, IDeveloperId developerId) + { + if (developerId is not DeveloperId.DeveloperId azureDeveloperId) + { + throw new NotSupportedException("Authenticated user is not an azure developer id"); + } + + _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); + + var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + + if (field.Equals(_server, StringComparison.OrdinalIgnoreCase)) + { + if (organizations.FirstOrDefault(x => x.AccountUri.OriginalString.Contains("dev.azure.com")) != null) + { + return "dev.azure.com"; + } + else + { + var organization = organizations[0]; + return $"{organization.AccountName}.visualstudio.com"; + } + } + + if (field.Equals(_organization, StringComparison.OrdinalIgnoreCase)) + { + var maybeNewOrganization = organizations.FirstOrDefault(x => x.AccountUri.OriginalString.Contains("dev.azure.com", StringComparison.OrdinalIgnoreCase)); + if (maybeNewOrganization != null) + { + return maybeNewOrganization.AccountName; + } + else + { + return organizations[0].AccountName; + } } - return new List(); + return string.Empty; } } diff --git a/src/AzureExtensionServer/AzureExtensionServer.csproj b/src/AzureExtensionServer/AzureExtensionServer.csproj index feba9327..0f1351b5 100644 --- a/src/AzureExtensionServer/AzureExtensionServer.csproj +++ b/src/AzureExtensionServer/AzureExtensionServer.csproj @@ -43,7 +43,7 @@ - + diff --git a/src/Logging/DevHome.Logging.csproj b/src/Logging/DevHome.Logging.csproj index 984b8fc7..b500c039 100644 --- a/src/Logging/DevHome.Logging.csproj +++ b/src/Logging/DevHome.Logging.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Telemetry/AzureExtension.Telemetry.csproj b/src/Telemetry/AzureExtension.Telemetry.csproj index 22c45d9c..2e4eb4bc 100644 --- a/src/Telemetry/AzureExtension.Telemetry.csproj +++ b/src/Telemetry/AzureExtension.Telemetry.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/AzureExtension/AzureExtension.Test.csproj b/test/AzureExtension/AzureExtension.Test.csproj index bc60a205..76a4566d 100644 --- a/test/AzureExtension/AzureExtension.Test.csproj +++ b/test/AzureExtension/AzureExtension.Test.csproj @@ -1,4 +1,4 @@ - + @@ -24,7 +24,7 @@ - + diff --git a/test/Console/AzureExtension.TestConsole.csproj b/test/Console/AzureExtension.TestConsole.csproj index 538b128b..73240af6 100644 --- a/test/Console/AzureExtension.TestConsole.csproj +++ b/test/Console/AzureExtension.TestConsole.csproj @@ -1,4 +1,4 @@ - + @@ -43,7 +43,7 @@ - + From 0cfbbcb0d389bba695fd79177023538031c84401 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Thu, 8 Feb 2024 15:10:42 -0800 Subject: [PATCH 03/15] Handling the new SDK + .NET 8.0 --- Directory.Build.props | 11 + ToolingVersions.props | 2 +- src/AzureExtension/AzureExtension.cs | 2 +- src/AzureExtension/AzureExtension.csproj | 2 +- src/AzureExtension/Constants.cs | 2 +- .../DataManager/AzureDataManager.cs | 4 +- .../DataModel/DataObjects/Identity.cs | 8 +- .../DeveloperId/AuthenticationHelper.cs | 4 +- .../Helpers/AzureRepositoryHierarchy.cs | 21 +- src/AzureExtension/Helpers/IconLoader.cs | 2 +- src/AzureExtension/Helpers/TimeSpanHelper.cs | 2 +- .../Notifications/NotificationHandler.cs | 4 +- .../Providers/DevHomeRepository.cs | 2 +- .../Providers/RepositoryProvider.cs | 361 +++++++++++------- .../Widgets/AzurePullRequestsWidget.cs | 6 +- .../Widgets/AzureQueryListWidget.cs | 6 +- .../Widgets/AzureQueryTilesWidget.cs | 18 +- src/AzureExtension/Widgets/AzureWidget.cs | 6 +- .../Widgets/WidgetImplFactory.cs | 2 +- src/AzureExtension/Widgets/WidgetProvider.cs | 30 +- src/AzureExtension/Widgets/WidgetServer.cs | 2 +- src/AzureExtensionServer/Program.cs | 5 +- src/Logging/helpers/DictionaryExtensions.cs | 5 +- src/Telemetry/Logger.cs | 2 +- .../AzureExtension/AzureExtension.Test.csproj | 4 +- .../Mocks/MockAuthenticationHelper.cs | 8 +- .../RepositoryProviderTests.cs | 6 +- 27 files changed, 308 insertions(+), 219 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 94c02178..4008a273 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -44,5 +44,16 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + true + + + + \ No newline at end of file diff --git a/ToolingVersions.props b/ToolingVersions.props index b6c8d340..f4a87568 100644 --- a/ToolingVersions.props +++ b/ToolingVersions.props @@ -2,7 +2,7 @@ - net6.0-windows10.0.22000.0 + net8.0-windows10.0.22000.0 10.0.19041.0 10.0.19041.0 diff --git a/src/AzureExtension/AzureExtension.cs b/src/AzureExtension/AzureExtension.cs index 464c7ca8..045a2e18 100644 --- a/src/AzureExtension/AzureExtension.cs +++ b/src/AzureExtension/AzureExtension.cs @@ -27,7 +27,7 @@ public AzureExtension(ManualResetEvent extensionDisposedEvent) case ProviderType.DeveloperId: return DeveloperIdProvider.GetInstance(); case ProviderType.Repository: - return new RepositoryProvider(); + return RepositoryProvider.GetInstance(); case ProviderType.FeaturedApplications: return new object(); default: diff --git a/src/AzureExtension/AzureExtension.csproj b/src/AzureExtension/AzureExtension.csproj index 1b81f8e9..42d58c85 100644 --- a/src/AzureExtension/AzureExtension.csproj +++ b/src/AzureExtension/AzureExtension.csproj @@ -58,7 +58,7 @@ - + diff --git a/src/AzureExtension/Constants.cs b/src/AzureExtension/Constants.cs index 3afa8dff..c381fddf 100644 --- a/src/AzureExtension/Constants.cs +++ b/src/AzureExtension/Constants.cs @@ -3,7 +3,7 @@ namespace DevHomeAzureExtension; -internal class Constants +internal sealed class Constants { #pragma warning disable SA1310 // Field names should not contain underscore public const string DEV_HOME_APPLICATION_NAME = "DevHome"; diff --git a/src/AzureExtension/DataManager/AzureDataManager.cs b/src/AzureExtension/DataManager/AzureDataManager.cs index b92c4021..cc1f07e4 100644 --- a/src/AzureExtension/DataManager/AzureDataManager.cs +++ b/src/AzureExtension/DataManager/AzureDataManager.cs @@ -102,7 +102,7 @@ public AzureDataManager(string identifier, DataStoreOptions dataStoreOptions) Log.Logger()?.ReportWarn(Name, InstanceName, "Failed setting DeveloperId change handler.", ex); } - if (Instances.ContainsKey(InstanceName)) + if (Instances.TryGetValue(InstanceName, out var value)) { // We should not have duplicate AzureDataManagers, as every client should have one, // but the identifiers may not be unique if using partial Guids. Note in the log @@ -819,7 +819,7 @@ protected virtual void Dispose(bool disposing) try { Log.Logger()?.ReportDebug(Name, InstanceName, "Disposing of all Disposable resources."); - if (Instances.ContainsKey(InstanceName) && Instances[InstanceName] == UniqueName) + if (Instances.TryGetValue(InstanceName, out var instanceName) && instanceName == UniqueName) { Instances.TryRemove(InstanceName, out _); Log.Logger()?.ReportInfo(Name, InstanceName, $"Removed AzureDataManager: {UniqueName}."); diff --git a/src/AzureExtension/DataModel/DataObjects/Identity.cs b/src/AzureExtension/DataModel/DataObjects/Identity.cs index d07f51d8..017bd867 100644 --- a/src/AzureExtension/DataModel/DataObjects/Identity.cs +++ b/src/AzureExtension/DataModel/DataObjects/Identity.cs @@ -145,12 +145,14 @@ public static Identity Get(DataStore? dataStore, long id) } public static Identity GetOrCreateIdentity(DataStore dataStore, IdentityRef? identityRef) - { + { +#pragma warning disable CA1510 // Use ArgumentNullException throw helper ThrowIfNull does not tell compiler that the value is not null if (identityRef == null) { throw new ArgumentNullException(nameof(identityRef)); - } - + } +#pragma warning restore CA1510 // Use ArgumentNullException throw helper + var newIdentity = CreateFromIdentityRef(identityRef); return AddOrUpdateIdentity(dataStore, newIdentity); } diff --git a/src/AzureExtension/DeveloperId/AuthenticationHelper.cs b/src/AzureExtension/DeveloperId/AuthenticationHelper.cs index 2a178e4d..3a67e5ac 100644 --- a/src/AzureExtension/DeveloperId/AuthenticationHelper.cs +++ b/src/AzureExtension/DeveloperId/AuthenticationHelper.cs @@ -35,6 +35,8 @@ public AuthenticationResult? AuthenticationResult public static Guid TransferTenetId { get; } = new("f8cdef31-a31e-4b4a-93e4-5f571e91255a"); + private static readonly string[] _capabilities = new string[1] { "cp1" }; + public AuthenticationHelper() { MicrosoftEntraIdSettings = new AuthenticationSettings(); @@ -53,7 +55,7 @@ public void InitializePublicClientApplicationBuilder() .WithAuthority(string.Format(CultureInfo.InvariantCulture, MicrosoftEntraIdSettings.Authority, MicrosoftEntraIdSettings.TenantId)) .WithRedirectUri(string.Format(CultureInfo.InvariantCulture, MicrosoftEntraIdSettings.RedirectURI, MicrosoftEntraIdSettings.ClientId)) .WithLogging(new MSALLogger(EventLogLevel.Warning), enablePiiLogging: false) - .WithClientCapabilities(new string[] { "cp1" }); + .WithClientCapabilities(_capabilities); Log.Logger()?.ReportInfo($"Created PublicClientApplicationBuilder"); } diff --git a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs index db91ff9b..6d617263 100644 --- a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs +++ b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. +using System.Collections.Concurrent; using DevHomeAzureExtension.Client; using DevHomeAzureExtension.DeveloperId; using Microsoft.TeamFoundation.Core.WebApi; @@ -17,11 +18,11 @@ public class AzureRepositoryHierarchy { private readonly DeveloperId _developerId; - private readonly Dictionary> _organizationsAndProjects; + private readonly ConcurrentDictionary> _organizationsAndProjects; private Task>? _queryOrganizationsTask; - private Dictionary>> _organizationsAndProjectTask; + private ConcurrentDictionary>> _organizationsAndProjectTask; /// /// Initializes a new instance of the class. @@ -36,8 +37,8 @@ public class AzureRepositoryHierarchy public AzureRepositoryHierarchy(DeveloperId developerId) { _developerId = developerId; - _organizationsAndProjects = new Dictionary>(); - _organizationsAndProjectTask = new Dictionary>>(); + _organizationsAndProjects = new ConcurrentDictionary>(); + _organizationsAndProjectTask = new ConcurrentDictionary>>(); } public async Task> GetOrganizationsAsync() @@ -53,7 +54,11 @@ public async Task> GetOrganizationsAsync() foreach (var organization in organizations) { // Extra protection against duplicates if two threads run QueryForOrganizations at the same time. - _organizationsAndProjects.TryAdd(organization, new List()); + var duplicateOrganization = _organizationsAndProjects.Keys.FirstOrDefault(x => x.AccountId == organization.AccountId); + if (duplicateOrganization == null) + { + _organizationsAndProjects.TryAdd(organization, new List()); + } } return _organizationsAndProjects.Keys.ToList(); @@ -68,12 +73,12 @@ public async Task> GetProjectsAsync(Organization orga } // if the task has been started - if (_organizationsAndProjectTask.ContainsKey(organization)) + if (_organizationsAndProjectTask.TryGetValue(organization, out var projectsTask)) { - if (_organizationsAndProjectTask[organization] != null) + if (projectsTask != null) { // Wait for it. - await _organizationsAndProjectTask[organization]; + await projectsTask; return _organizationsAndProjects[organization]; } } diff --git a/src/AzureExtension/Helpers/IconLoader.cs b/src/AzureExtension/Helpers/IconLoader.cs index 611620de..ab5d642b 100644 --- a/src/AzureExtension/Helpers/IconLoader.cs +++ b/src/AzureExtension/Helpers/IconLoader.cs @@ -12,7 +12,7 @@ public class IconLoader public static string GetIconAsBase64(string filename) { Log.Logger()?.ReportDebug(nameof(IconLoader), $"Asking for icon: {filename}"); - if (!Base64ImageRegistry.ContainsKey(filename)) + if (!Base64ImageRegistry.TryGetValue(filename, out var value)) { Base64ImageRegistry.Add(filename, ConvertIconToDataString(filename)); Log.Logger()?.ReportDebug(nameof(IconLoader), $"The icon {filename} was converted and is now stored."); diff --git a/src/AzureExtension/Helpers/TimeSpanHelper.cs b/src/AzureExtension/Helpers/TimeSpanHelper.cs index 1b5552a3..5e0d0a89 100644 --- a/src/AzureExtension/Helpers/TimeSpanHelper.cs +++ b/src/AzureExtension/Helpers/TimeSpanHelper.cs @@ -6,7 +6,7 @@ namespace DevHomeAzureExtension.Helpers; -internal class TimeSpanHelper +internal sealed class TimeSpanHelper { public static string TimeSpanToDisplayString(TimeSpan timeSpan, Logger? log = null) { diff --git a/src/AzureExtension/Notifications/NotificationHandler.cs b/src/AzureExtension/Notifications/NotificationHandler.cs index 842e5a64..0a86341a 100644 --- a/src/AzureExtension/Notifications/NotificationHandler.cs +++ b/src/AzureExtension/Notifications/NotificationHandler.cs @@ -18,15 +18,13 @@ public static void NotificationActivation(AppNotificationActivatedEventArgs args { Log.Logger()?.ReportInfo($"Notification Activated with args: {NotificationArgsToString(args)}"); - if (args.Arguments.ContainsKey("htmlurl")) + if (args.Arguments.TryGetValue("htmlurl", out var urlString)) { try { // Do not assume this string is a safe URL and blindly execute it; verify that it is // in fact a valid Azure URL. // TODO: Validate Azure URL - var urlString = args.Arguments["htmlurl"]; - Log.Logger()?.ReportInfo($"Launching Uri: {urlString}"); var processStartInfo = new ProcessStartInfo { diff --git a/src/AzureExtension/Providers/DevHomeRepository.cs b/src/AzureExtension/Providers/DevHomeRepository.cs index e5f14789..e6e73262 100644 --- a/src/AzureExtension/Providers/DevHomeRepository.cs +++ b/src/AzureExtension/Providers/DevHomeRepository.cs @@ -45,7 +45,7 @@ public DevHomeRepository(GitRepository gitRepository) } var repoInformation = new AzureUri(localUrl); - _owningAccountName = Path.Join(repoInformation.Organization, repoInformation.Project); + _owningAccountName = Path.Join(repoInformation.Connection.Host, repoInformation.Organization, repoInformation.Project); cloneUrl = localUrl; diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index 9b265582..db562c16 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -21,10 +21,16 @@ namespace DevHomeAzureExtension.Providers; -public class RepositoryProvider : IRepositoryProvider, IRepositoryProvider2, IRepositoryData +public class RepositoryProvider : IRepositoryProvider, IRepositoryProvider2 { + private static readonly object _constructorLock = new(); + + private static RepositoryProvider? _repositoryProvider; + private AzureRepositoryHierarchy? _azureHierarchy; + private const string _defaultSearchServerName = "dev.azure.com"; + private const string _server = "server"; private const string _organization = "organization"; @@ -37,6 +43,10 @@ public class RepositoryProvider : IRepositoryProvider, IRepositoryProvider2, IRe private string _selectedProject; + private bool _isFirstQuery; + + private CancellationTokenSource _tokenSource; + public string DisplayName => Resources.GetResource(@"RepositoryProviderDisplayName"); public IRandomAccessStreamReference Icon @@ -44,14 +54,18 @@ public IRandomAccessStreamReference Icon get; private set; } - string[] IRepositoryData.GetSearchFieldNames => new string[3] { _server, _organization, _project }; + string[] IRepositoryProvider2.GetSearchFieldNames => new string[3] { _server, _organization, _project }; + + private bool _shouldIgnorePRDate; public RepositoryProvider(IRandomAccessStreamReference icon) { Icon = icon; + _isFirstQuery = true; _selectedServer = string.Empty; _selectedOrganization = string.Empty; _selectedProject = string.Empty; + _tokenSource = new(); } public RepositoryProvider() @@ -59,6 +73,16 @@ public RepositoryProvider() { } + public static RepositoryProvider GetInstance() + { + lock (_constructorLock) + { + _repositoryProvider ??= new RepositoryProvider(); + } + + return _repositoryProvider; + } + public IAsyncOperation IsUriSupportedAsync(Uri uri) { return IsUriSupportedAsync(uri, null); @@ -86,7 +110,7 @@ public IAsyncOperation IsUriSupportedAsync(Uri uri, /// /// Gets all repos from the project that the user did a pull request into /// - private List GetRepos(VssConnection connection, TeamProjectReference project, GitPullRequestSearchCriteria criteria, bool shouldCheckForPrs) + private List GetRepos(VssConnection connection, TeamProjectReference project, GitPullRequestSearchCriteria criteria, bool shouldIgnorePRDate) { Log.Logger()?.ReportInfo("DevHomeRepository", $"Getting all repos for {project.Name}"); try @@ -99,7 +123,11 @@ private List GetRepos(VssConnection connection, TeamProjectReferenc { try { - if (shouldCheckForPrs) + if (shouldIgnorePRDate) + { + reposToReturn.Add(new DevHomeRepository(repo)); + } + else { var pullRequests = gitClient.GetPullRequestsAsync(repo.Id, criteria).Result; @@ -108,10 +136,6 @@ private List GetRepos(VssConnection connection, TeamProjectReferenc reposToReturn.Add(new DevHomeRepository(repo)); } } - else - { - reposToReturn.Add(new DevHomeRepository(repo)); - } } catch { @@ -129,7 +153,28 @@ private List GetRepos(VssConnection connection, TeamProjectReferenc public IAsyncOperation GetRepositoriesAsync(IDeveloperId developerId) { - return GetRepositoriesAsync(new Dictionary(), developerId); + if (developerId is not DeveloperId.DeveloperId azureDeveloperId) + { + return Task.Run(() => + { + var exception = new NotSupportedException("Authenticated user is not the an azure developer id."); + return new RepositoriesResult(exception, $"{exception.Message} HResult: {exception.HResult}"); + }).AsAsyncOperation(); + } + + _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); + + var server = string.Empty; + var organizations = _azureHierarchy.GetOrganizationsAsync().Result; +#pragma warning disable CA1309 // Use ordinal string comparison + var defaultOrg = organizations.FirstOrDefault(x => x.AccountName.Equals(_defaultSearchServerName)); +#pragma warning restore CA1309 // Use ordinal string comparison + if (defaultOrg != null) + { + server = _defaultSearchServerName; + } + + return GetRepositoriesAsync(new Dictionary { { _server, server } }, developerId); } public IAsyncOperation GetRepositoryFromUriAsync(Uri uri) @@ -304,86 +349,129 @@ public void Dispose() public IAsyncOperation GetRepositoriesAsync(IReadOnlyDictionary searchTerms, IDeveloperId developerId) { - var serverToUse = searchTerms.ContainsKey(_server) ? searchTerms[_server] : string.Empty; - _selectedServer = serverToUse; - - var organizationToUse = searchTerms.ContainsKey(_organization) ? searchTerms[_organization] : string.Empty; - _selectedOrganization = organizationToUse; - - var projectToUse = searchTerms.ContainsKey(_project) ? searchTerms[_project] : string.Empty; - _selectedProject = projectToUse; + if (!_isFirstQuery) + { + _tokenSource.Cancel(); + _tokenSource = new CancellationTokenSource(); + } + else + { + _isFirstQuery = false; + } - // Get access token for ADO API calls. - if (developerId is not DeveloperId.DeveloperId azureDeveloperId) + var shouldIgnorePRDateOld = _shouldIgnorePRDate; + _shouldIgnorePRDate = true; + try { - return Task.Run(() => + return Task.Run( + () => + { + try { - var exception = new NotSupportedException("Authenticated user is not the an azure developer id."); - return new RepositoriesResult(exception, $"{exception.Message} HResult: {exception.HResult}"); - }).AsAsyncOperation(); - } + var serverToUse = searchTerms.ContainsKey(_server) ? searchTerms[_server] : string.Empty; + _selectedServer = serverToUse; - _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); + var organizationToUse = searchTerms.ContainsKey(_organization) ? searchTerms[_organization] : string.Empty; + _selectedOrganization = organizationToUse; + + var projectToUse = searchTerms.ContainsKey(_project) ? searchTerms[_project] : string.Empty; + _selectedProject = projectToUse; + + // Get access token for ADO API calls. + if (developerId is not DeveloperId.DeveloperId azureDeveloperId) + { + var exception = new NotSupportedException("Authenticated user is not the an azure developer id."); + return new RepositoriesResult(exception, $"{exception.Message} HResult: {exception.HResult}"); + } + + _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); - var organizations = _azureHierarchy.GetOrganizationsAsync().Result; - if (!string.IsNullOrEmpty(_selectedOrganization)) - { #pragma warning disable CA1309 // Use ordinal string comparison. Organization names should match exactly. - organizations = organizations.Where(x => x.AccountName.Equals(_selectedOrganization)).ToList(); + var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + var orgsInModernUrlFormat = new List(); + if (_selectedServer.Equals(_defaultSearchServerName)) + { + // For a default search, look up all orgs with the modern URL format + orgsInModernUrlFormat = organizations.Where(x => x.AccountUri.Host.Equals(_defaultSearchServerName)).ToList(); + } + + // If the user is apart of orgs in the modern URL format, use these going forward. + if (orgsInModernUrlFormat.Count > 0) + { + organizations = orgsInModernUrlFormat; + } + + if (!string.IsNullOrEmpty(_selectedOrganization)) + { + organizations = organizations.Where(x => x.AccountName.Equals(_selectedOrganization)).ToList(); + } #pragma warning restore CA1309 // Use ordinal string comparison - } - var options = new ParallelOptions() - { - // Let the program decide - MaxDegreeOfParallelism = -1, - }; + var options = new ParallelOptions() + { + // Let the program decide + MaxDegreeOfParallelism = -1, + }; - Dictionary> organizationsAndProjects = new Dictionary>(); + Dictionary> organizationsAndProjects = new Dictionary>(); - Parallel.ForEach(organizations, options, orgnization => - { - var projects = _azureHierarchy.GetProjectsAsync(orgnization).Result; - if (!string.IsNullOrEmpty(_selectedProject)) - { + Parallel.ForEach(organizations, options, orgnization => + { + var projects = _azureHierarchy.GetProjectsAsync(orgnization).Result; + if (!string.IsNullOrEmpty(_selectedProject)) + { #pragma warning disable CA1309 // Use ordinal string comparison. Organization names should match exactly. - projects = projects.Where(x => x.Name.Equals(_selectedProject)).ToList(); + projects = projects.Where(x => x.Name.Equals(_selectedProject)).ToList(); #pragma warning restore CA1309 // Use ordinal string comparison - } - - if (projects.Count != 0) - { - organizationsAndProjects.Add(orgnization, projects); - } - }); + } - var reposToReturn = new List(); + if (projects.Count != 0) + { + organizationsAndProjects.Add(orgnization, projects); + } + }); - // By now organizationAndProjects include all relevant information to get repos. - Parallel.ForEach(organizationsAndProjects, options, organizationAndProject => - { - try - { - // Making a connection can throw. - var connection = AzureClientProvider.GetConnectionForLoggedInDeveloper(organizationAndProject.Key.AccountUri, azureDeveloperId); - var criteria = new GitPullRequestSearchCriteria(); - criteria.CreatorId = connection.AuthorizedIdentity.Id; + var reposToReturn = new List(); - Parallel.ForEach(organizationAndProject.Value, options, project => + // organizationAndProjects include all relevant information to get repos. + Parallel.ForEach(organizationsAndProjects, options, organizationAndProject => { - reposToReturn.AddRange(GetRepos(connection, project, criteria, true)); + try + { + // Making a connection can throw. + var connection = AzureClientProvider.GetConnectionForLoggedInDeveloper(organizationAndProject.Key.AccountUri, azureDeveloperId); + var criteria = new GitPullRequestSearchCriteria(); + criteria.CreatorId = connection.AuthorizedIdentity.Id; + + Parallel.ForEach(organizationAndProject.Value, options, project => + { + reposToReturn.AddRange(GetRepos(connection, project, criteria, shouldIgnorePRDateOld)); + }); + } + catch (Exception e) + { + Providers.Log.Logger()?.ReportError("DevHomeRepository", e); + } }); + + return new RepositoriesResult(reposToReturn); } - catch (Exception e) + catch (Exception ex) { - Providers.Log.Logger()?.ReportError("DevHomeRepository", e); + Providers.Log.Logger()?.ReportError("DevHomeRepository", ex); + return new RepositoriesResult(new List()); } - }); - - return Task.Run(() => + }, + _tokenSource.Token).AsAsyncOperation(); + } + catch (Exception ex) { - return new RepositoriesResult(reposToReturn); - }).AsAsyncOperation(); + return Task.Run(() => + { + Providers.Log.Logger()?.ReportError("DevHomeRepository", ex); + return new RepositoriesResult(new List()); + }).AsAsyncOperation(); + } } public IReadOnlyList GetSearchFieldNames() @@ -402,110 +490,95 @@ public IAsyncOperation> GetValuesForField(string fieldName, IReadO }).AsAsyncOperation(); } + return Task.Run(() => + { #pragma warning disable IDE0059 // Unnecessary assignment of a value. Server isn't needed until users use on prem. - var serverToUse = fieldValues.ContainsKey(_server) ? fieldValues[_server] : string.Empty; + var serverToUse = fieldValues.ContainsKey(_server) ? fieldValues[_server] : string.Empty; #pragma warning restore IDE0059 // Unnecessary assignment of a value - var organizationToUse = fieldValues.ContainsKey(_organization) ? fieldValues[_organization] : string.Empty; - var projectToUse = fieldValues.ContainsKey(_project) ? fieldValues[_project] : string.Empty; + var organizationToUse = fieldValues.ContainsKey(_organization) ? fieldValues[_organization] : string.Empty; + var projectToUse = fieldValues.ContainsKey(_project) ? fieldValues[_project] : string.Empty; - _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); + _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); #pragma warning disable CA1309 // Use ordinal string comparison. Make sure names match linguisticly. - if (fieldName.Equals(_organization)) - { - var organizations = _azureHierarchy.GetOrganizationsAsync().Result; - if (!string.IsNullOrEmpty(organizationToUse)) + if (fieldName.Equals(_organization)) { - organizations = organizations.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); - } + var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + if (!string.IsNullOrEmpty(organizationToUse)) + { + organizations = organizations.Where(x => x.AccountName.Equals(organizationToUse)).OrderBy(x => x.AccountName).ToList(); + } - if (string.IsNullOrEmpty(projectToUse)) - { - return Task.Run(() => + if (string.IsNullOrEmpty(projectToUse)) { - return organizations.Select(x => x.AccountName).ToList() as IList; - }).AsAsyncOperation(); - } - else - { - var organizationsToReturn = new List(); - foreach (var organization in organizations) + return organizations.Select(x => x.AccountName).Order().ToList() as IList; + } + else { - var projects = _azureHierarchy.GetProjectsAsync(organization).Result.Where(x => x.Name.Equals(projectToUse)).ToList(); - if (projects.Count > 0) + var organizationsToReturn = new List(); + foreach (var organization in organizations) { - organizationsToReturn.Add(organization.AccountName); + var projects = _azureHierarchy.GetProjectsAsync(organization).Result.Where(x => x.Name.Equals(projectToUse)).ToList(); + if (projects.Count > 0) + { + organizationsToReturn.Add(organization.AccountName); + } } - } - return Task.Run(() => - { - return organizationsToReturn as IList; - }).AsAsyncOperation(); + return organizationsToReturn.Order().ToList() as IList; + } } - } - else if (fieldName.Equals(_project)) - { - // Because projects exist in an organization searching behavior is different for each combonation. - var isOrganizationInInput = !string.IsNullOrEmpty(organizationToUse); - var isProjectInInput = !string.IsNullOrEmpty(projectToUse); - - if (isOrganizationInInput && !isProjectInInput) + else if (fieldName.Equals(_project)) { - // get all projects in the organization. - var organizations = _azureHierarchy.GetOrganizationsAsync().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); - List projectNames = new List(); - foreach (var organization in organizations) + // Because projects exist in an organization searching behavior is different for each combonation. + var isOrganizationInInput = !string.IsNullOrEmpty(organizationToUse); + var isProjectInInput = !string.IsNullOrEmpty(projectToUse); + + if (isOrganizationInInput && !isProjectInInput) { - projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Select(x => x.Name)); + // get all projects in the organization. + var organizations = _azureHierarchy.GetOrganizationsAsync().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); + List projectNames = new List(); + foreach (var organization in organizations) + { + projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Select(x => x.Name)); + } + + return projectNames.Order().ToList() as IList; } - return Task.Run(() => + if (isProjectInInput && !isOrganizationInInput) { - return projectNames as IList; - }).AsAsyncOperation(); - } + // Get all projects with the same name in all organizations. + // This does mean the project drop down might have duplicate names. + var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + List projectNames = new List(); + foreach (var organization in organizations) + { + projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Where(x => x.Name.Equals(projectToUse)).Select(x => x.Name)); + } - if (isProjectInInput && !isOrganizationInInput) - { - // Get all projects with the same name in all organizations. - // This does mean the project drop down might have duplicate names. - var organizations = _azureHierarchy.GetOrganizationsAsync().Result; - List projectNames = new List(); - foreach (var organization in organizations) - { - projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Where(x => x.Name.Equals(projectToUse)).Select(x => x.Name)); + return projectNames.Order().ToList() as IList; } - return Task.Run(() => + if (isOrganizationInInput && isProjectInInput) { - return projectNames as IList; - }).AsAsyncOperation(); - } + // Get the organization. If the organization does not exist, return nothing. + // If the organization has the project, return the project name. + // otherwise, return nothing. + var organizations = _azureHierarchy.GetOrganizationsAsync().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); - if (isOrganizationInInput && isProjectInInput) - { - // Get the organization. If the organization does not exist, return nothing. - // If the organization has the project, return the project name. - // otherwise, return nothing. - var organizations = _azureHierarchy.GetOrganizationsAsync().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); + List projectNames = new List(); + foreach (var organization in organizations) + { + projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Select(x => x.Name)); + } - List projectNames = new List(); - foreach (var organization in organizations) - { - projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Select(x => x.Name)); + return projectNames.Order().ToList() as IList; } - - return Task.Run(() => - { - return projectNames as IList; - }).AsAsyncOperation(); } - } - return Task.Run(() => - { return new List() as IList; }).AsAsyncOperation(); #pragma warning restore CA1309 // Use ordinal string comparison diff --git a/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs b/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs index fc546935..40b2afe4 100644 --- a/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs +++ b/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs @@ -12,11 +12,11 @@ namespace DevHomeAzureExtension.Widgets; -internal class AzurePullRequestsWidget : AzureWidget +internal sealed class AzurePullRequestsWidget : AzureWidget { private readonly string sampleIconData = IconLoader.GetIconAsBase64("screenshot.png"); - protected static readonly new string Name = nameof(AzurePullRequestsWidget); + private static readonly new string Name = nameof(AzurePullRequestsWidget); private static readonly string DefaultSelectedView = "Mine"; @@ -35,7 +35,7 @@ public AzurePullRequestsWidget() public override void CreateWidget(WidgetContext widgetContext, string state) { - if (state.Any()) + if (state.Length == 0) { ResetDataFromState(state); } diff --git a/src/AzureExtension/Widgets/AzureQueryListWidget.cs b/src/AzureExtension/Widgets/AzureQueryListWidget.cs index 379e073e..dc0cd717 100644 --- a/src/AzureExtension/Widgets/AzureQueryListWidget.cs +++ b/src/AzureExtension/Widgets/AzureQueryListWidget.cs @@ -11,11 +11,11 @@ namespace DevHomeAzureExtension.Widgets; -internal class AzureQueryListWidget : AzureWidget +internal sealed class AzureQueryListWidget : AzureWidget { private readonly string sampleIconData = IconLoader.GetIconAsBase64("screenshot.png"); - protected static readonly new string Name = nameof(AzureQueryListWidget); + private static readonly new string Name = nameof(AzureQueryListWidget); // Widget Data private string widgetTitle = string.Empty; @@ -31,7 +31,7 @@ public AzureQueryListWidget() public override void CreateWidget(WidgetContext widgetContext, string state) { - if (state.Any()) + if (state.Length != 0) { ResetDataFromState(state); } diff --git a/src/AzureExtension/Widgets/AzureQueryTilesWidget.cs b/src/AzureExtension/Widgets/AzureQueryTilesWidget.cs index 83adefef..75b69178 100644 --- a/src/AzureExtension/Widgets/AzureQueryTilesWidget.cs +++ b/src/AzureExtension/Widgets/AzureQueryTilesWidget.cs @@ -11,7 +11,7 @@ namespace DevHomeAzureExtension.Widgets; -internal class AzureQueryTilesWidget : AzureWidget +internal sealed class AzureQueryTilesWidget : AzureWidget { private readonly string sampleIconData = IconLoader.GetIconAsBase64("screenshot.png"); @@ -19,7 +19,7 @@ internal class AzureQueryTilesWidget : AzureWidget private readonly int maxNumColumns = 2; // Widget data - protected static readonly new string Name = nameof(AzureQueryTilesWidget); + private static readonly new string Name = nameof(AzureQueryTilesWidget); private readonly List tiles = new(); // Creation and destruction methods @@ -30,7 +30,7 @@ public AzureQueryTilesWidget() public override void CreateWidget(WidgetContext widgetContext, string state) { - if (state.Any()) + if (state.Length != 0) { // Not newly created widget, recovering tiles from state ResetNumberOfTilesFromData(state); @@ -54,7 +54,7 @@ public override void DeleteWidget(string widgetId, string customState) } // Action handler methods - protected void HandleAddTile(WidgetActionInvokedArgs args) + private void HandleAddTile(WidgetActionInvokedArgs args) { Page = WidgetPageState.Loading; UpdateWidget(); @@ -81,12 +81,12 @@ private string RemoveLastTileFromData(string data) return dataObject.ToJsonString(); } - protected void HandleRemoveTile(WidgetActionInvokedArgs args) + private void HandleRemoveTile(WidgetActionInvokedArgs args) { Page = WidgetPageState.Loading; UpdateWidget(); - if (tiles.Any()) + if (tiles.Count != 0) { tiles.RemoveAt(tiles.Count - 1); } @@ -152,7 +152,7 @@ protected override bool ValidateConfiguration(WidgetActionInvokedArgs actionInvo UpdateWidget(); UpdateAllTiles(actionInvokedArgs.Data); - bool hasValidData = ValidateConfigurationData(); + var hasValidData = ValidateConfigurationData(); if (hasValidData) { Page = WidgetPageState.Content; @@ -313,7 +313,7 @@ private bool ValidateConfigurationData() return false; } - if (!tiles.Any()) + if (tiles.Count == 0) { return false; } @@ -458,7 +458,7 @@ public override string GetConfiguration(string data) { { "url", tiles[i].AzureUri.ToString() }, { "title", tiles[i].Title }, - { "message", tiles[i].Message.Any() ? tiles[i].Message : null }, + { "message", tiles[i].Message.Length != 0 ? tiles[i].Message : null }, }); } diff --git a/src/AzureExtension/Widgets/AzureWidget.cs b/src/AzureExtension/Widgets/AzureWidget.cs index f52f47ae..03e51a7b 100644 --- a/src/AzureExtension/Widgets/AzureWidget.cs +++ b/src/AzureExtension/Widgets/AzureWidget.cs @@ -106,7 +106,7 @@ public override void CreateWidget(WidgetContext widgetContext, string state) // If there is a state, it is being retrieved from the widget service, so // this widget was pinned before. - if (state.Any()) + if (state.Length != 0) { Pinned = true; CanSave = true; @@ -324,10 +324,10 @@ public virtual string GetData(WidgetPageState page) protected string GetTemplateForPage(WidgetPageState page) { - if (Template.ContainsKey(page)) + if (Template.TryGetValue(page, out var azureTemplate)) { Log.Logger()?.ReportDebug(Name, ShortId, $"Using cached template for {page}"); - return Template[page]; + return azureTemplate; } try diff --git a/src/AzureExtension/Widgets/WidgetImplFactory.cs b/src/AzureExtension/Widgets/WidgetImplFactory.cs index c7a0ce9f..da7a4a5d 100644 --- a/src/AzureExtension/Widgets/WidgetImplFactory.cs +++ b/src/AzureExtension/Widgets/WidgetImplFactory.cs @@ -6,7 +6,7 @@ namespace DevHomeAzureExtension.Widgets; [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Templated class")] -internal class WidgetImplFactory : IWidgetImplFactory +internal sealed class WidgetImplFactory : IWidgetImplFactory where T : WidgetImpl, new() { public WidgetImpl Create(WidgetContext widgetContext, string state) diff --git a/src/AzureExtension/Widgets/WidgetProvider.cs b/src/AzureExtension/Widgets/WidgetProvider.cs index 32cfe150..3f1d6ae8 100644 --- a/src/AzureExtension/Widgets/WidgetProvider.cs +++ b/src/AzureExtension/Widgets/WidgetProvider.cs @@ -33,7 +33,7 @@ private void InitializeWidget(WidgetContext widgetContext, string state) var widgetId = widgetContext.Id; var widgetDefinitionId = widgetContext.DefinitionId; Log.Logger()?.ReportDebug($"Calling Initialize for Widget Id: {widgetId} - {widgetDefinitionId}"); - if (widgetDefinitionRegistry.ContainsKey(widgetDefinitionId)) + if (widgetDefinitionRegistry.TryGetValue(widgetDefinitionId, out var _)) { if (!runningWidgets.ContainsKey(widgetId)) { @@ -92,37 +92,37 @@ public void Activate(WidgetContext widgetContext) { Log.Logger()?.ReportDebug($"Activate id: {widgetContext.Id} definitionId: {widgetContext.DefinitionId}"); var widgetId = widgetContext.Id; - if (runningWidgets.ContainsKey(widgetId)) + if (runningWidgets.TryGetValue(widgetId, out var widgetImpl)) { - runningWidgets[widgetId].Activate(widgetContext); + widgetImpl.Activate(widgetContext); } else { // Called to activate a widget that we don't know about, which is unexpected. Try to recover by creating it. Log.Logger()?.ReportWarn($"Found WidgetId that was not known: {widgetContext.Id}, attempting to recover by creating it."); CreateWidget(widgetContext); - if (runningWidgets.ContainsKey(widgetId)) + if (runningWidgets.TryGetValue(widgetId, out var widgetImplForUnknownWidget)) { - runningWidgets[widgetId].Activate(widgetContext); + widgetImplForUnknownWidget.Activate(widgetContext); } } } public void Deactivate(string widgetId) { - if (runningWidgets.ContainsKey(widgetId)) + if (runningWidgets.TryGetValue(widgetId, out var widgetToDeactivate)) { Log.Logger()?.ReportDebug($"Deactivate id: {widgetId}"); - runningWidgets[widgetId].Deactivate(widgetId); + widgetToDeactivate.Deactivate(widgetId); } } public void DeleteWidget(string widgetId, string customState) { - if (runningWidgets.ContainsKey(widgetId)) + if (runningWidgets.TryGetValue(widgetId, out var widgetToDelete)) { Log.Logger()?.ReportInfo($"DeleteWidget id: {widgetId}"); - runningWidgets[widgetId].DeleteWidget(widgetId, customState); + widgetToDelete.DeleteWidget(widgetId, customState); runningWidgets.Remove(widgetId); } } @@ -132,9 +132,9 @@ public void OnActionInvoked(WidgetActionInvokedArgs actionInvokedArgs) Log.Logger()?.ReportDebug($"OnActionInvoked id: {actionInvokedArgs.WidgetContext.Id} definitionId: {actionInvokedArgs.WidgetContext.DefinitionId}"); var widgetContext = actionInvokedArgs.WidgetContext; var widgetId = widgetContext.Id; - if (runningWidgets.ContainsKey(widgetId)) + if (runningWidgets.TryGetValue(widgetId, out var widgetToInvokeAnActionOn)) { - runningWidgets[widgetId].OnActionInvoked(actionInvokedArgs); + widgetToInvokeAnActionOn.OnActionInvoked(actionInvokedArgs); } } @@ -143,9 +143,9 @@ public void OnCustomizationRequested(WidgetCustomizationRequestedArgs customizat Log.Logger()?.ReportDebug($"OnCustomizationRequested id: {customizationRequestedArgs.WidgetContext.Id} definitionId: {customizationRequestedArgs.WidgetContext.DefinitionId}"); var widgetContext = customizationRequestedArgs.WidgetContext; var widgetId = widgetContext.Id; - if (runningWidgets.ContainsKey(widgetId)) + if (runningWidgets.TryGetValue(widgetId, out var widgetToCustomize)) { - runningWidgets[widgetId].OnCustomizationRequested(customizationRequestedArgs); + widgetToCustomize.OnCustomizationRequested(customizationRequestedArgs); } } @@ -154,9 +154,9 @@ public void OnWidgetContextChanged(WidgetContextChangedArgs contextChangedArgs) Log.Logger()?.ReportDebug($"OnWidgetContextChanged id: {contextChangedArgs.WidgetContext.Id} definitionId: {contextChangedArgs.WidgetContext.DefinitionId}"); var widgetContext = contextChangedArgs.WidgetContext; var widgetId = widgetContext.Id; - if (runningWidgets.ContainsKey(widgetId)) + if (runningWidgets.TryGetValue(widgetId, out var widgetWithAChangedContext)) { - runningWidgets[widgetId].OnWidgetContextChanged(contextChangedArgs); + widgetWithAChangedContext.OnWidgetContextChanged(contextChangedArgs); } } } diff --git a/src/AzureExtension/Widgets/WidgetServer.cs b/src/AzureExtension/Widgets/WidgetServer.cs index d09b6a27..ccc48cc5 100644 --- a/src/AzureExtension/Widgets/WidgetServer.cs +++ b/src/AzureExtension/Widgets/WidgetServer.cs @@ -62,7 +62,7 @@ public void Dispose() } } - private class Ole32 + private sealed class Ole32 { #pragma warning disable SA1310 // Field names should not contain underscore // https://docs.microsoft.com/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx diff --git a/src/AzureExtensionServer/Program.cs b/src/AzureExtensionServer/Program.cs index 4304d83e..12663f3a 100644 --- a/src/AzureExtensionServer/Program.cs +++ b/src/AzureExtensionServer/Program.cs @@ -172,10 +172,9 @@ private static void RecreateDataStoreIfNecessary() try { var localSettings = ApplicationData.Current.LocalSettings; - if (localSettings.Values.ContainsKey(AzureDataManager.RecreateDataStoreSettingsKey)) + if (localSettings.Values.TryGetValue(AzureDataManager.RecreateDataStoreSettingsKey, out var recreateDataStore)) { - var recreateDataStore = (bool)localSettings.Values[AzureDataManager.RecreateDataStoreSettingsKey]; - if (recreateDataStore) + if ((bool)recreateDataStore) { Log.Logger()?.ReportInfo("Recreating DataStore"); diff --git a/src/Logging/helpers/DictionaryExtensions.cs b/src/Logging/helpers/DictionaryExtensions.cs index 0de1afba..c36d1c98 100644 --- a/src/Logging/helpers/DictionaryExtensions.cs +++ b/src/Logging/helpers/DictionaryExtensions.cs @@ -7,10 +7,7 @@ public static class DictionaryExtensions { public static void DisposeAll(this IDictionary dictionary) { - if (dictionary is null) - { - throw new ArgumentNullException(nameof(dictionary)); - } + ArgumentException.ThrowIfNullOrEmpty(nameof(dictionary)); foreach (var kv in dictionary) { diff --git a/src/Telemetry/Logger.cs b/src/Telemetry/Logger.cs index ce4f0eb8..7f529289 100644 --- a/src/Telemetry/Logger.cs +++ b/src/Telemetry/Logger.cs @@ -12,7 +12,7 @@ namespace DevHomeAzureExtension.Telemetry; -internal class Logger : ILogger +internal sealed class Logger : ILogger { private const string ProviderName = "Microsoft.AzureExtension"; diff --git a/test/AzureExtension/AzureExtension.Test.csproj b/test/AzureExtension/AzureExtension.Test.csproj index 4fe50f8e..c0ef1f4d 100644 --- a/test/AzureExtension/AzureExtension.Test.csproj +++ b/test/AzureExtension/AzureExtension.Test.csproj @@ -5,8 +5,8 @@ enable enable AzureExtension.Test - win10-x86;win10-x64;win10-arm64 - Properties\PublishProfiles\win10-$(Platform).pubxml + win-x86;win-x64;win-arm64 + Properties\PublishProfiles\win-$(Platform).pubxml true false enable diff --git a/test/AzureExtension/DeveloperId/Mocks/MockAuthenticationHelper.cs b/test/AzureExtension/DeveloperId/Mocks/MockAuthenticationHelper.cs index 48563684..d2622bca 100644 --- a/test/AzureExtension/DeveloperId/Mocks/MockAuthenticationHelper.cs +++ b/test/AzureExtension/DeveloperId/Mocks/MockAuthenticationHelper.cs @@ -12,15 +12,17 @@ namespace DevHomeAzureExtension.Test.DeveloperId.Mocks; -internal class MockAuthenticationHelper : IAuthenticationHelper +internal sealed class MockAuthenticationHelper : IAuthenticationHelper { private readonly List loginIds = new(); + + private static readonly string[] _scopes = new[] { "scope1", "scope2" }; public AuthenticationSettings MicrosoftEntraIdSettings { get => throw new NotImplementedException(); set => throw new NotImplementedException(); - } + } public Task> AcquireAllDeveloperAccountTokens(object scopesArray) => throw new NotImplementedException(); @@ -67,7 +69,7 @@ public void InitializePublicClientApplicationBuilder() tenantId: string.Empty, account: null, idToken: "id token", - scopes: new[] { "scope1", "scope2" }, + scopes: _scopes, correlationId: Guid.Empty, authenticationResultMetadata: null, tokenType: string.Empty); diff --git a/test/AzureExtension/RepositoryProvider/RepositoryProviderTests.cs b/test/AzureExtension/RepositoryProvider/RepositoryProviderTests.cs index 028d3730..4f6d53ca 100644 --- a/test/AzureExtension/RepositoryProvider/RepositoryProviderTests.cs +++ b/test/AzureExtension/RepositoryProvider/RepositoryProviderTests.cs @@ -20,7 +20,7 @@ public partial class RepositoryProviderTests private List> GetValidUrls() { - if (_validUrls.Any()) + if (_validUrls.Count != 0) { return _validUrls; } @@ -58,7 +58,7 @@ private List> GetValidUrls() private List GetInvalidUrls() { - if (_invalidUrls.Any()) + if (_invalidUrls.Count != 0) { return _invalidUrls; } @@ -72,7 +72,7 @@ private List GetInvalidUrls() private List GetValidNotRepoUrls() { - if (_validNotRepoUrls.Any()) + if (_validNotRepoUrls.Count != 0) { return _validNotRepoUrls; } From 621a6a677203279161a5dcc7cc625e476616b267 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Fri, 9 Feb 2024 16:31:36 -0800 Subject: [PATCH 04/15] Adding comments --- .../Helpers/AzureRepositoryHierarchy.cs | 42 ++++++- .../Providers/RepositoryProvider.cs | 108 +++++++++++------- 2 files changed, 107 insertions(+), 43 deletions(-) diff --git a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs index 6d617263..5ddb094c 100644 --- a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs +++ b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs @@ -14,15 +14,24 @@ namespace AzureExtension.Helpers; +/// +/// Handles the hierarchy between organizations and projects. Handles querying for both as well. +/// public class AzureRepositoryHierarchy { - private readonly DeveloperId _developerId; - private readonly ConcurrentDictionary> _organizationsAndProjects; - private Task>? _queryOrganizationsTask; + /// + /// Used to keep track of multiple Get requests to make sure work isn't duplicated. + /// + private readonly ConcurrentDictionary>> _organizationsAndProjectTask; - private ConcurrentDictionary>> _organizationsAndProjectTask; + private readonly DeveloperId _developerId; + + /// + /// Used to keep track of all GetOrganization requests to make sure work isn't duplicated. + /// + private Task>? _queryOrganizationsTask; /// /// Initializes a new instance of the class. @@ -41,6 +50,10 @@ public AzureRepositoryHierarchy(DeveloperId developerId) _organizationsAndProjectTask = new ConcurrentDictionary>>(); } + /// + /// Get all organizations the is apart of. + /// + /// A list of all organizations. public async Task> GetOrganizationsAsync() { // if something is running the query, wait for it to finish. @@ -64,6 +77,11 @@ public async Task> GetOrganizationsAsync() return _organizationsAndProjects.Keys.ToList(); } + /// + /// Get all projects the user is apart of. + /// + /// Filters down the returned list to those under organization + /// A list of projects. public async Task> GetProjectsAsync(Organization organization) { // if something is running the query, wait for it to finish. @@ -89,6 +107,14 @@ public async Task> GetProjectsAsync(Organization orga return _organizationsAndProjects[organization]; } + /// + /// Contacts the server to get all organizations the user is apart of. + /// + /// A list of organizations. + /// + /// The returned list does include disabled organizations. + /// _queryOrganizationsTask is set here. + /// private List QueryForOrganizations() { // Establish a connection to get all organizations. @@ -118,6 +144,14 @@ private List QueryForOrganizations() } } + /// + /// Contacts the server to get all projects in an organization. + /// + /// PRojects are returned only for this organization. + /// A list of projects. + /// + /// the Task to get the projects is added to _organizationsAndProjectTask. + /// private List QueryForProjects(Organization organization) { try diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index db562c16..672eaeda 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -23,6 +23,9 @@ namespace DevHomeAzureExtension.Providers; public class RepositoryProvider : IRepositoryProvider, IRepositoryProvider2 { + /// + /// Used for singleton instance. + /// private static readonly object _constructorLock = new(); private static RepositoryProvider? _repositoryProvider; @@ -43,10 +46,6 @@ public class RepositoryProvider : IRepositoryProvider, IRepositoryProvider2 private string _selectedProject; - private bool _isFirstQuery; - - private CancellationTokenSource _tokenSource; - public string DisplayName => Resources.GetResource(@"RepositoryProviderDisplayName"); public IRandomAccessStreamReference Icon @@ -56,16 +55,17 @@ public IRandomAccessStreamReference Icon string[] IRepositoryProvider2.GetSearchFieldNames => new string[3] { _server, _organization, _project }; + /// + /// On first query, get repos based on the PR date. All other requests won't check for PRs. + /// private bool _shouldIgnorePRDate; public RepositoryProvider(IRandomAccessStreamReference icon) { Icon = icon; - _isFirstQuery = true; _selectedServer = string.Empty; _selectedOrganization = string.Empty; _selectedProject = string.Empty; - _tokenSource = new(); } public RepositoryProvider() @@ -108,15 +108,20 @@ public IAsyncOperation IsUriSupportedAsync(Uri uri, } /// - /// Gets all repos from the project that the user did a pull request into + /// Get all repositories under a porject. /// - private List GetRepos(VssConnection connection, TeamProjectReference project, GitPullRequestSearchCriteria criteria, bool shouldIgnorePRDate) + /// The connection to the organization. + /// The project to get the repos for + /// Holds the user ID. + /// If criteria should be used or not. + /// A list of IRepository's. + private List GetRepos(VssConnection organizationConnection, TeamProjectReference project, GitPullRequestSearchCriteria criteria, bool shouldIgnorePRDate) { Log.Logger()?.ReportInfo("DevHomeRepository", $"Getting all repos for {project.Name}"); try { - var gitClient = connection.GetClient(); - var repos = gitClient.GetRepositoriesAsync(project.Id, true, true).Result.OrderBy(x => x.Name); + var gitClient = organizationConnection.GetClient(); + var repos = gitClient.GetRepositoriesAsync(project.Id, false, false).Result.OrderBy(x => x.Name); var reposToReturn = new List(); foreach (var repo in repos) @@ -151,6 +156,11 @@ private List GetRepos(VssConnection connection, TeamProjectReferenc } } + /// + /// Get repositories for the developer. + /// + /// The account to use to get repositories. + /// A list of repositories the user has access to. public IAsyncOperation GetRepositoriesAsync(IDeveloperId developerId) { if (developerId is not DeveloperId.DeveloperId azureDeveloperId) @@ -167,6 +177,7 @@ public IAsyncOperation GetRepositoriesAsync(IDeveloperId dev var server = string.Empty; var organizations = _azureHierarchy.GetOrganizationsAsync().Result; #pragma warning disable CA1309 // Use ordinal string comparison + // Try to find any organizations with the modern URL format. var defaultOrg = organizations.FirstOrDefault(x => x.AccountName.Equals(_defaultSearchServerName)); #pragma warning restore CA1309 // Use ordinal string comparison if (defaultOrg != null) @@ -349,25 +360,14 @@ public void Dispose() public IAsyncOperation GetRepositoriesAsync(IReadOnlyDictionary searchTerms, IDeveloperId developerId) { - if (!_isFirstQuery) - { - _tokenSource.Cancel(); - _tokenSource = new CancellationTokenSource(); - } - else - { - _isFirstQuery = false; - } - + // First query will find repos only if the user made a PR. var shouldIgnorePRDateOld = _shouldIgnorePRDate; _shouldIgnorePRDate = true; - try - { - return Task.Run( - () => + return Task.Run(() => { try { + // Store the search terms. var serverToUse = searchTerms.ContainsKey(_server) ? searchTerms[_server] : string.Empty; _selectedServer = serverToUse; @@ -415,6 +415,7 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction Dictionary> organizationsAndProjects = new Dictionary>(); + // Establish a connection between organizations and their projects. Parallel.ForEach(organizations, options, orgnization => { var projects = _azureHierarchy.GetProjectsAsync(orgnization).Result; @@ -461,24 +462,25 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction Providers.Log.Logger()?.ReportError("DevHomeRepository", ex); return new RepositoriesResult(new List()); } - }, - _tokenSource.Token).AsAsyncOperation(); - } - catch (Exception ex) - { - return Task.Run(() => - { - Providers.Log.Logger()?.ReportError("DevHomeRepository", ex); - return new RepositoriesResult(new List()); - }).AsAsyncOperation(); - } + }).AsAsyncOperation(); } + /// + /// Returns a list of field names this will accept as search input. + /// + /// A readonly list of search field names. public IReadOnlyList GetSearchFieldNames() { return new List { _server, _organization, _project }; } + /// + /// Gets a list of values that work with the given input. + /// + /// Results will be for this search field. + /// The inputs used to filter the results. + /// Used to access private org and project information. + /// A list of all values for fieldName that makes snes given the input. public IAsyncOperation> GetValuesForField(string fieldName, IReadOnlyDictionary fieldValues, IDeveloperId developerId) { // Get access token for ADO API calls. @@ -504,18 +506,21 @@ public IAsyncOperation> GetValuesForField(string fieldName, IReadO #pragma warning disable CA1309 // Use ordinal string comparison. Make sure names match linguisticly. if (fieldName.Equals(_organization)) { + // Requesting organizations and an organization is given. var organizations = _azureHierarchy.GetOrganizationsAsync().Result; if (!string.IsNullOrEmpty(organizationToUse)) { organizations = organizations.Where(x => x.AccountName.Equals(organizationToUse)).OrderBy(x => x.AccountName).ToList(); } + // If no projects are in the input, return the organizations. if (string.IsNullOrEmpty(projectToUse)) { return organizations.Select(x => x.AccountName).Order().ToList() as IList; } else { + // Find all organizations with the given project name. var organizationsToReturn = new List(); foreach (var organization in organizations) { @@ -606,17 +611,27 @@ public IAsyncOperation> GetValuesForField(string fieldName, IReadO #pragma warning restore CA1309 // Use ordinal string comparison } + /// + /// Used to return terms used for a default search. + /// In this case server is dev.azure.com if any organizations use the modern URL format. Otherwise it is the old URL format. + /// Organization is the first one found. + /// + /// The search field to search for. + /// Used to get server and organization information. + /// A default search value. public string GetDefaultValueFor(string field, IDeveloperId developerId) { if (developerId is not DeveloperId.DeveloperId azureDeveloperId) { - throw new NotSupportedException("Authenticated user is not an azure developer id"); + Providers.Log.Logger()?.ReportError("DevHomeRepository", "Authenticated user is not an azure developer id"); + return string.Empty; } _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + // Get a default server name. if (field.Equals(_server, StringComparison.OrdinalIgnoreCase)) { if (organizations.FirstOrDefault(x => x.AccountUri.OriginalString.Contains("dev.azure.com")) != null) @@ -625,11 +640,19 @@ public string GetDefaultValueFor(string field, IDeveloperId developerId) } else { - var organization = organizations[0]; - return $"{organization.AccountName}.visualstudio.com"; + if (organizations.Count != 0) + { + var organization = organizations[0]; + return $"{organization.AccountName}.visualstudio.com"; + } + else + { + return string.Empty; + } } } + // get the default organization if (field.Equals(_organization, StringComparison.OrdinalIgnoreCase)) { var maybeNewOrganization = organizations.FirstOrDefault(x => x.AccountUri.OriginalString.Contains("dev.azure.com", StringComparison.OrdinalIgnoreCase)); @@ -639,7 +662,14 @@ public string GetDefaultValueFor(string field, IDeveloperId developerId) } else { - return organizations[0].AccountName; + if (organizations.Count != 0) + { + return organizations[0].AccountName; + } + else + { + return string.Empty; + } } } From 6c84dddcd465a2d5dd761a0b79d0ee14694ac198 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Fri, 9 Feb 2024 16:36:37 -0800 Subject: [PATCH 05/15] Fixing some errors --- nuget.config | 1 - src/AzureExtension/Widgets/AzurePullRequestsWidget.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/nuget.config b/nuget.config index a98931e2..6285fe02 100644 --- a/nuget.config +++ b/nuget.config @@ -8,7 +8,6 @@ - diff --git a/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs b/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs index 40b2afe4..3ecd2b3f 100644 --- a/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs +++ b/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs @@ -35,7 +35,7 @@ public AzurePullRequestsWidget() public override void CreateWidget(WidgetContext widgetContext, string state) { - if (state.Length == 0) + if (state.Length != 0) { ResetDataFromState(state); } From 871273b282e1321fb3aac8cf07fd2a6d677f132b Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Thu, 15 Feb 2024 11:02:46 -0800 Subject: [PATCH 06/15] Using updated SDK --- .../Providers/RepositoryProvider.cs | 111 ++++++++---------- 1 file changed, 47 insertions(+), 64 deletions(-) diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index 672eaeda..fa23eecb 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Drawing; using System.Security.Authentication; using AzureExtension.Helpers; using DevHomeAzureExtension.Client; @@ -40,12 +39,6 @@ public class RepositoryProvider : IRepositoryProvider, IRepositoryProvider2 private const string _project = "project"; - private string _selectedServer; - - private string _selectedOrganization; - - private string _selectedProject; - public string DisplayName => Resources.GetResource(@"RepositoryProviderDisplayName"); public IRandomAccessStreamReference Icon @@ -53,7 +46,7 @@ public IRandomAccessStreamReference Icon get; private set; } - string[] IRepositoryProvider2.GetSearchFieldNames => new string[3] { _server, _organization, _project }; + public string[] SearchFieldNames => new string[3] { _server, _organization, _project }; /// /// On first query, get repos based on the PR date. All other requests won't check for PRs. @@ -63,9 +56,6 @@ public IRandomAccessStreamReference Icon public RepositoryProvider(IRandomAccessStreamReference icon) { Icon = icon; - _selectedServer = string.Empty; - _selectedOrganization = string.Empty; - _selectedProject = string.Empty; } public RepositoryProvider() @@ -358,7 +348,8 @@ public void Dispose() GC.SuppressFinalize(this); } - public IAsyncOperation GetRepositoriesAsync(IReadOnlyDictionary searchTerms, IDeveloperId developerId) +#pragma warning disable CA1309 // Use ordinal string comparison. I want to use linguistic comparison. + public IAsyncOperation GetRepositoriesAsync(IReadOnlyDictionary fieldValues, IDeveloperId developerId) { // First query will find repos only if the user made a PR. var shouldIgnorePRDateOld = _shouldIgnorePRDate; @@ -367,15 +358,9 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction { try { - // Store the search terms. - var serverToUse = searchTerms.ContainsKey(_server) ? searchTerms[_server] : string.Empty; - _selectedServer = serverToUse; - - var organizationToUse = searchTerms.ContainsKey(_organization) ? searchTerms[_organization] : string.Empty; - _selectedOrganization = organizationToUse; - - var projectToUse = searchTerms.ContainsKey(_project) ? searchTerms[_project] : string.Empty; - _selectedProject = projectToUse; + var serverToUse = fieldValues.ContainsKey(_server) ? fieldValues[_server] : string.Empty; + var organizationToUse = fieldValues.ContainsKey(_organization) ? fieldValues[_organization] : string.Empty; + var projectToUse = fieldValues.ContainsKey(_project) ? fieldValues[_project] : string.Empty; // Get access token for ADO API calls. if (developerId is not DeveloperId.DeveloperId azureDeveloperId) @@ -386,10 +371,9 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); -#pragma warning disable CA1309 // Use ordinal string comparison. Organization names should match exactly. var organizations = _azureHierarchy.GetOrganizationsAsync().Result; var orgsInModernUrlFormat = new List(); - if (_selectedServer.Equals(_defaultSearchServerName)) + if (serverToUse.Equals(_defaultSearchServerName)) { // For a default search, look up all orgs with the modern URL format orgsInModernUrlFormat = organizations.Where(x => x.AccountUri.Host.Equals(_defaultSearchServerName)).ToList(); @@ -401,11 +385,10 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction organizations = orgsInModernUrlFormat; } - if (!string.IsNullOrEmpty(_selectedOrganization)) + if (!string.IsNullOrEmpty(organizationToUse)) { - organizations = organizations.Where(x => x.AccountName.Equals(_selectedOrganization)).ToList(); + organizations = organizations.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); } -#pragma warning restore CA1309 // Use ordinal string comparison var options = new ParallelOptions() { @@ -419,11 +402,9 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction Parallel.ForEach(organizations, options, orgnization => { var projects = _azureHierarchy.GetProjectsAsync(orgnization).Result; - if (!string.IsNullOrEmpty(_selectedProject)) + if (!string.IsNullOrEmpty(projectToUse)) { -#pragma warning disable CA1309 // Use ordinal string comparison. Organization names should match exactly. - projects = projects.Where(x => x.Name.Equals(_selectedProject)).ToList(); -#pragma warning restore CA1309 // Use ordinal string comparison + projects = projects.Where(x => x.Name.Equals(projectToUse)).ToList(); } if (projects.Count != 0) @@ -464,6 +445,7 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction } }).AsAsyncOperation(); } +#pragma warning restore CA1309 // Use ordinal string comparison /// /// Returns a list of field names this will accept as search input. @@ -474,21 +456,22 @@ public IReadOnlyList GetSearchFieldNames() return new List { _server, _organization, _project }; } +#pragma warning disable CA1309 // Use ordinal string comparison. Make sure names match linguisticly. /// /// Gets a list of values that work with the given input. /// - /// Results will be for this search field. + /// Results will be for this search field. /// The inputs used to filter the results. /// Used to access private org and project information. /// A list of all values for fieldName that makes snes given the input. - public IAsyncOperation> GetValuesForField(string fieldName, IReadOnlyDictionary fieldValues, IDeveloperId developerId) + public IAsyncOperation> GetValuesForSearchFieldAsync(string requestedSearchField, IReadOnlyDictionary fieldValues, IDeveloperId developerId) { // Get access token for ADO API calls. if (developerId is not DeveloperId.DeveloperId azureDeveloperId) { return Task.Run(() => { - return new List() as IList; + return new List() as IReadOnlyList; }).AsAsyncOperation(); } @@ -503,8 +486,30 @@ public IAsyncOperation> GetValuesForField(string fieldName, IReadO _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); -#pragma warning disable CA1309 // Use ordinal string comparison. Make sure names match linguisticly. - if (fieldName.Equals(_organization)) + if (requestedSearchField.Equals(_server)) + { + var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + if (!string.IsNullOrEmpty(organizationToUse)) + { + organizations = organizations.Where(x => x.AccountName.Equals(organizationToUse)).OrderBy(x => x.AccountName).ToList(); + } + + HashSet servers = new(); + foreach (var organization in organizations) + { + if (organization.AccountUri.OriginalString.Contains("dev.azure.com")) + { + servers.Add("dev.azure.com"); + } + else + { + servers.Add($"{organization.AccountName}.visualstudio.com"); + } + } + + return servers.Order().ToList() as IReadOnlyList; + } + else if (requestedSearchField.Equals(_organization)) { // Requesting organizations and an organization is given. var organizations = _azureHierarchy.GetOrganizationsAsync().Result; @@ -516,7 +521,7 @@ public IAsyncOperation> GetValuesForField(string fieldName, IReadO // If no projects are in the input, return the organizations. if (string.IsNullOrEmpty(projectToUse)) { - return organizations.Select(x => x.AccountName).Order().ToList() as IList; + return organizations.Select(x => x.AccountName).Order().ToList() as IReadOnlyList; } else { @@ -531,10 +536,10 @@ public IAsyncOperation> GetValuesForField(string fieldName, IReadO } } - return organizationsToReturn.Order().ToList() as IList; + return organizationsToReturn.Order().ToList() as IReadOnlyList; } } - else if (fieldName.Equals(_project)) + else if (requestedSearchField.Equals(_project)) { // Because projects exist in an organization searching behavior is different for each combonation. var isOrganizationInInput = !string.IsNullOrEmpty(organizationToUse); @@ -550,7 +555,7 @@ public IAsyncOperation> GetValuesForField(string fieldName, IReadO projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Select(x => x.Name)); } - return projectNames.Order().ToList() as IList; + return projectNames.Order().ToList() as IReadOnlyList; } if (isProjectInInput && !isOrganizationInInput) @@ -564,7 +569,7 @@ public IAsyncOperation> GetValuesForField(string fieldName, IReadO projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Where(x => x.Name.Equals(projectToUse)).Select(x => x.Name)); } - return projectNames.Order().ToList() as IList; + return projectNames.Order().ToList() as IReadOnlyList; } if (isOrganizationInInput && isProjectInInput) @@ -580,36 +585,14 @@ public IAsyncOperation> GetValuesForField(string fieldName, IReadO projectNames.AddRange(_azureHierarchy.GetProjectsAsync(organization).Result.Select(x => x.Name)); } - return projectNames.Order().ToList() as IList; + return projectNames.Order().ToList() as IReadOnlyList; } } - return new List() as IList; + return new List() as IReadOnlyList; }).AsAsyncOperation(); -#pragma warning restore CA1309 // Use ordinal string comparison } - - public string? GetFieldSearchValue(string field, IDeveloperId developerId) - { -#pragma warning disable CA1309 // Use ordinal string comparison. Make sure names match linguisticly. - if (field.Equals(_server)) - { - return _selectedServer; - } - else if (field.Equals(_organization)) - { - return _selectedOrganization; - } - else if (field.Equals(_project)) - { - return _selectedProject; - } - else - { - return null; - } #pragma warning restore CA1309 // Use ordinal string comparison - } /// /// Used to return terms used for a default search. From 6e38f335e7268ed0dc8aed86abe7efc35b9818a6 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Thu, 15 Feb 2024 11:41:54 -0800 Subject: [PATCH 07/15] Update src/AzureExtension/Providers/RepositoryProvider.cs Co-authored-by: Branden Bonaby <105318831+bbonaby@users.noreply.github.com> --- src/AzureExtension/Providers/RepositoryProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index fa23eecb..92d874b6 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -98,7 +98,7 @@ public IAsyncOperation IsUriSupportedAsync(Uri uri, } /// - /// Get all repositories under a porject. + /// Get all repositories under a project. /// /// The connection to the organization. /// The project to get the repos for From 30f49429c5b63fa8aa5380c04e77303467624980 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Thu, 15 Feb 2024 11:44:44 -0800 Subject: [PATCH 08/15] Update src/AzureExtension/Providers/RepositoryProvider.cs Co-authored-by: Branden Bonaby <105318831+bbonaby@users.noreply.github.com> --- src/AzureExtension/Providers/RepositoryProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index 92d874b6..40cb3e14 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -365,7 +365,7 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction // Get access token for ADO API calls. if (developerId is not DeveloperId.DeveloperId azureDeveloperId) { - var exception = new NotSupportedException("Authenticated user is not the an azure developer id."); + var exception = new NotSupportedException("The DeveloperId is not valid."); return new RepositoriesResult(exception, $"{exception.Message} HResult: {exception.HResult}"); } From f4386e8f97802b872a606a0c651bbf173099614a Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Thu, 15 Feb 2024 11:47:50 -0800 Subject: [PATCH 09/15] Reverting .NET 8 changes --- Directory.Build.props | 11 ------- ToolingVersions.props | 2 +- src/AzureExtension/Constants.cs | 2 +- .../DataManager/AzureDataManager.cs | 4 +-- .../DataModel/DataObjects/Identity.cs | 8 ++--- .../DeveloperId/AuthenticationHelper.cs | 4 +-- .../Helpers/AzureRepositoryHierarchy.cs | 8 ++--- src/AzureExtension/Helpers/IconLoader.cs | 2 +- src/AzureExtension/Helpers/TimeSpanHelper.cs | 2 +- .../Notifications/NotificationHandler.cs | 4 ++- .../Providers/DevHomeRepository.cs | 2 +- .../Widgets/AzurePullRequestsWidget.cs | 6 ++-- .../Widgets/AzureQueryListWidget.cs | 6 ++-- .../Widgets/AzureQueryTilesWidget.cs | 18 +++++------ src/AzureExtension/Widgets/AzureWidget.cs | 6 ++-- .../Widgets/WidgetImplFactory.cs | 2 +- src/AzureExtension/Widgets/WidgetProvider.cs | 30 +++++++++---------- src/AzureExtension/Widgets/WidgetServer.cs | 2 +- src/AzureExtensionServer/Program.cs | 5 ++-- src/Logging/helpers/DictionaryExtensions.cs | 5 +++- src/Telemetry/Logger.cs | 2 +- .../AzureExtension/AzureExtension.Test.csproj | 6 ++-- .../Mocks/MockAuthenticationHelper.cs | 8 ++--- .../RepositoryProviderTests.cs | 6 ++-- .../Console/AzureExtension.TestConsole.csproj | 2 +- 25 files changed, 70 insertions(+), 83 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4008a273..94c02178 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -44,16 +44,5 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - true - - - - \ No newline at end of file diff --git a/ToolingVersions.props b/ToolingVersions.props index f4a87568..b6c8d340 100644 --- a/ToolingVersions.props +++ b/ToolingVersions.props @@ -2,7 +2,7 @@ - net8.0-windows10.0.22000.0 + net6.0-windows10.0.22000.0 10.0.19041.0 10.0.19041.0 diff --git a/src/AzureExtension/Constants.cs b/src/AzureExtension/Constants.cs index c381fddf..3afa8dff 100644 --- a/src/AzureExtension/Constants.cs +++ b/src/AzureExtension/Constants.cs @@ -3,7 +3,7 @@ namespace DevHomeAzureExtension; -internal sealed class Constants +internal class Constants { #pragma warning disable SA1310 // Field names should not contain underscore public const string DEV_HOME_APPLICATION_NAME = "DevHome"; diff --git a/src/AzureExtension/DataManager/AzureDataManager.cs b/src/AzureExtension/DataManager/AzureDataManager.cs index cc1f07e4..b92c4021 100644 --- a/src/AzureExtension/DataManager/AzureDataManager.cs +++ b/src/AzureExtension/DataManager/AzureDataManager.cs @@ -102,7 +102,7 @@ public AzureDataManager(string identifier, DataStoreOptions dataStoreOptions) Log.Logger()?.ReportWarn(Name, InstanceName, "Failed setting DeveloperId change handler.", ex); } - if (Instances.TryGetValue(InstanceName, out var value)) + if (Instances.ContainsKey(InstanceName)) { // We should not have duplicate AzureDataManagers, as every client should have one, // but the identifiers may not be unique if using partial Guids. Note in the log @@ -819,7 +819,7 @@ protected virtual void Dispose(bool disposing) try { Log.Logger()?.ReportDebug(Name, InstanceName, "Disposing of all Disposable resources."); - if (Instances.TryGetValue(InstanceName, out var instanceName) && instanceName == UniqueName) + if (Instances.ContainsKey(InstanceName) && Instances[InstanceName] == UniqueName) { Instances.TryRemove(InstanceName, out _); Log.Logger()?.ReportInfo(Name, InstanceName, $"Removed AzureDataManager: {UniqueName}."); diff --git a/src/AzureExtension/DataModel/DataObjects/Identity.cs b/src/AzureExtension/DataModel/DataObjects/Identity.cs index 017bd867..d07f51d8 100644 --- a/src/AzureExtension/DataModel/DataObjects/Identity.cs +++ b/src/AzureExtension/DataModel/DataObjects/Identity.cs @@ -145,14 +145,12 @@ public static Identity Get(DataStore? dataStore, long id) } public static Identity GetOrCreateIdentity(DataStore dataStore, IdentityRef? identityRef) - { -#pragma warning disable CA1510 // Use ArgumentNullException throw helper ThrowIfNull does not tell compiler that the value is not null + { if (identityRef == null) { throw new ArgumentNullException(nameof(identityRef)); - } -#pragma warning restore CA1510 // Use ArgumentNullException throw helper - + } + var newIdentity = CreateFromIdentityRef(identityRef); return AddOrUpdateIdentity(dataStore, newIdentity); } diff --git a/src/AzureExtension/DeveloperId/AuthenticationHelper.cs b/src/AzureExtension/DeveloperId/AuthenticationHelper.cs index 3a67e5ac..2a178e4d 100644 --- a/src/AzureExtension/DeveloperId/AuthenticationHelper.cs +++ b/src/AzureExtension/DeveloperId/AuthenticationHelper.cs @@ -35,8 +35,6 @@ public AuthenticationResult? AuthenticationResult public static Guid TransferTenetId { get; } = new("f8cdef31-a31e-4b4a-93e4-5f571e91255a"); - private static readonly string[] _capabilities = new string[1] { "cp1" }; - public AuthenticationHelper() { MicrosoftEntraIdSettings = new AuthenticationSettings(); @@ -55,7 +53,7 @@ public void InitializePublicClientApplicationBuilder() .WithAuthority(string.Format(CultureInfo.InvariantCulture, MicrosoftEntraIdSettings.Authority, MicrosoftEntraIdSettings.TenantId)) .WithRedirectUri(string.Format(CultureInfo.InvariantCulture, MicrosoftEntraIdSettings.RedirectURI, MicrosoftEntraIdSettings.ClientId)) .WithLogging(new MSALLogger(EventLogLevel.Warning), enablePiiLogging: false) - .WithClientCapabilities(_capabilities); + .WithClientCapabilities(new string[] { "cp1" }); Log.Logger()?.ReportInfo($"Created PublicClientApplicationBuilder"); } diff --git a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs index 5ddb094c..618f0370 100644 --- a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs +++ b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs @@ -19,12 +19,12 @@ namespace AzureExtension.Helpers; /// public class AzureRepositoryHierarchy { - private readonly ConcurrentDictionary> _organizationsAndProjects; + private readonly ConcurrentDictionary> _organizationsAndProjects = new(); /// /// Used to keep track of multiple Get requests to make sure work isn't duplicated. /// - private readonly ConcurrentDictionary>> _organizationsAndProjectTask; + private readonly ConcurrentDictionary>> _organizationsAndProjectTask = new(); private readonly DeveloperId _developerId; @@ -46,12 +46,10 @@ public class AzureRepositoryHierarchy public AzureRepositoryHierarchy(DeveloperId developerId) { _developerId = developerId; - _organizationsAndProjects = new ConcurrentDictionary>(); - _organizationsAndProjectTask = new ConcurrentDictionary>>(); } /// - /// Get all organizations the is apart of. + /// Get all organizations the user is apart of. /// /// A list of all organizations. public async Task> GetOrganizationsAsync() diff --git a/src/AzureExtension/Helpers/IconLoader.cs b/src/AzureExtension/Helpers/IconLoader.cs index ab5d642b..611620de 100644 --- a/src/AzureExtension/Helpers/IconLoader.cs +++ b/src/AzureExtension/Helpers/IconLoader.cs @@ -12,7 +12,7 @@ public class IconLoader public static string GetIconAsBase64(string filename) { Log.Logger()?.ReportDebug(nameof(IconLoader), $"Asking for icon: {filename}"); - if (!Base64ImageRegistry.TryGetValue(filename, out var value)) + if (!Base64ImageRegistry.ContainsKey(filename)) { Base64ImageRegistry.Add(filename, ConvertIconToDataString(filename)); Log.Logger()?.ReportDebug(nameof(IconLoader), $"The icon {filename} was converted and is now stored."); diff --git a/src/AzureExtension/Helpers/TimeSpanHelper.cs b/src/AzureExtension/Helpers/TimeSpanHelper.cs index 5e0d0a89..1b5552a3 100644 --- a/src/AzureExtension/Helpers/TimeSpanHelper.cs +++ b/src/AzureExtension/Helpers/TimeSpanHelper.cs @@ -6,7 +6,7 @@ namespace DevHomeAzureExtension.Helpers; -internal sealed class TimeSpanHelper +internal class TimeSpanHelper { public static string TimeSpanToDisplayString(TimeSpan timeSpan, Logger? log = null) { diff --git a/src/AzureExtension/Notifications/NotificationHandler.cs b/src/AzureExtension/Notifications/NotificationHandler.cs index 0a86341a..842e5a64 100644 --- a/src/AzureExtension/Notifications/NotificationHandler.cs +++ b/src/AzureExtension/Notifications/NotificationHandler.cs @@ -18,13 +18,15 @@ public static void NotificationActivation(AppNotificationActivatedEventArgs args { Log.Logger()?.ReportInfo($"Notification Activated with args: {NotificationArgsToString(args)}"); - if (args.Arguments.TryGetValue("htmlurl", out var urlString)) + if (args.Arguments.ContainsKey("htmlurl")) { try { // Do not assume this string is a safe URL and blindly execute it; verify that it is // in fact a valid Azure URL. // TODO: Validate Azure URL + var urlString = args.Arguments["htmlurl"]; + Log.Logger()?.ReportInfo($"Launching Uri: {urlString}"); var processStartInfo = new ProcessStartInfo { diff --git a/src/AzureExtension/Providers/DevHomeRepository.cs b/src/AzureExtension/Providers/DevHomeRepository.cs index e6e73262..dfcdc31d 100644 --- a/src/AzureExtension/Providers/DevHomeRepository.cs +++ b/src/AzureExtension/Providers/DevHomeRepository.cs @@ -45,7 +45,7 @@ public DevHomeRepository(GitRepository gitRepository) } var repoInformation = new AzureUri(localUrl); - _owningAccountName = Path.Join(repoInformation.Connection.Host, repoInformation.Organization, repoInformation.Project); + _owningAccountName = Path.Join(repoInformation.Organization, repoInformation.Repository); cloneUrl = localUrl; diff --git a/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs b/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs index 3ecd2b3f..fc546935 100644 --- a/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs +++ b/src/AzureExtension/Widgets/AzurePullRequestsWidget.cs @@ -12,11 +12,11 @@ namespace DevHomeAzureExtension.Widgets; -internal sealed class AzurePullRequestsWidget : AzureWidget +internal class AzurePullRequestsWidget : AzureWidget { private readonly string sampleIconData = IconLoader.GetIconAsBase64("screenshot.png"); - private static readonly new string Name = nameof(AzurePullRequestsWidget); + protected static readonly new string Name = nameof(AzurePullRequestsWidget); private static readonly string DefaultSelectedView = "Mine"; @@ -35,7 +35,7 @@ public AzurePullRequestsWidget() public override void CreateWidget(WidgetContext widgetContext, string state) { - if (state.Length != 0) + if (state.Any()) { ResetDataFromState(state); } diff --git a/src/AzureExtension/Widgets/AzureQueryListWidget.cs b/src/AzureExtension/Widgets/AzureQueryListWidget.cs index dc0cd717..379e073e 100644 --- a/src/AzureExtension/Widgets/AzureQueryListWidget.cs +++ b/src/AzureExtension/Widgets/AzureQueryListWidget.cs @@ -11,11 +11,11 @@ namespace DevHomeAzureExtension.Widgets; -internal sealed class AzureQueryListWidget : AzureWidget +internal class AzureQueryListWidget : AzureWidget { private readonly string sampleIconData = IconLoader.GetIconAsBase64("screenshot.png"); - private static readonly new string Name = nameof(AzureQueryListWidget); + protected static readonly new string Name = nameof(AzureQueryListWidget); // Widget Data private string widgetTitle = string.Empty; @@ -31,7 +31,7 @@ public AzureQueryListWidget() public override void CreateWidget(WidgetContext widgetContext, string state) { - if (state.Length != 0) + if (state.Any()) { ResetDataFromState(state); } diff --git a/src/AzureExtension/Widgets/AzureQueryTilesWidget.cs b/src/AzureExtension/Widgets/AzureQueryTilesWidget.cs index 75b69178..83adefef 100644 --- a/src/AzureExtension/Widgets/AzureQueryTilesWidget.cs +++ b/src/AzureExtension/Widgets/AzureQueryTilesWidget.cs @@ -11,7 +11,7 @@ namespace DevHomeAzureExtension.Widgets; -internal sealed class AzureQueryTilesWidget : AzureWidget +internal class AzureQueryTilesWidget : AzureWidget { private readonly string sampleIconData = IconLoader.GetIconAsBase64("screenshot.png"); @@ -19,7 +19,7 @@ internal sealed class AzureQueryTilesWidget : AzureWidget private readonly int maxNumColumns = 2; // Widget data - private static readonly new string Name = nameof(AzureQueryTilesWidget); + protected static readonly new string Name = nameof(AzureQueryTilesWidget); private readonly List tiles = new(); // Creation and destruction methods @@ -30,7 +30,7 @@ public AzureQueryTilesWidget() public override void CreateWidget(WidgetContext widgetContext, string state) { - if (state.Length != 0) + if (state.Any()) { // Not newly created widget, recovering tiles from state ResetNumberOfTilesFromData(state); @@ -54,7 +54,7 @@ public override void DeleteWidget(string widgetId, string customState) } // Action handler methods - private void HandleAddTile(WidgetActionInvokedArgs args) + protected void HandleAddTile(WidgetActionInvokedArgs args) { Page = WidgetPageState.Loading; UpdateWidget(); @@ -81,12 +81,12 @@ private string RemoveLastTileFromData(string data) return dataObject.ToJsonString(); } - private void HandleRemoveTile(WidgetActionInvokedArgs args) + protected void HandleRemoveTile(WidgetActionInvokedArgs args) { Page = WidgetPageState.Loading; UpdateWidget(); - if (tiles.Count != 0) + if (tiles.Any()) { tiles.RemoveAt(tiles.Count - 1); } @@ -152,7 +152,7 @@ protected override bool ValidateConfiguration(WidgetActionInvokedArgs actionInvo UpdateWidget(); UpdateAllTiles(actionInvokedArgs.Data); - var hasValidData = ValidateConfigurationData(); + bool hasValidData = ValidateConfigurationData(); if (hasValidData) { Page = WidgetPageState.Content; @@ -313,7 +313,7 @@ private bool ValidateConfigurationData() return false; } - if (tiles.Count == 0) + if (!tiles.Any()) { return false; } @@ -458,7 +458,7 @@ public override string GetConfiguration(string data) { { "url", tiles[i].AzureUri.ToString() }, { "title", tiles[i].Title }, - { "message", tiles[i].Message.Length != 0 ? tiles[i].Message : null }, + { "message", tiles[i].Message.Any() ? tiles[i].Message : null }, }); } diff --git a/src/AzureExtension/Widgets/AzureWidget.cs b/src/AzureExtension/Widgets/AzureWidget.cs index 03e51a7b..f52f47ae 100644 --- a/src/AzureExtension/Widgets/AzureWidget.cs +++ b/src/AzureExtension/Widgets/AzureWidget.cs @@ -106,7 +106,7 @@ public override void CreateWidget(WidgetContext widgetContext, string state) // If there is a state, it is being retrieved from the widget service, so // this widget was pinned before. - if (state.Length != 0) + if (state.Any()) { Pinned = true; CanSave = true; @@ -324,10 +324,10 @@ public virtual string GetData(WidgetPageState page) protected string GetTemplateForPage(WidgetPageState page) { - if (Template.TryGetValue(page, out var azureTemplate)) + if (Template.ContainsKey(page)) { Log.Logger()?.ReportDebug(Name, ShortId, $"Using cached template for {page}"); - return azureTemplate; + return Template[page]; } try diff --git a/src/AzureExtension/Widgets/WidgetImplFactory.cs b/src/AzureExtension/Widgets/WidgetImplFactory.cs index da7a4a5d..c7a0ce9f 100644 --- a/src/AzureExtension/Widgets/WidgetImplFactory.cs +++ b/src/AzureExtension/Widgets/WidgetImplFactory.cs @@ -6,7 +6,7 @@ namespace DevHomeAzureExtension.Widgets; [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Templated class")] -internal sealed class WidgetImplFactory : IWidgetImplFactory +internal class WidgetImplFactory : IWidgetImplFactory where T : WidgetImpl, new() { public WidgetImpl Create(WidgetContext widgetContext, string state) diff --git a/src/AzureExtension/Widgets/WidgetProvider.cs b/src/AzureExtension/Widgets/WidgetProvider.cs index 3f1d6ae8..32cfe150 100644 --- a/src/AzureExtension/Widgets/WidgetProvider.cs +++ b/src/AzureExtension/Widgets/WidgetProvider.cs @@ -33,7 +33,7 @@ private void InitializeWidget(WidgetContext widgetContext, string state) var widgetId = widgetContext.Id; var widgetDefinitionId = widgetContext.DefinitionId; Log.Logger()?.ReportDebug($"Calling Initialize for Widget Id: {widgetId} - {widgetDefinitionId}"); - if (widgetDefinitionRegistry.TryGetValue(widgetDefinitionId, out var _)) + if (widgetDefinitionRegistry.ContainsKey(widgetDefinitionId)) { if (!runningWidgets.ContainsKey(widgetId)) { @@ -92,37 +92,37 @@ public void Activate(WidgetContext widgetContext) { Log.Logger()?.ReportDebug($"Activate id: {widgetContext.Id} definitionId: {widgetContext.DefinitionId}"); var widgetId = widgetContext.Id; - if (runningWidgets.TryGetValue(widgetId, out var widgetImpl)) + if (runningWidgets.ContainsKey(widgetId)) { - widgetImpl.Activate(widgetContext); + runningWidgets[widgetId].Activate(widgetContext); } else { // Called to activate a widget that we don't know about, which is unexpected. Try to recover by creating it. Log.Logger()?.ReportWarn($"Found WidgetId that was not known: {widgetContext.Id}, attempting to recover by creating it."); CreateWidget(widgetContext); - if (runningWidgets.TryGetValue(widgetId, out var widgetImplForUnknownWidget)) + if (runningWidgets.ContainsKey(widgetId)) { - widgetImplForUnknownWidget.Activate(widgetContext); + runningWidgets[widgetId].Activate(widgetContext); } } } public void Deactivate(string widgetId) { - if (runningWidgets.TryGetValue(widgetId, out var widgetToDeactivate)) + if (runningWidgets.ContainsKey(widgetId)) { Log.Logger()?.ReportDebug($"Deactivate id: {widgetId}"); - widgetToDeactivate.Deactivate(widgetId); + runningWidgets[widgetId].Deactivate(widgetId); } } public void DeleteWidget(string widgetId, string customState) { - if (runningWidgets.TryGetValue(widgetId, out var widgetToDelete)) + if (runningWidgets.ContainsKey(widgetId)) { Log.Logger()?.ReportInfo($"DeleteWidget id: {widgetId}"); - widgetToDelete.DeleteWidget(widgetId, customState); + runningWidgets[widgetId].DeleteWidget(widgetId, customState); runningWidgets.Remove(widgetId); } } @@ -132,9 +132,9 @@ public void OnActionInvoked(WidgetActionInvokedArgs actionInvokedArgs) Log.Logger()?.ReportDebug($"OnActionInvoked id: {actionInvokedArgs.WidgetContext.Id} definitionId: {actionInvokedArgs.WidgetContext.DefinitionId}"); var widgetContext = actionInvokedArgs.WidgetContext; var widgetId = widgetContext.Id; - if (runningWidgets.TryGetValue(widgetId, out var widgetToInvokeAnActionOn)) + if (runningWidgets.ContainsKey(widgetId)) { - widgetToInvokeAnActionOn.OnActionInvoked(actionInvokedArgs); + runningWidgets[widgetId].OnActionInvoked(actionInvokedArgs); } } @@ -143,9 +143,9 @@ public void OnCustomizationRequested(WidgetCustomizationRequestedArgs customizat Log.Logger()?.ReportDebug($"OnCustomizationRequested id: {customizationRequestedArgs.WidgetContext.Id} definitionId: {customizationRequestedArgs.WidgetContext.DefinitionId}"); var widgetContext = customizationRequestedArgs.WidgetContext; var widgetId = widgetContext.Id; - if (runningWidgets.TryGetValue(widgetId, out var widgetToCustomize)) + if (runningWidgets.ContainsKey(widgetId)) { - widgetToCustomize.OnCustomizationRequested(customizationRequestedArgs); + runningWidgets[widgetId].OnCustomizationRequested(customizationRequestedArgs); } } @@ -154,9 +154,9 @@ public void OnWidgetContextChanged(WidgetContextChangedArgs contextChangedArgs) Log.Logger()?.ReportDebug($"OnWidgetContextChanged id: {contextChangedArgs.WidgetContext.Id} definitionId: {contextChangedArgs.WidgetContext.DefinitionId}"); var widgetContext = contextChangedArgs.WidgetContext; var widgetId = widgetContext.Id; - if (runningWidgets.TryGetValue(widgetId, out var widgetWithAChangedContext)) + if (runningWidgets.ContainsKey(widgetId)) { - widgetWithAChangedContext.OnWidgetContextChanged(contextChangedArgs); + runningWidgets[widgetId].OnWidgetContextChanged(contextChangedArgs); } } } diff --git a/src/AzureExtension/Widgets/WidgetServer.cs b/src/AzureExtension/Widgets/WidgetServer.cs index ccc48cc5..d09b6a27 100644 --- a/src/AzureExtension/Widgets/WidgetServer.cs +++ b/src/AzureExtension/Widgets/WidgetServer.cs @@ -62,7 +62,7 @@ public void Dispose() } } - private sealed class Ole32 + private class Ole32 { #pragma warning disable SA1310 // Field names should not contain underscore // https://docs.microsoft.com/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx diff --git a/src/AzureExtensionServer/Program.cs b/src/AzureExtensionServer/Program.cs index 12663f3a..4304d83e 100644 --- a/src/AzureExtensionServer/Program.cs +++ b/src/AzureExtensionServer/Program.cs @@ -172,9 +172,10 @@ private static void RecreateDataStoreIfNecessary() try { var localSettings = ApplicationData.Current.LocalSettings; - if (localSettings.Values.TryGetValue(AzureDataManager.RecreateDataStoreSettingsKey, out var recreateDataStore)) + if (localSettings.Values.ContainsKey(AzureDataManager.RecreateDataStoreSettingsKey)) { - if ((bool)recreateDataStore) + var recreateDataStore = (bool)localSettings.Values[AzureDataManager.RecreateDataStoreSettingsKey]; + if (recreateDataStore) { Log.Logger()?.ReportInfo("Recreating DataStore"); diff --git a/src/Logging/helpers/DictionaryExtensions.cs b/src/Logging/helpers/DictionaryExtensions.cs index c36d1c98..0de1afba 100644 --- a/src/Logging/helpers/DictionaryExtensions.cs +++ b/src/Logging/helpers/DictionaryExtensions.cs @@ -7,7 +7,10 @@ public static class DictionaryExtensions { public static void DisposeAll(this IDictionary dictionary) { - ArgumentException.ThrowIfNullOrEmpty(nameof(dictionary)); + if (dictionary is null) + { + throw new ArgumentNullException(nameof(dictionary)); + } foreach (var kv in dictionary) { diff --git a/src/Telemetry/Logger.cs b/src/Telemetry/Logger.cs index 7f529289..ce4f0eb8 100644 --- a/src/Telemetry/Logger.cs +++ b/src/Telemetry/Logger.cs @@ -12,7 +12,7 @@ namespace DevHomeAzureExtension.Telemetry; -internal sealed class Logger : ILogger +internal class Logger : ILogger { private const string ProviderName = "Microsoft.AzureExtension"; diff --git a/test/AzureExtension/AzureExtension.Test.csproj b/test/AzureExtension/AzureExtension.Test.csproj index c0ef1f4d..aff4df7a 100644 --- a/test/AzureExtension/AzureExtension.Test.csproj +++ b/test/AzureExtension/AzureExtension.Test.csproj @@ -1,12 +1,12 @@ - + enable enable AzureExtension.Test - win-x86;win-x64;win-arm64 - Properties\PublishProfiles\win-$(Platform).pubxml + win10-x86;win10-x64;win10-arm64 + Properties\PublishProfiles\win10-$(Platform).pubxml true false enable diff --git a/test/AzureExtension/DeveloperId/Mocks/MockAuthenticationHelper.cs b/test/AzureExtension/DeveloperId/Mocks/MockAuthenticationHelper.cs index d2622bca..48563684 100644 --- a/test/AzureExtension/DeveloperId/Mocks/MockAuthenticationHelper.cs +++ b/test/AzureExtension/DeveloperId/Mocks/MockAuthenticationHelper.cs @@ -12,17 +12,15 @@ namespace DevHomeAzureExtension.Test.DeveloperId.Mocks; -internal sealed class MockAuthenticationHelper : IAuthenticationHelper +internal class MockAuthenticationHelper : IAuthenticationHelper { private readonly List loginIds = new(); - - private static readonly string[] _scopes = new[] { "scope1", "scope2" }; public AuthenticationSettings MicrosoftEntraIdSettings { get => throw new NotImplementedException(); set => throw new NotImplementedException(); - } + } public Task> AcquireAllDeveloperAccountTokens(object scopesArray) => throw new NotImplementedException(); @@ -69,7 +67,7 @@ public void InitializePublicClientApplicationBuilder() tenantId: string.Empty, account: null, idToken: "id token", - scopes: _scopes, + scopes: new[] { "scope1", "scope2" }, correlationId: Guid.Empty, authenticationResultMetadata: null, tokenType: string.Empty); diff --git a/test/AzureExtension/RepositoryProvider/RepositoryProviderTests.cs b/test/AzureExtension/RepositoryProvider/RepositoryProviderTests.cs index 4f6d53ca..028d3730 100644 --- a/test/AzureExtension/RepositoryProvider/RepositoryProviderTests.cs +++ b/test/AzureExtension/RepositoryProvider/RepositoryProviderTests.cs @@ -20,7 +20,7 @@ public partial class RepositoryProviderTests private List> GetValidUrls() { - if (_validUrls.Count != 0) + if (_validUrls.Any()) { return _validUrls; } @@ -58,7 +58,7 @@ private List> GetValidUrls() private List GetInvalidUrls() { - if (_invalidUrls.Count != 0) + if (_invalidUrls.Any()) { return _invalidUrls; } @@ -72,7 +72,7 @@ private List GetInvalidUrls() private List GetValidNotRepoUrls() { - if (_validNotRepoUrls.Count != 0) + if (_validNotRepoUrls.Any()) { return _validNotRepoUrls; } diff --git a/test/Console/AzureExtension.TestConsole.csproj b/test/Console/AzureExtension.TestConsole.csproj index 68e816af..06268056 100644 --- a/test/Console/AzureExtension.TestConsole.csproj +++ b/test/Console/AzureExtension.TestConsole.csproj @@ -1,4 +1,4 @@ - + From d01e84b4cb27e356b4771e2d42ae8667a3f0599f Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Thu, 15 Feb 2024 13:04:47 -0800 Subject: [PATCH 10/15] Slipped through --- src/AzureExtension/Providers/DevHomeRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureExtension/Providers/DevHomeRepository.cs b/src/AzureExtension/Providers/DevHomeRepository.cs index 2eb7b207..9adb2a93 100644 --- a/src/AzureExtension/Providers/DevHomeRepository.cs +++ b/src/AzureExtension/Providers/DevHomeRepository.cs @@ -45,7 +45,7 @@ public DevHomeRepository(GitRepository gitRepository) } var repoInformation = new AzureUri(localUrl); - _owningAccountName = Path.Join(repoInformation.Organization, repoInformation.Repository); + _owningAccountName = Path.Join(repoInformation.Connection.Host, repoInformation.Organization, repoInformation.Project); cloneUrl = localUrl; From ea986af2362c87a0ba348e2d8bffde12221a4072 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Thu, 15 Feb 2024 13:19:19 -0800 Subject: [PATCH 11/15] Removing IRepositoryProvider since IRepositoryProvider2 requires it. --- src/AzureExtension/Providers/RepositoryProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index 24904cd5..9ac931a1 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -20,7 +20,7 @@ namespace DevHomeAzureExtension.Providers; -public class RepositoryProvider : IRepositoryProvider, IRepositoryProvider2 +public class RepositoryProvider : IRepositoryProvider2 { /// /// Used for singleton instance. From 34002d91fc727de70f443980753a748b74f14c99 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Fri, 16 Feb 2024 11:48:42 -0800 Subject: [PATCH 12/15] Using lock --- .../Helpers/AzureRepositoryHierarchy.cs | 81 +++++++------------ .../Providers/RepositoryProvider.cs | 16 ++-- 2 files changed, 36 insertions(+), 61 deletions(-) diff --git a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs index 618f0370..fc10904d 100644 --- a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs +++ b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Collections.Concurrent; using DevHomeAzureExtension.Client; using DevHomeAzureExtension.DeveloperId; using Microsoft.TeamFoundation.Core.WebApi; using Microsoft.VisualStudio.Services.Account.Client; -using Microsoft.VisualStudio.Services.Organization; using Microsoft.VisualStudio.Services.WebApi; // In the past, an organization was known as an account. Typedef to organization to make the code easier to read. @@ -19,19 +17,14 @@ namespace AzureExtension.Helpers; /// public class AzureRepositoryHierarchy { - private readonly ConcurrentDictionary> _organizationsAndProjects = new(); + private readonly object _getOrganizationsLock = new(); - /// - /// Used to keep track of multiple Get requests to make sure work isn't duplicated. - /// - private readonly ConcurrentDictionary>> _organizationsAndProjectTask = new(); + private readonly object _getProjectsLock = new(); - private readonly DeveloperId _developerId; + // 1:N Organization to project. + private readonly Dictionary> _organizationsAndProjects = new(); - /// - /// Used to keep track of all GetOrganization requests to make sure work isn't duplicated. - /// - private Task>? _queryOrganizationsTask; + private readonly DeveloperId _developerId; /// /// Initializes a new instance of the class. @@ -52,27 +45,21 @@ public AzureRepositoryHierarchy(DeveloperId developerId) /// Get all organizations the user is apart of. /// /// A list of all organizations. - public async Task> GetOrganizationsAsync() + public List GetOrganizations() { - // if something is running the query, wait for it to finish. - if (_queryOrganizationsTask != null) - { - await _queryOrganizationsTask; - return _organizationsAndProjects.Keys.ToList(); - } - - var organizations = QueryForOrganizations(); - foreach (var organization in organizations) + lock (_getOrganizationsLock) { - // Extra protection against duplicates if two threads run QueryForOrganizations at the same time. - var duplicateOrganization = _organizationsAndProjects.Keys.FirstOrDefault(x => x.AccountId == organization.AccountId); - if (duplicateOrganization == null) - { - _organizationsAndProjects.TryAdd(organization, new List()); + if (_organizationsAndProjects.Keys.Count == 0) + { + var organizations = QueryForOrganizations(); + foreach (var organization in organizations) + { + _organizationsAndProjects.TryAdd(organization, new List()); + } } - } - return _organizationsAndProjects.Keys.ToList(); + return _organizationsAndProjects.Keys.ToList(); + } } /// @@ -82,27 +69,18 @@ public async Task> GetOrganizationsAsync() /// A list of projects. public async Task> GetProjectsAsync(Organization organization) { - // if something is running the query, wait for it to finish. - if (_queryOrganizationsTask != null) - { - await _queryOrganizationsTask; - } + // Makes sure _organizationsAndProjects has all organizations. + await Task.Run(() => GetOrganizations()); - // if the task has been started - if (_organizationsAndProjectTask.TryGetValue(organization, out var projectsTask)) + lock (_getProjectsLock) { - if (projectsTask != null) + if (!_organizationsAndProjects.TryGetValue(organization, out var _)) { - // Wait for it. - await projectsTask; - return _organizationsAndProjects[organization]; + _organizationsAndProjects[organization] = QueryForProjects(organization); } - } - - var projects = QueryForProjects(organization); - _organizationsAndProjects[organization] = projects; - return _organizationsAndProjects[organization]; + return _organizationsAndProjects[organization]; + } } /// @@ -132,9 +110,8 @@ private List QueryForOrganizations() try { - _queryOrganizationsTask = accountClient.GetAccountsByMemberAsync( - memberId: accountConnection.AuthorizedIdentity.Id); - return _queryOrganizationsTask.Result; + return accountClient.GetAccountsByMemberAsync( + memberId: accountConnection.AuthorizedIdentity.Id).Result; } catch { @@ -160,13 +137,11 @@ private List QueryForProjects(Organization organization) if (connection != null) { var projectClient = connection.GetClient(); - var getProjectsTask = projectClient.GetProjects(); - - // Add the task if it does not yet exist. - _organizationsAndProjectTask.TryAdd(organization, getProjectsTask); + var projects = projectClient.GetProjects().Result.ToList(); + _organizationsAndProjects[organization] = projects; // in both cases, wait for the task to finish. - return _organizationsAndProjectTask[organization].Result.ToList(); + return projects; } } catch (Exception e) diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index 9ac931a1..4b79f856 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -165,7 +165,7 @@ public IAsyncOperation GetRepositoriesAsync(IDeveloperId dev _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); var server = string.Empty; - var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + var organizations = _azureHierarchy.GetOrganizations(); #pragma warning disable CA1309 // Use ordinal string comparison // Try to find any organizations with the modern URL format. var defaultOrg = organizations.FirstOrDefault(x => x.AccountName.Equals(_defaultSearchServerName)); @@ -371,7 +371,7 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); - var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + var organizations = _azureHierarchy.GetOrganizations().Result; var orgsInModernUrlFormat = new List(); if (serverToUse.Equals(_defaultSearchServerName)) { @@ -488,7 +488,7 @@ public IAsyncOperation> GetValuesForSearchFieldAsync(strin if (requestedSearchField.Equals(_server)) { - var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + var organizations = _azureHierarchy.GetOrganizations().Result; if (!string.IsNullOrEmpty(organizationToUse)) { organizations = organizations.Where(x => x.AccountName.Equals(organizationToUse)).OrderBy(x => x.AccountName).ToList(); @@ -512,7 +512,7 @@ public IAsyncOperation> GetValuesForSearchFieldAsync(strin else if (requestedSearchField.Equals(_organization)) { // Requesting organizations and an organization is given. - var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + var organizations = _azureHierarchy.GetOrganizations().Result; if (!string.IsNullOrEmpty(organizationToUse)) { organizations = organizations.Where(x => x.AccountName.Equals(organizationToUse)).OrderBy(x => x.AccountName).ToList(); @@ -548,7 +548,7 @@ public IAsyncOperation> GetValuesForSearchFieldAsync(strin if (isOrganizationInInput && !isProjectInInput) { // get all projects in the organization. - var organizations = _azureHierarchy.GetOrganizationsAsync().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); + var organizations = _azureHierarchy.GetOrganizations().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); List projectNames = new List(); foreach (var organization in organizations) { @@ -562,7 +562,7 @@ public IAsyncOperation> GetValuesForSearchFieldAsync(strin { // Get all projects with the same name in all organizations. // This does mean the project drop down might have duplicate names. - var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + var organizations = _azureHierarchy.GetOrganizations().Result; List projectNames = new List(); foreach (var organization in organizations) { @@ -577,7 +577,7 @@ public IAsyncOperation> GetValuesForSearchFieldAsync(strin // Get the organization. If the organization does not exist, return nothing. // If the organization has the project, return the project name. // otherwise, return nothing. - var organizations = _azureHierarchy.GetOrganizationsAsync().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); + var organizations = _azureHierarchy.GetOrganizations().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); List projectNames = new List(); foreach (var organization in organizations) @@ -612,7 +612,7 @@ public string GetDefaultValueFor(string field, IDeveloperId developerId) _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); - var organizations = _azureHierarchy.GetOrganizationsAsync().Result; + var organizations = _azureHierarchy.GetOrganizations().Result; // Get a default server name. if (field.Equals(_server, StringComparison.OrdinalIgnoreCase)) From d3be60bbbb8f7404171b22bf09ebb366b94574ea Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Fri, 16 Feb 2024 12:19:51 -0800 Subject: [PATCH 13/15] USing locks instead of tasks --- .../Helpers/AzureRepositoryHierarchy.cs | 8 +- .../Providers/RepositoryProvider.cs | 124 +----------------- 2 files changed, 12 insertions(+), 120 deletions(-) diff --git a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs index fc10904d..b16acee8 100644 --- a/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs +++ b/src/AzureExtension/Helpers/AzureRepositoryHierarchy.cs @@ -1,5 +1,5 @@ -// Copyright (c) Microsoft Corporation and Contributors -// Licensed under the MIT license. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. using DevHomeAzureExtension.Client; using DevHomeAzureExtension.DeveloperId; @@ -74,7 +74,9 @@ public async Task> GetProjectsAsync(Organization orga lock (_getProjectsLock) { - if (!_organizationsAndProjects.TryGetValue(organization, out var _)) + _organizationsAndProjects.TryGetValue(organization, out var projects); + + if (projects == null || projects.Count == 0) { _organizationsAndProjects[organization] = QueryForProjects(organization); } diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index e7704c9e..03e2ea91 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -175,117 +175,7 @@ public IAsyncOperation GetRepositoriesAsync(IDeveloperId dev server = _defaultSearchServerName; } -<<<<<<< HEAD return GetRepositoriesAsync(new Dictionary { { _server, server } }, developerId); -======= - var accountClient = accountConnection.GetClient(); - - // Get all organizations the current user belongs to. - var internalDevId = DeveloperIdProvider.GetInstance().GetDeveloperIdInternal(developerId); - IReadOnlyList? theseOrganizations; - - try - { - theseOrganizations = accountClient.GetAccountsByMemberAsync( - memberId: accountConnection.AuthorizedIdentity.Id).Result; - } - catch (Exception e) - { - return Task.Run(() => - { - return new RepositoriesResult(e, "Could not get the member id for this user."); - }).AsAsyncOperation(); - } - - // Set up parallel options to get all projects for each organization. - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = Environment.ProcessorCount, - }; - - var projectsWithOrgName = new List<(TeamProjectReference, Microsoft.VisualStudio.Services.Account.Account)>(); - - try - { - Parallel.ForEach(theseOrganizations, options, organization => - { - var projects = GetProjects(organization, azureDeveloperId); - if (projects.Count != 0) - { - foreach (var project in projects) - { - projectsWithOrgName.Add((project, organization)); - } - } - }); - } - catch (AggregateException aggregateException) - { - var exceptionMessages = new StringBuilder(); - - foreach (var exceptionMessage in aggregateException.InnerExceptions) - { - exceptionMessages.AppendLine(exceptionMessage.Message); - } - - return Task.Run(() => - { - return new RepositoriesResult(aggregateException, exceptionMessages.ToString()); - }).AsAsyncOperation(); - } - catch (Exception e) - { - return Task.Run(() => - { - return new RepositoriesResult(e, e.Message); - }).AsAsyncOperation(); - } - - projectsWithOrgName = projectsWithOrgName.OrderByDescending(x => x.Item1.LastUpdateTime).ToList(); - - // Get a list of the 200 most recently updated repos. - var reposToReturn = new List(); - foreach (var projectWithOrgName in projectsWithOrgName) - { - // Hard limit on 200. - // TODO: Figure out a better way to limit results, or, at least, pagination. - if (reposToReturn.Count >= 200) - { - break; - } - - try - { - // Making a connection can throw. - var connection = AzureClientProvider.GetConnectionForLoggedInDeveloper(projectWithOrgName.Item2.AccountUri, azureDeveloperId); - - // Make the GitHttpClient inside try/catch because an exception happens if the project is disabled. - var gitClient = connection.GetClient(); - var repos = gitClient.GetRepositoriesAsync(projectWithOrgName.Item1.Id, false, false).Result; - foreach (var repo in repos) - { - if (repo.IsDisabled.HasValue && !repo.IsDisabled.Value) - { - if (reposToReturn.Count >= 200) - { - break; - } - - reposToReturn.Add(new DevHomeRepository(repo)); - } - } - } - catch (Exception e) - { - Providers.Log.Logger()?.ReportError("DevHomeRepository", e); - } - } - - return Task.Run(() => - { - return new RepositoriesResult(reposToReturn); - }).AsAsyncOperation(); ->>>>>>> main } public IAsyncOperation GetRepositoryFromUriAsync(Uri uri) @@ -481,7 +371,7 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); - var organizations = _azureHierarchy.GetOrganizations().Result; + var organizations = _azureHierarchy.GetOrganizations(); var orgsInModernUrlFormat = new List(); if (serverToUse.Equals(_defaultSearchServerName)) { @@ -598,7 +488,7 @@ public IAsyncOperation> GetValuesForSearchFieldAsync(strin if (requestedSearchField.Equals(_server)) { - var organizations = _azureHierarchy.GetOrganizations().Result; + var organizations = _azureHierarchy.GetOrganizations(); if (!string.IsNullOrEmpty(organizationToUse)) { organizations = organizations.Where(x => x.AccountName.Equals(organizationToUse)).OrderBy(x => x.AccountName).ToList(); @@ -622,7 +512,7 @@ public IAsyncOperation> GetValuesForSearchFieldAsync(strin else if (requestedSearchField.Equals(_organization)) { // Requesting organizations and an organization is given. - var organizations = _azureHierarchy.GetOrganizations().Result; + var organizations = _azureHierarchy.GetOrganizations(); if (!string.IsNullOrEmpty(organizationToUse)) { organizations = organizations.Where(x => x.AccountName.Equals(organizationToUse)).OrderBy(x => x.AccountName).ToList(); @@ -658,7 +548,7 @@ public IAsyncOperation> GetValuesForSearchFieldAsync(strin if (isOrganizationInInput && !isProjectInInput) { // get all projects in the organization. - var organizations = _azureHierarchy.GetOrganizations().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); + var organizations = _azureHierarchy.GetOrganizations().Where(x => x.AccountName.Equals(organizationToUse)).ToList(); List projectNames = new List(); foreach (var organization in organizations) { @@ -672,7 +562,7 @@ public IAsyncOperation> GetValuesForSearchFieldAsync(strin { // Get all projects with the same name in all organizations. // This does mean the project drop down might have duplicate names. - var organizations = _azureHierarchy.GetOrganizations().Result; + var organizations = _azureHierarchy.GetOrganizations(); List projectNames = new List(); foreach (var organization in organizations) { @@ -687,7 +577,7 @@ public IAsyncOperation> GetValuesForSearchFieldAsync(strin // Get the organization. If the organization does not exist, return nothing. // If the organization has the project, return the project name. // otherwise, return nothing. - var organizations = _azureHierarchy.GetOrganizations().Result.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); + var organizations = _azureHierarchy.GetOrganizations().Where(x => x.AccountName.Equals(organizationToUse)).ToList(); List projectNames = new List(); foreach (var organization in organizations) @@ -722,7 +612,7 @@ public string GetDefaultValueFor(string field, IDeveloperId developerId) _azureHierarchy ??= new AzureRepositoryHierarchy(azureDeveloperId); - var organizations = _azureHierarchy.GetOrganizations().Result; + var organizations = _azureHierarchy.GetOrganizations(); // Get a default server name. if (field.Equals(_server, StringComparison.OrdinalIgnoreCase)) From 2a4ee0a91734f1146cfe1e5df4822f3d4253b7c4 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Fri, 16 Feb 2024 12:24:11 -0800 Subject: [PATCH 14/15] Moving return to inside the lock. --- src/AzureExtension/Providers/RepositoryProvider.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index 03e2ea91..7e0874bb 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -68,9 +68,8 @@ public static RepositoryProvider GetInstance() lock (_constructorLock) { _repositoryProvider ??= new RepositoryProvider(); + return _repositoryProvider; } - - return _repositoryProvider; } public IAsyncOperation IsUriSupportedAsync(Uri uri) From d6ebb899573c474c5566dc14f420a040d186cf07 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Tue, 20 Feb 2024 14:29:51 -0800 Subject: [PATCH 15/15] typo --- .../Providers/RepositoryProvider.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index 7e0874bb..4e1b9822 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -389,18 +389,12 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction organizations = organizations.Where(x => x.AccountName.Equals(organizationToUse)).ToList(); } - var options = new ParallelOptions() - { - // Let the program decide - MaxDegreeOfParallelism = -1, - }; - Dictionary> organizationsAndProjects = new Dictionary>(); // Establish a connection between organizations and their projects. - Parallel.ForEach(organizations, options, orgnization => + Parallel.ForEach(organizations, organization => { - var projects = _azureHierarchy.GetProjectsAsync(orgnization).Result; + var projects = _azureHierarchy.GetProjectsAsync(organization).Result; if (!string.IsNullOrEmpty(projectToUse)) { projects = projects.Where(x => x.Name.Equals(projectToUse)).ToList(); @@ -408,14 +402,14 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction if (projects.Count != 0) { - organizationsAndProjects.Add(orgnization, projects); + organizationsAndProjects.Add(organization, projects); } }); var reposToReturn = new List(); // organizationAndProjects include all relevant information to get repos. - Parallel.ForEach(organizationsAndProjects, options, organizationAndProject => + Parallel.ForEach(organizationsAndProjects, organizationAndProject => { try { @@ -424,7 +418,7 @@ public IAsyncOperation GetRepositoriesAsync(IReadOnlyDiction var criteria = new GitPullRequestSearchCriteria(); criteria.CreatorId = connection.AuthorizedIdentity.Id; - Parallel.ForEach(organizationAndProject.Value, options, project => + Parallel.ForEach(organizationAndProject.Value, project => { reposToReturn.AddRange(GetRepos(connection, project, criteria, shouldIgnorePRDateOld)); });