diff --git a/.editorconfig b/.editorconfig index 984a626..557eb2e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -47,7 +47,7 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false #Formatting - wrapping options #leave code block on separate lines -csharp_preserve_single_line_blocks = false +csharp_preserve_single_line_blocks = true #Style - Code block preferences diff --git a/Directory.Build.props b/Directory.Build.props index b4a1e11..e293c53 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -32,7 +32,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index e5c97b0..3af0ea2 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -20,7 +20,7 @@ parameters: - release variables: - MSIXVersion: '0.1100' + MSIXVersion: '0.1200' solution: '**/GitHubExtension.sln' appxPackageDir: 'AppxPackages' testOutputArtifactDir: 'TestResults' diff --git a/build/scripts/CreateBuildInfo.ps1 b/build/scripts/CreateBuildInfo.ps1 index 17a84fc..53b8a5e 100644 --- a/build/scripts/CreateBuildInfo.ps1 +++ b/build/scripts/CreateBuildInfo.ps1 @@ -5,7 +5,7 @@ Param( ) $Major = "0" -$Minor = "11" +$Minor = "12" $Patch = "99" # default to 99 for local builds $versionSplit = $Version.Split("."); diff --git a/codeAnalysis/GlobalSuppressions.cs b/codeAnalysis/GlobalSuppressions.cs index 6adc7ff..3edd0bd 100644 --- a/codeAnalysis/GlobalSuppressions.cs +++ b/codeAnalysis/GlobalSuppressions.cs @@ -7,9 +7,6 @@ // a specific target and scoped to a namespace, type, member, etc. using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009:ClosingParenthesisMustBeSpacedCorrectly", Justification = "All current violations are due to Tuple shorthand and so valid.")] -[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:ClosingSquareBracketsMustBeSpacedCorrectly", Justification = "Optional arrays need to be supported. Ex []?")] - [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:PrefixLocalCallsWithThis", Justification = "We follow the C# Core Coding Style which avoids using `this` unless absolutely necessary.")] [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:UsingDirectivesMustBePlacedWithinNamespace", Justification = "We follow the C# Core Coding Style which puts using statements outside the namespace.")] diff --git a/src/GitHubExtension/Client/Exceptions.cs b/src/GitHubExtension/Client/Exceptions.cs deleted file mode 100644 index ef04f59..0000000 --- a/src/GitHubExtension/Client/Exceptions.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace GitHubExtension.Client; - -public class InvalidUrlException : Exception -{ - public InvalidUrlException() - { - } - - public InvalidUrlException(string message) - : base(message) - { - } -} - -public class InvalidGitHubUrlException : Exception -{ - public InvalidGitHubUrlException() - { - } - - public InvalidGitHubUrlException(string message) - : base(message) - { - } -} - -public class InvalidApiException : Exception -{ - public InvalidApiException() - { - } - - public InvalidApiException(string message) - : base(message) - { - } -} diff --git a/src/GitHubExtension/Client/GithubClientProvider.cs b/src/GitHubExtension/Client/GithubClientProvider.cs index 830bc53..bc53d0b 100644 --- a/src/GitHubExtension/Client/GithubClientProvider.cs +++ b/src/GitHubExtension/Client/GithubClientProvider.cs @@ -12,7 +12,7 @@ public class GitHubClientProvider { private readonly GitHubClient publicRepoClient; - private static readonly object InstanceLock = new (); + private static readonly object InstanceLock = new(); private static GitHubClientProvider? _instance; diff --git a/src/GitHubExtension/Client/InvalidApiException.cs b/src/GitHubExtension/Client/InvalidApiException.cs new file mode 100644 index 0000000..7354738 --- /dev/null +++ b/src/GitHubExtension/Client/InvalidApiException.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace GitHubExtension.Client; + +public class InvalidApiException : Exception +{ + public InvalidApiException() + { + } + + public InvalidApiException(string message) + : base(message) + { + } +} diff --git a/src/GitHubExtension/Client/InvalidGitHubUrlException.cs b/src/GitHubExtension/Client/InvalidGitHubUrlException.cs new file mode 100644 index 0000000..2084491 --- /dev/null +++ b/src/GitHubExtension/Client/InvalidGitHubUrlException.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace GitHubExtension.Client; + +public class InvalidGitHubUrlException : Exception +{ + public InvalidGitHubUrlException() + { + } + + public InvalidGitHubUrlException(string message) + : base(message) + { + } +} diff --git a/src/GitHubExtension/Client/InvalidUrlException.cs b/src/GitHubExtension/Client/InvalidUrlException.cs new file mode 100644 index 0000000..44d0bd1 --- /dev/null +++ b/src/GitHubExtension/Client/InvalidUrlException.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace GitHubExtension.Client; + +public class InvalidUrlException : Exception +{ + public InvalidUrlException() + { + } + + public InvalidUrlException(string message) + : base(message) + { + } +} diff --git a/src/GitHubExtension/DataManager/Exceptions.cs b/src/GitHubExtension/DataManager/DataStoreInaccessibleException.cs similarity index 57% rename from src/GitHubExtension/DataManager/Exceptions.cs rename to src/GitHubExtension/DataManager/DataStoreInaccessibleException.cs index 3fd9078..d39f4f3 100644 --- a/src/GitHubExtension/DataManager/Exceptions.cs +++ b/src/GitHubExtension/DataManager/DataStoreInaccessibleException.cs @@ -3,18 +3,6 @@ namespace GitHubExtension; -public class RepositoryNotFoundException : ApplicationException -{ - public RepositoryNotFoundException() - { - } - - public RepositoryNotFoundException(string message) - : base(message) - { - } -} - public class DataStoreInaccessibleException : ApplicationException { public DataStoreInaccessibleException() diff --git a/src/GitHubExtension/DataManager/DataStoreOperationParameters.cs b/src/GitHubExtension/DataManager/DataStoreOperationParameters.cs index bb1a8c4..1fee039 100644 --- a/src/GitHubExtension/DataManager/DataStoreOperationParameters.cs +++ b/src/GitHubExtension/DataManager/DataStoreOperationParameters.cs @@ -1,9 +1,10 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.Windows.DevHome.SDK; namespace GitHubExtension; + public class DataStoreOperationParameters { // parameters for updating the data store. diff --git a/src/GitHubExtension/DataManager/GitHubDataManager.cs b/src/GitHubExtension/DataManager/GitHubDataManager.cs index c5b3ec3..36141fe 100644 --- a/src/GitHubExtension/DataManager/GitHubDataManager.cs +++ b/src/GitHubExtension/DataManager/GitHubDataManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using GitHubExtension.Client; @@ -18,6 +18,7 @@ public partial class GitHubDataManager : IGitHubDataManager, IDisposable private static readonly TimeSpan SearchRetentionTime = TimeSpan.FromDays(7); private static readonly TimeSpan PullRequestStaleTime = TimeSpan.FromDays(1); private static readonly TimeSpan ReviewStaleTime = TimeSpan.FromDays(7); + private static readonly TimeSpan ReleaseRetentionTime = TimeSpan.FromDays(7); // It is possible different widgets have queries which touch the same pull requests. // We want to keep this window large enough that we don't delete data being used by @@ -179,6 +180,28 @@ public async Task UpdatePullRequestsForLoggedInDeveloperIdsAsync() SendDeveloperUpdateEvent(this); } + public async Task UpdateReleasesForRepositoryAsync(string owner, string name, RequestOptions? options = null) + { + ValidateDataStore(); + var parameters = new DataStoreOperationParameters + { + Owner = owner, + RepositoryName = name, + RequestOptions = options, + OperationName = "UpdateReleasesForRepositoryAsync", + }; + + await UpdateDataForRepositoryAsync( + parameters, + async (parameters, devId) => + { + var repository = await UpdateRepositoryAsync(parameters.Owner!, parameters.RepositoryName!, devId.GitHubClient); + await UpdateReleasesAsync(repository, devId.GitHubClient, parameters.RequestOptions); + }); + + SendRepositoryUpdateEvent(this, GetFullNameFromOwnerAndRepository(owner, name), new string[] { "Releases" }); + } + public IEnumerable GetRepositories() { ValidateDataStore(); @@ -269,29 +292,30 @@ private async Task UpdateDataForRepositoryAsync(DataStoreOperationParameters par found = true; break; } - catch (Exception ex) + catch (Exception ex) when (ex is Octokit.ApiException) { - if (ex is Octokit.ForbiddenException) - { - // This can happen most commonly with SAML-enabled organizations. - Log.Logger()?.ReportDebug(Name, $"DeveloperId {devId.LoginId} was forbidden access to {parameters.Owner}/{parameters.RepositoryName}"); - continue; - } - - if (ex is Octokit.NotFoundException) - { - // A private repository can come back as "not found" by the GitHub API when an unauthorized account cannot even view it. - Log.Logger()?.ReportDebug(Name, $"DeveloperId {devId.LoginId} did not find {parameters.Owner}/{parameters.RepositoryName}"); - continue; - } - - if (ex is Octokit.RateLimitExceededException) + switch (ex) { - Log.Logger()?.ReportError(Name, $"DeveloperId {devId.LoginId} rate limit exceeded.", ex); - throw; + case Octokit.NotFoundException: + // A private repository will come back as "not found" by the GitHub API when an unauthorized account cannot even view it. + Log.Logger()?.ReportDebug(Name, $"DeveloperId {devId.LoginId} did not find {parameters.Owner}/{parameters.RepositoryName}"); + continue; + + case Octokit.RateLimitExceededException: + Log.Logger()?.ReportDebug(Name, $"DeveloperId {devId.LoginId} rate limit exceeded."); + throw; + + case Octokit.ForbiddenException: + // This can happen most commonly with SAML-enabled organizations. + // The user may have access but the org blocked the application. + Log.Logger()?.ReportDebug(Name, $"DeveloperId {devId.LoginId} was forbidden access to {parameters.Owner}/{parameters.RepositoryName}"); + throw; + + default: + // If it's some other error like abuse detection, abort and do not continue. + Log.Logger()?.ReportDebug(Name, $"Unhandled Octokit API error for {devId.LoginId} and {parameters.Owner} / {parameters.RepositoryName}"); + throw; } - - throw; } } @@ -703,6 +727,41 @@ private async Task UpdateIssuesAsync(Repository repository, Octokit.GitHubClient Issue.DeleteLastObservedBefore(DataStore, repository.Id, DateTime.UtcNow - LastObservedDeleteSpan); } + // Internal method to update releases. Assumes Repository has already been populated and created. + // DataStore transaction is assumed to be wrapped around this in the public method. + private async Task UpdateReleasesAsync(Repository repository, Octokit.GitHubClient? client = null, RequestOptions? options = null) + { + options ??= RequestOptions.RequestOptionsDefault(); + + // Limit the number of fetched releases. + options.ApiOptions.PageCount = 1; + options.ApiOptions.PageSize = 10; + + client ??= await GitHubClientProvider.Instance.GetClientForLoggedInDeveloper(true); + Log.Logger()?.ReportInfo(Name, $"Updating releases for: {repository.FullName}"); + + var releasesResult = await client.Repository.Release.GetAll(repository.InternalId, options.ApiOptions); + if (releasesResult == null) + { + Log.Logger()?.ReportDebug($"No releases found."); + return; + } + + Log.Logger()?.ReportDebug(Name, $"Results contain {releasesResult.Count} releases."); + foreach (var release in releasesResult) + { + if (release.Draft) + { + continue; + } + + _ = Release.GetOrCreateByOctokitRelease(DataStore, release, repository); + } + + // Remove releases from this repository that were not observed recently. + Release.DeleteLastObservedBefore(DataStore, repository.Id, DateTime.UtcNow - LastObservedDeleteSpan); + } + // Removes unused data from the datastore. private void PruneObsoleteData() { @@ -714,6 +773,7 @@ private void PruneObsoleteData() Search.DeleteBefore(DataStore, DateTime.Now - SearchRetentionTime); SearchIssue.DeleteUnreferenced(DataStore); Review.DeleteUnreferenced(DataStore); + Release.DeleteBefore(DataStore, DateTime.Now - ReleaseRetentionTime); } // Sets a last-updated in the MetaData. @@ -763,7 +823,7 @@ private void ValidateDataStore() // Making the default options a singleton to avoid repeatedly calling the storage APIs and // creating a new GitHubDataStoreSchema when not necessary. - private static readonly Lazy LazyDataStoreOptions = new (DefaultOptionsInit); + private static readonly Lazy LazyDataStoreOptions = new(DefaultOptionsInit); private static DataStoreOptions DefaultOptions => LazyDataStoreOptions.Value; diff --git a/src/GitHubExtension/DataManager/IGitHubDataManager.cs b/src/GitHubExtension/DataManager/IGitHubDataManager.cs index a7f6e21..c6c981c 100644 --- a/src/GitHubExtension/DataManager/IGitHubDataManager.cs +++ b/src/GitHubExtension/DataManager/IGitHubDataManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using GitHubExtension.DataModel; @@ -25,6 +25,8 @@ public interface IGitHubDataManager : IDisposable Task UpdatePullRequestsForLoggedInDeveloperIdsAsync(); + Task UpdateReleasesForRepositoryAsync(string owner, string name, RequestOptions? options = null); + IEnumerable GetRepositories(); IEnumerable GetDeveloperUsers(); diff --git a/src/GitHubExtension/DataManager/RepositoryNotFoundException.cs b/src/GitHubExtension/DataManager/RepositoryNotFoundException.cs new file mode 100644 index 0000000..d72de9b --- /dev/null +++ b/src/GitHubExtension/DataManager/RepositoryNotFoundException.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace GitHubExtension.DataManager; + +public class RepositoryNotFoundException : ApplicationException +{ + public RepositoryNotFoundException() + { + } + + public RepositoryNotFoundException(string message) + : base(message) + { + } +} diff --git a/src/GitHubExtension/DataModel/DataObjects/Release.cs b/src/GitHubExtension/DataModel/DataObjects/Release.cs new file mode 100644 index 0000000..55eca59 --- /dev/null +++ b/src/GitHubExtension/DataModel/DataObjects/Release.cs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Dapper; +using Dapper.Contrib.Extensions; +using GitHubExtension.Helpers; + +namespace GitHubExtension.DataModel; + +[Table("Release")] +public class Release +{ + [Key] + public long Id { get; set; } = DataStore.NoForeignKey; + + public long InternalId { get; set; } = DataStore.NoForeignKey; + + // Repository table + public long RepositoryId { get; set; } = DataStore.NoForeignKey; + + public string Name { get; set; } = string.Empty; + + public string TagName { get; set; } = string.Empty; + + public long Prerelease { get; set; } = DataStore.NoForeignKey; + + public string HtmlUrl { get; set; } = string.Empty; + + public long TimeCreated { get; set; } = DataStore.NoForeignKey; + + public long TimePublished { get; set; } = DataStore.NoForeignKey; + + public long TimeLastObserved { get; set; } = DataStore.NoForeignKey; + + [Write(false)] + private DataStore? DataStore + { + get; set; + } + + [Write(false)] + [Computed] + public DateTime CreatedAt => TimeCreated.ToDateTime(); + + [Write(false)] + [Computed] + public DateTime? PublishedAt => TimePublished != 0 ? TimePublished.ToDateTime() : null; + + [Write(false)] + [Computed] + public DateTime LastObservedAt => TimeLastObserved.ToDateTime(); + + public override string ToString() => Name; + + public static Release GetOrCreateByOctokitRelease(DataStore dataStore, Octokit.Release okitRelease, Repository repository) + { + var release = CreateFromOctokitRelease(dataStore, okitRelease, repository); + return AddOrUpdateRelease(dataStore, release); + } + + public static IEnumerable GetAllForRepository(DataStore dataStore, Repository repository) + { + var sql = $"SELECT * FROM Release WHERE RepositoryId = @RepositoryId ORDER BY TimePublished DESC;"; + var param = new + { + RepositoryId = repository.Id, + }; + + Log.Logger()?.ReportDebug(DataStore.GetSqlLogMessage(sql, param)); + var releases = dataStore.Connection!.Query(sql, param, null) ?? Enumerable.Empty(); + foreach (var release in releases) + { + release.DataStore = dataStore; + } + + return releases; + } + + public static Release? GetByInternalId(DataStore dataStore, long internalId) + { + var sql = $"SELECT * FROM Release WHERE InternalId = @InternalId;"; + var param = new + { + InternalId = internalId, + }; + + var release = dataStore.Connection!.QueryFirstOrDefault(sql, param, null); + if (release is not null) + { + // Add Datastore so this object can make internal queries. + release.DataStore = dataStore; + } + + return release; + } + + public static void DeleteLastObservedBefore(DataStore dataStore, long repositoryId, DateTime date) + { + // Delete releases older than the time specified for the given repository. + // This is intended to be run after updating a repository's releases so that non-observed + // records will be removed. + var sql = @"DELETE FROM Release WHERE RepositoryId = $RepositoryId AND TimeLastObserved < $Time;"; + var command = dataStore.Connection!.CreateCommand(); + command.CommandText = sql; + command.Parameters.AddWithValue("$Time", date.ToDataStoreInteger()); + command.Parameters.AddWithValue("$RepositoryId", repositoryId); + Log.Logger()?.ReportDebug(DataStore.GetCommandLogMessage(sql, command)); + var rowsDeleted = command.ExecuteNonQuery(); + Log.Logger()?.ReportDebug(DataStore.GetDeletedLogMessage(rowsDeleted)); + } + + private static Release CreateFromOctokitRelease(DataStore dataStore, Octokit.Release okitRelease, Repository repository) + { + var release = new Release + { + DataStore = dataStore, + InternalId = okitRelease.Id, + RepositoryId = repository.Id, + Name = okitRelease.Name, + TagName = okitRelease.TagName, + Prerelease = okitRelease.Prerelease ? 1 : 0, + HtmlUrl = okitRelease.HtmlUrl, + TimeCreated = okitRelease.CreatedAt.DateTime.ToDataStoreInteger(), + TimePublished = okitRelease.PublishedAt.HasValue ? okitRelease.PublishedAt.Value.DateTime.ToDataStoreInteger() : 0, + TimeLastObserved = DateTime.UtcNow.ToDataStoreInteger(), + }; + + return release; + } + + private static Release AddOrUpdateRelease(DataStore dataStore, Release release) + { + // Check for existing release data. + var existing = GetByInternalId(dataStore, release.InternalId); + if (existing is not null) + { + // Existing releases must be updated and always marked observed. + release.Id = existing.Id; + dataStore.Connection!.Update(release); + release.DataStore = dataStore; + return release; + } + + // No existing release, add it. + release.Id = dataStore.Connection!.Insert(release); + release.DataStore = dataStore; + return release; + } + + public static void DeleteBefore(DataStore dataStore, DateTime date) + { + // Delete releases older than the date listed. + var sql = @"DELETE FROM Release WHERE TimeLastObserved < $Time;"; + var command = dataStore.Connection!.CreateCommand(); + command.CommandText = sql; + command.Parameters.AddWithValue("$Time", date.ToDataStoreInteger()); + Log.Logger()?.ReportDebug(DataStore.GetCommandLogMessage(sql, command)); + var rowsDeleted = command.ExecuteNonQuery(); + Log.Logger()?.ReportDebug(DataStore.GetDeletedLogMessage(rowsDeleted)); + } +} diff --git a/src/GitHubExtension/DataModel/DataObjects/Repository.cs b/src/GitHubExtension/DataModel/DataObjects/Repository.cs index f0fc3d8..2fbaf5b 100644 --- a/src/GitHubExtension/DataModel/DataObjects/Repository.cs +++ b/src/GitHubExtension/DataModel/DataObjects/Repository.cs @@ -109,6 +109,23 @@ public IEnumerable Issues } } + [Write(false)] + [Computed] + public IEnumerable Releases + { + get + { + if (DataStore == null) + { + return Enumerable.Empty(); + } + else + { + return Release.GetAllForRepository(DataStore, this) ?? Enumerable.Empty(); + } + } + } + public IEnumerable GetIssuesForQuery(string query) { if (DataStore == null) diff --git a/src/GitHubExtension/DataModel/GitHubDataStoreSchema.cs b/src/GitHubExtension/DataModel/GitHubDataStoreSchema.cs index 13fe1dd..440a64a 100644 --- a/src/GitHubExtension/DataModel/GitHubDataStoreSchema.cs +++ b/src/GitHubExtension/DataModel/GitHubDataStoreSchema.cs @@ -14,7 +14,7 @@ public GitHubDataStoreSchema() } // Update this anytime incompatible changes happen with a released version. - private const long SchemaVersionValue = 0x0006; + private const long SchemaVersionValue = 0x0007; private static readonly string Metadata = @"CREATE TABLE Metadata (" + @@ -248,8 +248,23 @@ public GitHubDataStoreSchema() ");" + "CREATE UNIQUE INDEX IDX_Review_InternalId ON Review (InternalId);"; + private static readonly string Release = + @"CREATE TABLE Release (" + + "Id INTEGER PRIMARY KEY NOT NULL," + + "InternalId INTEGER NOT NULL," + + "RepositoryId INTEGER NOT NULL," + + "Name TEXT NOT NULL COLLATE NOCASE," + + "TagName TEXT NOT NULL COLLATE NOCASE," + + "Prerelease INTEGER NOT NULL," + + "HtmlUrl TEXT NULL COLLATE NOCASE," + + "TimeCreated INTEGER NOT NULL," + + "TimePublished INTEGER NOT NULL," + + "TimeLastObserved INTEGER NOT NULL" + + ");" + + "CREATE UNIQUE INDEX IDX_Release_InternalId ON Release (InternalId);"; + // All Sqls together. - private static readonly List SchemaSqlsValue = new () + private static readonly List SchemaSqlsValue = new() { Metadata, User, @@ -269,5 +284,6 @@ public GitHubDataStoreSchema() Search, SearchIssue, Review, + Release, }; } diff --git a/src/GitHubExtension/DataModel/Logging.cs b/src/GitHubExtension/DataModel/Log.cs similarity index 92% rename from src/GitHubExtension/DataModel/Logging.cs rename to src/GitHubExtension/DataModel/Log.cs index 4898760..72b823c 100644 --- a/src/GitHubExtension/DataModel/Logging.cs +++ b/src/GitHubExtension/DataModel/Log.cs @@ -38,7 +38,7 @@ public static Options GetLoggingOptions() return new Options { LogFileFolderRoot = ApplicationData.Current.TemporaryFolder.Path, - LogFileName = "DataStore_{now}.log", + LogFileName = "DataStore_{now}.dhlog", LogFileFolderName = "DataStore", DebugListenerEnabled = true, #if DEBUG diff --git a/src/GitHubExtension/DeveloperId/CredentialVault.cs b/src/GitHubExtension/DeveloperId/CredentialVault.cs index 99a678c..cfe453e 100644 --- a/src/GitHubExtension/DeveloperId/CredentialVault.cs +++ b/src/GitHubExtension/DeveloperId/CredentialVault.cs @@ -112,7 +112,7 @@ public void SaveCredentials(string loginId, SecureString? accessToken) Marshal.Copy(credentialObject.CredentialBlob, accessTokenInChars, 0, accessTokenInChars.Length); // convert accessTokenInChars to string - string accessTokenString = new (accessTokenInChars); + string accessTokenString = new(accessTokenInChars); for (var i = 0; i < accessTokenInChars.Length; i++) { diff --git a/src/GitHubExtension/DeveloperId/DeveloperId.cs b/src/GitHubExtension/DeveloperId/DeveloperId.cs index 48fd4e8..c085e0d 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperId.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperId.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.Windows.DevHome.SDK; @@ -26,7 +26,7 @@ public DeveloperId() DisplayName = string.Empty; Email = string.Empty; Url = string.Empty; - GitHubClient = new (new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME)); + GitHubClient = new(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME)); } public DeveloperId(string loginId, string displayName, string email, string url, GitHubClient gitHubClient) @@ -65,7 +65,7 @@ public Windows.Security.Credentials.PasswordCredential RefreshDeveloperId() CredentialExpiryTime = DateTime.MaxValue; DeveloperIdProvider.GetInstance().RefreshDeveloperId(this); var credential = DeveloperIdProvider.GetInstance().GetCredentials(this) ?? throw new InvalidOperationException("Invalid credential present for valid DeveloperId"); - GitHubClient.Credentials = new (credential.Password); + GitHubClient.Credentials = new(credential.Password); return credential; } diff --git a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs index 566949f..a47f67f 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs @@ -14,9 +14,9 @@ namespace GitHubExtension.DeveloperId; public class DeveloperIdProvider : IDeveloperIdProviderInternal { // Locks to control access to Singleton class members. - private static readonly object _developerIdsLock = new (); + private static readonly object _developerIdsLock = new(); - private static readonly object _oAuthRequestsLock = new (); + private static readonly object _oAuthRequestsLock = new(); // DeveloperId list containing all Logged in Ids. private List DeveloperIds @@ -39,7 +39,7 @@ private List OAuthRequests public string DisplayName => "GitHub"; // DeveloperIdProvider uses singleton pattern. - private static Lazy _singletonDeveloperIdProvider = new (() => new DeveloperIdProvider()); + private static Lazy _singletonDeveloperIdProvider = new(() => new DeveloperIdProvider()); public static DeveloperIdProvider GetInstance() { @@ -51,7 +51,7 @@ private DeveloperIdProvider() { Log.Logger()?.ReportInfo($"Creating DeveloperIdProvider singleton instance"); - _credentialVault = new (() => new CredentialVault()); + _credentialVault = new(() => new CredentialVault()); lock (_oAuthRequestsLock) { @@ -76,7 +76,7 @@ private DeveloperIdProvider() public DeveloperIdsResult GetLoggedInDeveloperIds() { - List iDeveloperIds = new (); + List iDeveloperIds = new(); lock (_developerIdsLock) { iDeveloperIds.AddRange(DeveloperIds); @@ -113,11 +113,11 @@ public DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString pers { try { - GitHubClient gitHubClient = new (new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME), hostAddress); + GitHubClient gitHubClient = new(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME), hostAddress); var credentials = new Credentials(new System.Net.NetworkCredential(string.Empty, personalAccessToken).Password); gitHubClient.Credentials = credentials; var newUser = gitHubClient.User.Current().Result; - DeveloperId developerId = new (newUser.Login, newUser.Name, newUser.Email, newUser.Url, gitHubClient); + DeveloperId developerId = new(newUser.Login, newUser.Name, newUser.Email, newUser.Url, gitHubClient); SaveOrOverwriteDeveloperId(developerId, personalAccessToken); Log.Logger()?.ReportInfo($"{developerId.LoginId} logged in with PAT flow to {developerId.GetHostAddress()}"); @@ -133,7 +133,7 @@ public DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString pers private OAuthRequest? LoginNewDeveloperId() { - OAuthRequest oauthRequest = new (); + OAuthRequest oauthRequest = new(); lock (_oAuthRequestsLock) { @@ -222,7 +222,7 @@ public void HandleOauthRedirection(Uri authorizationResponse) public IEnumerable GetLoggedInDeveloperIdsInternal() { - List iDeveloperIds = new (); + List iDeveloperIds = new(); lock (_developerIdsLock) { iDeveloperIds.AddRange(DeveloperIds); @@ -321,15 +321,15 @@ private void RestoreDeveloperIds(IEnumerable loginIdsAndUrls) // For loginIds without URL, use GitHub.com as default. var hostAddress = isUrl ? new Uri(loginIdOrUrl) : new Uri(Constants.GITHUB_COM_URL); - GitHubClient gitHubClient = new (new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME), hostAddress) + GitHubClient gitHubClient = new(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME), hostAddress) { - Credentials = new (_credentialVault.Value.GetCredentials(loginIdOrUrl)?.Password), + Credentials = new(_credentialVault.Value.GetCredentials(loginIdOrUrl)?.Password), }; try { var user = gitHubClient.User.Current().Result; - DeveloperId developerId = new (user.Login, user.Name, user.Email, user.Url, gitHubClient); + DeveloperId developerId = new(user.Login, user.Name, user.Email, user.Url, gitHubClient); lock (_developerIdsLock) { DeveloperIds.Add(developerId); @@ -396,7 +396,7 @@ public void Dispose() // This function is to be used for testing purposes only. public static void ResetInstanceForTests() { - _singletonDeveloperIdProvider = new (() => new DeveloperIdProvider()); + _singletonDeveloperIdProvider = new(() => new DeveloperIdProvider()); } public IAsyncOperation ShowLogonSession(WindowId windowHandle) => throw new NotImplementedException(); diff --git a/src/GitHubExtension/DeveloperId/Logging.cs b/src/GitHubExtension/DeveloperId/Log.cs similarity index 92% rename from src/GitHubExtension/DeveloperId/Logging.cs rename to src/GitHubExtension/DeveloperId/Log.cs index 1d8e628..40b1cc8 100644 --- a/src/GitHubExtension/DeveloperId/Logging.cs +++ b/src/GitHubExtension/DeveloperId/Log.cs @@ -38,7 +38,7 @@ public static Options GetLoggingOptions() return new Options { LogFileFolderRoot = ApplicationData.Current.TemporaryFolder.Path, - LogFileName = "DeveloperId_{now}.log", + LogFileName = "DeveloperId_{now}.dhlog", LogFileFolderName = "DeveloperId", DebugListenerEnabled = true, #if DEBUG diff --git a/src/GitHubExtension/DeveloperId/OAuthRequest.cs b/src/GitHubExtension/DeveloperId/OAuthRequest.cs index 826fd6e..2c0d390 100644 --- a/src/GitHubExtension/DeveloperId/OAuthRequest.cs +++ b/src/GitHubExtension/DeveloperId/OAuthRequest.cs @@ -23,8 +23,8 @@ internal DateTime StartTime internal OAuthRequest() { - gitHubClient = new (new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME)); - oAuthCompleted = new (0); + gitHubClient = new(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME)); + oAuthCompleted = new(0); State = string.Empty; } @@ -133,7 +133,7 @@ internal DeveloperId RetrieveDeveloperId() } var newUser = gitHubClient.User.Current().Result; - DeveloperId developerId = new (newUser.Login, newUser.Name, newUser.Email, newUser.Url, gitHubClient); + DeveloperId developerId = new(newUser.Login, newUser.Name, newUser.Email, newUser.Url, gitHubClient); return developerId; } diff --git a/src/GitHubExtension/GitHubExtension.csproj b/src/GitHubExtension/GitHubExtension.csproj index ebef53d..85cca9a 100644 --- a/src/GitHubExtension/GitHubExtension.csproj +++ b/src/GitHubExtension/GitHubExtension.csproj @@ -26,6 +26,8 @@ + + @@ -38,9 +40,15 @@ Always + + Always + Always + + Always + Always @@ -80,7 +88,7 @@ - + @@ -137,6 +145,15 @@ Always + + Always + + + Always + + + Always + Always diff --git a/src/GitHubExtension/Helpers/EnumHelper.cs b/src/GitHubExtension/Helpers/EnumHelper.cs index e0db3c4..f7d3500 100644 --- a/src/GitHubExtension/Helpers/EnumHelper.cs +++ b/src/GitHubExtension/Helpers/EnumHelper.cs @@ -12,7 +12,7 @@ public class EnumHelper SearchCategory.Issues => "Issues", SearchCategory.PullRequests => "PullRequests", SearchCategory.IssuesAndPullRequests => "IssuesAndPullRequests", - _ => "unknown" + _ => "unknown", }; public static SearchCategory StringToSearchCategory(string value) diff --git a/src/GitHubExtension/Helpers/IconLoader.cs b/src/GitHubExtension/Helpers/IconLoader.cs index c6a1764..e10fe37 100644 --- a/src/GitHubExtension/Helpers/IconLoader.cs +++ b/src/GitHubExtension/Helpers/IconLoader.cs @@ -7,7 +7,7 @@ namespace GitHubExtension.Helpers; public class IconLoader { - private static readonly Dictionary Base64ImageRegistry = new (); + private static readonly Dictionary Base64ImageRegistry = new(); public static string GetIconAsBase64(string filename) { diff --git a/src/GitHubExtension/Helpers/Resources.cs b/src/GitHubExtension/Helpers/Resources.cs index e305170..0b8ce7b 100644 --- a/src/GitHubExtension/Helpers/Resources.cs +++ b/src/GitHubExtension/Helpers/Resources.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using DevHome.Logging; @@ -60,6 +60,7 @@ public static string[] GetWidgetResourceIdentifiers() "Widget_Template/EmptyAssigned", "Widget_Template/EmptyMentioned", "Widget_Template/EmptyReviews", + "Widget_Template/EmptyReleases", "Widget_Template/Pulls", "Widget_Template/Issues", "Widget_Template/Opened", @@ -102,6 +103,8 @@ public static string[] GetWidgetResourceIdentifiers() "Widget_Template_Tooltip/Save", "Widget_Template_Tooltip/Cancel", "Widget_Template/ChooseAccountPlaceholder", + "Widget_Template/Published", + "Widget_Template_Tooltip/OpenRelease", }; } } diff --git a/src/GitHubExtension/Notifications/Logging.cs b/src/GitHubExtension/Notifications/Log.cs similarity index 92% rename from src/GitHubExtension/Notifications/Logging.cs rename to src/GitHubExtension/Notifications/Log.cs index 390d7ff..55d5670 100644 --- a/src/GitHubExtension/Notifications/Logging.cs +++ b/src/GitHubExtension/Notifications/Log.cs @@ -38,7 +38,7 @@ public static Options GetLoggingOptions() return new Options { LogFileFolderRoot = ApplicationData.Current.TemporaryFolder.Path, - LogFileName = "Notifications_{now}.log", + LogFileName = "Notifications_{now}.dhlog", LogFileFolderName = "Notifications", DebugListenerEnabled = true, #if DEBUG diff --git a/src/GitHubExtension/Providers/Logging.cs b/src/GitHubExtension/Providers/Log.cs similarity index 91% rename from src/GitHubExtension/Providers/Logging.cs rename to src/GitHubExtension/Providers/Log.cs index af5e21a..58fcb34 100644 --- a/src/GitHubExtension/Providers/Logging.cs +++ b/src/GitHubExtension/Providers/Log.cs @@ -29,7 +29,7 @@ public static Options GetLoggingOptions() return new Options { LogFileFolderRoot = ApplicationData.Current.TemporaryFolder.Path, - LogFileName = "GitHubExtension_{now}.log", + LogFileName = "GitHubExtension_{now}.dhlog", LogFileFolderName = "GitHubExtension", DebugListenerEnabled = true, #if DEBUG diff --git a/src/GitHubExtension/Providers/RepositoryProvider.cs b/src/GitHubExtension/Providers/RepositoryProvider.cs index 47b1fc2..011b35a 100644 --- a/src/GitHubExtension/Providers/RepositoryProvider.cs +++ b/src/GitHubExtension/Providers/RepositoryProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using GitHubExtension.Client; +using GitHubExtension.DataModel; using GitHubExtension.DeveloperId; using GitHubExtension.Helpers; using Microsoft.Windows.DevHome.SDK; @@ -55,6 +56,19 @@ public IAsyncOperation IsUriSupportedAsync(Uri uri, }).AsAsyncOperation(); } + private Octokit.GitHubClient GetClient(IDeveloperId developerId) + { + if (developerId != null) + { + var loggedInDeveloperId = DeveloperId.DeveloperIdProvider.GetInstance().GetDeveloperIdInternal(developerId); + return loggedInDeveloperId.GitHubClient; + } + else + { + return GitHubClientProvider.Instance.GetClient(); + } + } + IAsyncOperation IRepositoryProvider.GetRepositoriesAsync(IDeveloperId developerId) { return Task.Run(async () => @@ -64,14 +78,15 @@ IAsyncOperation IRepositoryProvider.GetRepositoriesAsync(IDe var repositoryList = new List(); try { - ApiOptions apiOptions = new () + ApiOptions apiOptions = new() { - PageSize = 50, + PageSize = 100, PageCount = 1, }; // Authenticate as the specified developer Id. - var client = DeveloperIdProvider.GetInstance().GetDeveloperIdInternal(developerId).GitHubClient; + var client = GetClient(developerId); + var request = new RepositoryRequest { Sort = RepositorySort.Updated, @@ -93,12 +108,17 @@ IAsyncOperation IRepositoryProvider.GetRepositoriesAsync(IDe var getAllOrgReposTask = client.Repository.GetAllForCurrent(request, apiOptions); var publicRepos = await getPublicReposTask; + publicRepos = publicRepos.OrderByDescending(x => x.UpdatedAt).ToList(); + var privateRepos = await getPrivateReposTask; + privateRepos = privateRepos.OrderByDescending(x => x.UpdatedAt).ToList(); + var orgRepos = await getAllOrgReposTask; + orgRepos = orgRepos.OrderByDescending(x => x.UpdatedAt).ToList(); var allRepos = publicRepos.Union(privateRepos).Union(orgRepos); - foreach (var repository in allRepos.OrderByDescending(x => x.UpdatedAt)) + foreach (var repository in allRepos) { repositoryList.Add(new DevHomeRepository(repository)); } diff --git a/src/GitHubExtension/Providers/SettingsUIController.cs b/src/GitHubExtension/Providers/SettingsUIController.cs index 6c8cfa3..a4b6fa7 100644 --- a/src/GitHubExtension/Providers/SettingsUIController.cs +++ b/src/GitHubExtension/Providers/SettingsUIController.cs @@ -13,7 +13,7 @@ internal class SettingsUIController : IExtensionAdaptiveCardSession private static readonly string _notificationsEnabledString = "NotificationsEnabled"; private IExtensionAdaptiveCard? _settingsUI; - private static readonly SettingsUITemplate _settingsUITemplate = new (); + private static readonly SettingsUITemplate _settingsUITemplate = new(); public void Dispose() { diff --git a/src/GitHubExtension/Strings/en-US/Resources.resw b/src/GitHubExtension/Strings/en-US/Resources.resw index f3efd99..dde7ecd 100644 --- a/src/GitHubExtension/Strings/en-US/Resources.resw +++ b/src/GitHubExtension/Strings/en-US/Resources.resw @@ -1,4 +1,4 @@ - + @@ -86,4 +145,12 @@ GitHub (Preview) The display name of our widgets provider + + List of releases in a GitHub repository. + Description for widget that displays the releases of a repository + + + Releases + Title for widget that displays the releases of a repository + \ No newline at end of file diff --git a/src/Logging/listeners/LogFileListenerOptions.cs b/src/Logging/listeners/LogFileListenerOptions.cs index b30594b..557f8b6 100644 --- a/src/Logging/listeners/LogFileListenerOptions.cs +++ b/src/Logging/listeners/LogFileListenerOptions.cs @@ -5,7 +5,7 @@ namespace DevHome.Logging; public partial class Options { - private const string LogFileNameDefault = "DevHomeGitHubExtension.log"; + private const string LogFileNameDefault = "DevHomeGitHubExtension.dhlog"; private const string LogFileFolderNameDefault = "{now}"; public string LogFileName { get; set; } = LogFileNameDefault; diff --git a/src/Logging/listeners/StdoutListener.cs b/src/Logging/listeners/StdoutListener.cs index c8e8954..7f80c01 100644 --- a/src/Logging/listeners/StdoutListener.cs +++ b/src/Logging/listeners/StdoutListener.cs @@ -19,7 +19,7 @@ public class StdoutListener : ListenerBase // Static lock object so different instances of the Stdout listener do not simultaneously write // to stdout and have interleaved tearing of messages. - private static readonly object _stdoutLock = new (); + private static readonly object _stdoutLock = new(); public StdoutListener(string name) : base(name) diff --git a/src/Logging/logger/SeverityLevel.cs b/src/Logging/logger/FailFast.cs similarity index 100% rename from src/Logging/logger/SeverityLevel.cs rename to src/Logging/logger/FailFast.cs diff --git a/src/Logging/logger/LogEvent.cs b/src/Logging/logger/LogEvent.cs index a555a41..b1ef61d 100644 --- a/src/Logging/logger/LogEvent.cs +++ b/src/Logging/logger/LogEvent.cs @@ -47,7 +47,7 @@ public TimeSpan Elapsed public static long NoElapsedTicks => -1L; - public static TimeSpan NoElapsed => new (NoElapsedTicks); + public static TimeSpan NoElapsed => new(NoElapsedTicks); public bool HasElapsed => Elapsed.Ticks >= 0; @@ -68,7 +68,7 @@ private LogEvent(string source, string subSource, SeverityLevel severity, string public static LogEvent Create(string source, string subSource, SeverityLevel severity, string message, TimeSpan elapsed) => Create(source, subSource, severity, message, null, elapsed); - public static LogEvent Create(string source, string subSource, SeverityLevel severity, string message, Exception? exception, TimeSpan elapsed) => new (source, subSource, severity, message, exception!, elapsed); + public static LogEvent Create(string source, string subSource, SeverityLevel severity, string message, Exception? exception, TimeSpan elapsed) => new(source, subSource, severity, message, exception!, elapsed); public string FullSourceName { diff --git a/src/Logging/logger/Logger.cs b/src/Logging/logger/Logger.cs index 1601c2a..932c79d 100644 --- a/src/Logging/logger/Logger.cs +++ b/src/Logging/logger/Logger.cs @@ -45,9 +45,9 @@ public Logger(string name, Options options) Dispose(); } - private readonly BlockingCollection eventQueue = new (new ConcurrentQueue()); + private readonly BlockingCollection eventQueue = new(new ConcurrentQueue()); - private readonly ManualResetEvent processorCanceledEvent = new (true); + private readonly ManualResetEvent processorCanceledEvent = new(true); private CancellationTokenSource? cancelTokenSource; diff --git a/src/Telemetry/Logger.cs b/src/Telemetry/Logger.cs index 21af0ee..4f8275f 100644 --- a/src/Telemetry/Logger.cs +++ b/src/Telemetry/Logger.cs @@ -37,48 +37,48 @@ internal class Logger : ILogger /// Logs telemetry locally, but shouldn't upload it. Similar to an ETW event. /// Should be the same as EventSourceOptions(), as Verbose is the default level. /// - private static readonly EventSourceOptions LocalOption = new () { Level = EventLevel.Verbose }; + private static readonly EventSourceOptions LocalOption = new() { Level = EventLevel.Verbose }; /// /// Logs error telemetry locally, but shouldn't upload it. Similar to an ETW event. /// - private static readonly EventSourceOptions LocalErrorOption = new () { Level = EventLevel.Error }; + private static readonly EventSourceOptions LocalErrorOption = new() { Level = EventLevel.Error }; /// /// Logs telemetry. /// Currently this is at 0% sampling for both internal and external retail devices. /// - private static readonly EventSourceOptions InfoOption = new () { Keywords = TelemetryEventSource.TelemetryKeyword }; + private static readonly EventSourceOptions InfoOption = new() { Keywords = TelemetryEventSource.TelemetryKeyword }; /// /// Logs error telemetry. /// Currently this is at 0% sampling for both internal and external retail devices. /// - private static readonly EventSourceOptions InfoErrorOption = new () { Level = EventLevel.Error, Keywords = TelemetryEventSource.TelemetryKeyword }; + private static readonly EventSourceOptions InfoErrorOption = new() { Level = EventLevel.Error, Keywords = TelemetryEventSource.TelemetryKeyword }; /// /// Logs measure telemetry. /// This should be sent back on internal devices, and a small, sampled % of external retail devices. /// - private static readonly EventSourceOptions MeasureOption = new () { Keywords = TelemetryEventSource.MeasuresKeyword }; + private static readonly EventSourceOptions MeasureOption = new() { Keywords = TelemetryEventSource.MeasuresKeyword }; /// /// Logs measure error telemetry. /// This should be sent back on internal devices, and a small, sampled % of external retail devices. /// - private static readonly EventSourceOptions MeasureErrorOption = new () { Level = EventLevel.Error, Keywords = TelemetryEventSource.MeasuresKeyword }; + private static readonly EventSourceOptions MeasureErrorOption = new() { Level = EventLevel.Error, Keywords = TelemetryEventSource.MeasuresKeyword }; /// /// Logs critical telemetry. /// This should be sent back on all devices sampled at 100%. /// - private static readonly EventSourceOptions CriticalDataOption = new () { Keywords = TelemetryEventSource.CriticalDataKeyword }; + private static readonly EventSourceOptions CriticalDataOption = new() { Keywords = TelemetryEventSource.CriticalDataKeyword }; /// /// Logs critical error telemetry. /// This should be sent back on all devices sampled at 100%. /// - private static readonly EventSourceOptions CriticalDataErrorOption = new () { Level = EventLevel.Error, Keywords = TelemetryEventSource.CriticalDataKeyword }; + private static readonly EventSourceOptions CriticalDataErrorOption = new() { Level = EventLevel.Error, Keywords = TelemetryEventSource.CriticalDataKeyword }; /// /// ActivityId so we can correlate all events in the same run @@ -88,7 +88,7 @@ internal class Logger : ILogger /// /// List of strings we should try removing for sensitivity reasons. /// - private readonly List> sensitiveStrings = new (); + private readonly List> sensitiveStrings = new(); /// /// Initializes a new instance of the class. diff --git a/src/Telemetry/LoggerFactory.cs b/src/Telemetry/LoggerFactory.cs index 600c85a..8aa3ea9 100644 --- a/src/Telemetry/LoggerFactory.cs +++ b/src/Telemetry/LoggerFactory.cs @@ -8,7 +8,7 @@ namespace GitHubExtension.Telemetry; /// This would be useful for future when we have updated interfaces for logger like ILogger2, ILogger3 and so on public class LoggerFactory { - private static readonly object LockObj = new (); + private static readonly object LockObj = new(); private static Logger loggerInstance; diff --git a/test/GitHubExtension/DataStore/DataObjectTests.cs b/test/GitHubExtension/DataStore/DataObjectTests.cs index 8af7574..82ce2ab 100644 --- a/test/GitHubExtension/DataStore/DataObjectTests.cs +++ b/test/GitHubExtension/DataStore/DataObjectTests.cs @@ -594,4 +594,67 @@ public void ReadAndWriteReview() testListener.PrintEventCounts(); Assert.AreEqual(false, testListener.FoundErrors()); } + + [TestMethod] + [TestCategory("Unit")] + public void ReadAndWriteRelease() + { + using var log = new Logger("TestStore", TestOptions.LogOptions); + var testListener = new TestListener("TestListener", TestContext!); + log.AddListener(testListener); + Log.Attach(log); + + using var dataStore = new DataStore("TestStore", TestHelpers.GetDataStoreFilePath(TestOptions), TestOptions.DataStoreOptions.DataStoreSchema!); + Assert.IsNotNull(dataStore); + dataStore.Create(); + Assert.IsNotNull(dataStore.Connection); + + // Add repository record + dataStore.Connection.Insert(new Repository { OwnerId = 1, InternalId = 47, Name = "TestRepo1", Description = "Short Desc", HtmlUrl = "https://www.microsoft.com", DefaultBranch = "main" }); + + var releases = new List + { + { new Release { InternalId = 13, Name = "Release 0.0.1", TagName = "0.0.1", Prerelease = 1, HtmlUrl = "https://www.microsoft.com", RepositoryId = 1 } }, + { new Release { InternalId = 23, Name = "Release 1.0.0", TagName = "1.0.0", Prerelease = 0, HtmlUrl = "https://www.microsoft.com", RepositoryId = 1 } }, + }; + + using var tx = dataStore.Connection!.BeginTransaction(); + dataStore.Connection.Insert(releases[0]); + dataStore.Connection.Insert(releases[1]); + tx.Commit(); + + // Verify retrieval and input into data objects. + var dataStoreReleases = dataStore.Connection.GetAll().ToList(); + Assert.AreEqual(dataStoreReleases.Count, 2); + foreach (var release in dataStoreReleases) + { + // Get Repo info + var repo = dataStore.Connection.Get(release.RepositoryId); + + TestContext?.WriteLine($" Repo: {repo.Name} - {release.Name} - {release.TagName}"); + Assert.AreEqual("TestRepo1", repo.Name); + Assert.IsTrue(release.Id == 1 || release.Id == 2); + + if (release.Id == 1) + { + Assert.AreEqual(13, release.InternalId); + Assert.AreEqual("Release 0.0.1", release.Name); + Assert.AreEqual("0.0.1", release.TagName); + Assert.AreEqual(1, release.Prerelease); + Assert.AreEqual("https://www.microsoft.com", release.HtmlUrl); + } + + if (release.Id == 2) + { + Assert.AreEqual(23, release.InternalId); + Assert.AreEqual("Release 1.0.0", release.Name); + Assert.AreEqual("1.0.0", release.TagName); + Assert.AreEqual(0, release.Prerelease); + Assert.AreEqual("https://www.microsoft.com", release.HtmlUrl); + } + } + + testListener.PrintEventCounts(); + Assert.AreEqual(false, testListener.FoundErrors()); + } } diff --git a/test/GitHubExtension/DataStore/DataStoreTestsSetup.cs b/test/GitHubExtension/DataStore/DataStoreTestsSetup.cs index 45e5b77..407cbc1 100644 --- a/test/GitHubExtension/DataStore/DataStoreTestsSetup.cs +++ b/test/GitHubExtension/DataStore/DataStoreTestsSetup.cs @@ -12,7 +12,7 @@ public TestContext? TestContext set; } - private TestOptions testOptions = new (); + private TestOptions testOptions = new(); private TestOptions TestOptions { diff --git a/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs b/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs index 950d82e..c0a0e8d 100644 --- a/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs +++ b/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs @@ -42,7 +42,7 @@ public TestContext? TestContext set; } - private TestOptions testOptions = new (); + private TestOptions testOptions = new(); private TestOptions TestOptions { diff --git a/test/GitHubExtension/Helpers/TestSetupHelpers.cs b/test/GitHubExtension/Helpers/TestSetupHelpers.cs index c7a4a91..e4a332d 100644 --- a/test/GitHubExtension/Helpers/TestSetupHelpers.cs +++ b/test/GitHubExtension/Helpers/TestSetupHelpers.cs @@ -10,7 +10,7 @@ namespace GitHubExtension.Test; public partial class TestHelpers { private const string DataBaseFileName = "GitHubExtension-Test.db"; - private const string LogFileName = "GitHubExtension-{now}.log"; + private const string LogFileName = "GitHubExtension-{now}.dhlog"; public static void CleanupTempTestOptions(TestOptions options, TestContext context) { diff --git a/test/GitHubExtension/MockExtensionAdaptiveCard.cs b/test/GitHubExtension/MockExtensionAdaptiveCard.cs new file mode 100644 index 0000000..581eff5 --- /dev/null +++ b/test/GitHubExtension/MockExtensionAdaptiveCard.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Windows.DevHome.SDK; + +namespace GitHubExtension.Test; + +public class MockExtensionAdaptiveCard : IExtensionAdaptiveCard +{ + private int updateCount; + + public int UpdateCount + { + get => updateCount; + set => updateCount = value; + } + + public MockExtensionAdaptiveCard(string templateJson, string dataJson, string state) + { + TemplateJson = templateJson; + DataJson = dataJson; + State = state; + } + + public string DataJson + { + get; set; + } + + public string State + { + get; set; + } + + public string TemplateJson + { + get; set; + } + + public ProviderOperationResult Update(string templateJson, string dataJson, string state) + { + UpdateCount++; + TemplateJson = templateJson; + DataJson = dataJson; + State = state; + return new ProviderOperationResult(ProviderOperationStatus.Success, null, "Update() succeeded", "Update() succeeded"); + } +} diff --git a/test/GitHubExtension/Mocks/DeveloperIdProvider.cs b/test/GitHubExtension/Mocks/MockDeveloperIdProvider.cs similarity index 69% rename from test/GitHubExtension/Mocks/DeveloperIdProvider.cs rename to test/GitHubExtension/Mocks/MockDeveloperIdProvider.cs index 4800dfe..63b7b6b 100644 --- a/test/GitHubExtension/Mocks/DeveloperIdProvider.cs +++ b/test/GitHubExtension/Mocks/MockDeveloperIdProvider.cs @@ -27,7 +27,7 @@ public void Dispose() public AuthenticationState GetDeveloperIdState(IDeveloperId developerId) => throw new NotImplementedException(); - public DeveloperIdsResult GetLoggedInDeveloperIds() => new (new List()); + public DeveloperIdsResult GetLoggedInDeveloperIds() => new(new List()); public AdaptiveCardSessionResult GetLoginAdaptiveCardSession() => throw new NotImplementedException(); @@ -69,45 +69,3 @@ public static MockDeveloperIdProvider GetInstance() public IEnumerable GetLoggedInDeveloperIdsInternal() => new List(); } - -public class MockExtensionAdaptiveCard : IExtensionAdaptiveCard -{ - private int updateCount; - - public int UpdateCount - { - get => updateCount; - set => updateCount = value; - } - - public MockExtensionAdaptiveCard(string templateJson, string dataJson, string state) - { - TemplateJson = templateJson; - DataJson = dataJson; - State = state; - } - - public string DataJson - { - get; set; - } - - public string State - { - get; set; - } - - public string TemplateJson - { - get; set; - } - - public ProviderOperationResult Update(string templateJson, string dataJson, string state) - { - UpdateCount++; - TemplateJson = templateJson; - DataJson = dataJson; - State = state; - return new ProviderOperationResult(ProviderOperationStatus.Success, null, "Update() succeeded", "Update() succeeded"); - } -} diff --git a/test/GitHubExtension/Mocks/Repository.cs b/test/GitHubExtension/Mocks/MockRepository.cs similarity index 100% rename from test/GitHubExtension/Mocks/Repository.cs rename to test/GitHubExtension/Mocks/MockRepository.cs diff --git a/test/GitHubExtension/TestClass.cs b/test/GitHubExtension/TestClass.cs index 1bb75d5..e86ec9d 100644 --- a/test/GitHubExtension/TestClass.cs +++ b/test/GitHubExtension/TestClass.cs @@ -50,7 +50,7 @@ private static void RemoveTestRepo() } } - private static readonly Semaphore AuthenticationEventTriggered = new (initialCount: 0, maximumCount: 1); + private static readonly Semaphore AuthenticationEventTriggered = new(initialCount: 0, maximumCount: 1); public void AuthenticationEvent(object? sender, IDeveloperId developerId) { diff --git a/test/GitHubExtension/Widgets/WidgetTestsSetup.cs b/test/GitHubExtension/Widgets/WidgetTestsSetup.cs index a6eb4c8..a699992 100644 --- a/test/GitHubExtension/Widgets/WidgetTestsSetup.cs +++ b/test/GitHubExtension/Widgets/WidgetTestsSetup.cs @@ -12,7 +12,7 @@ public TestContext? TestContext set; } - private TestOptions testOptions = new (); + private TestOptions testOptions = new(); private TestOptions TestOptions {