From 1ef0b0ff1dd30bc0c68974af28ba8c1afe25f5df Mon Sep 17 00:00:00 2001 From: Davide Giacometti Date: Sat, 10 Feb 2024 16:58:29 +0100 Subject: [PATCH 1/3] add releases widget --- .../DataManager/GitHubDataManager.cs | 57 ++++++ .../DataManager/IGitHubDataManager.cs | 4 +- .../DataModel/DataObjects/Release.cs | 155 ++++++++++++++++ .../DataModel/DataObjects/Repository.cs | 17 ++ .../DataModel/GitHubDataStoreSchema.cs | 18 +- src/GitHubExtension/GitHubExtension.csproj | 11 ++ src/GitHubExtension/Helpers/Resources.cs | 4 +- .../Strings/en-US/Resources.resw | 18 +- .../Widgets/Assets/releases.png | Bin 0 -> 529 bytes .../Widgets/GitHubReleasesWidget.cs | 175 ++++++++++++++++++ .../GitHubReleasesConfigurationTemplate.json | 106 +++++++++++ .../Templates/GitHubReleasesTemplate.json | 163 ++++++++++++++++ src/GitHubExtension/Widgets/WidgetProvider.cs | 1 + .../Package.appxmanifest | 34 ++++ .../Strings/en-us/Resources.resw | 67 +++++++ 15 files changed, 826 insertions(+), 4 deletions(-) create mode 100644 src/GitHubExtension/DataModel/DataObjects/Release.cs create mode 100644 src/GitHubExtension/Widgets/Assets/releases.png create mode 100644 src/GitHubExtension/Widgets/GitHubReleasesWidget.cs create mode 100644 src/GitHubExtension/Widgets/Templates/GitHubReleasesConfigurationTemplate.json create mode 100644 src/GitHubExtension/Widgets/Templates/GitHubReleasesTemplate.json diff --git a/src/GitHubExtension/DataManager/GitHubDataManager.cs b/src/GitHubExtension/DataManager/GitHubDataManager.cs index d86299c..3c9a2e2 100644 --- a/src/GitHubExtension/DataManager/GitHubDataManager.cs +++ b/src/GitHubExtension/DataManager/GitHubDataManager.cs @@ -179,6 +179,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(); @@ -703,6 +725,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 = 50; + + 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.Id); + } + + // 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() { 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/DataModel/DataObjects/Release.cs b/src/GitHubExtension/DataModel/DataObjects/Release.cs new file mode 100644 index 0000000..6f56d3e --- /dev/null +++ b/src/GitHubExtension/DataModel/DataObjects/Release.cs @@ -0,0 +1,155 @@ +// 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, long repositoryId) + { + var release = CreateFromOctokitRelease(dataStore, okitRelease, repositoryId); + 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, long repositoryId) + { + var release = new Release + { + DataStore = dataStore, + InternalId = okitRelease.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(), + }; + + var repo = Repository.GetById(dataStore, repositoryId); + + if (repo != null) + { + release.RepositoryId = repo.Id; + } + + 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; + } +} 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 ffeb916..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,6 +248,21 @@ 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() { @@ -269,5 +284,6 @@ public GitHubDataStoreSchema() Search, SearchIssue, Review, + Release, }; } diff --git a/src/GitHubExtension/GitHubExtension.csproj b/src/GitHubExtension/GitHubExtension.csproj index 15c68e5..30001ab 100644 --- a/src/GitHubExtension/GitHubExtension.csproj +++ b/src/GitHubExtension/GitHubExtension.csproj @@ -26,6 +26,8 @@ + + @@ -38,9 +40,15 @@ Always + + Always + Always + + Always + Always @@ -137,6 +145,9 @@ Always + + Always + Always diff --git a/src/GitHubExtension/Helpers/Resources.cs b/src/GitHubExtension/Helpers/Resources.cs index e305170..b3cf259 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; @@ -102,6 +102,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/Strings/en-US/Resources.resw b/src/GitHubExtension/Strings/en-US/Resources.resw index f3efd99..ba96228 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 From 49fb8558263fc8ab9cb415d9c9b9e569ab58fb24 Mon Sep 17 00:00:00 2001 From: Davide Giacometti Date: Thu, 15 Feb 2024 12:50:05 +0100 Subject: [PATCH 2/3] page size, purge unreferenced data and data object test --- .../DataManager/GitHubDataManager.cs | 4 +- .../DataModel/DataObjects/Release.cs | 322 +++++++++--------- .../DataStore/DataObjectTests.cs | 63 ++++ 3 files changed, 233 insertions(+), 156 deletions(-) diff --git a/src/GitHubExtension/DataManager/GitHubDataManager.cs b/src/GitHubExtension/DataManager/GitHubDataManager.cs index 3c9a2e2..02fa3c6 100644 --- a/src/GitHubExtension/DataManager/GitHubDataManager.cs +++ b/src/GitHubExtension/DataManager/GitHubDataManager.cs @@ -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 @@ -733,7 +734,7 @@ private async Task UpdateReleasesAsync(Repository repository, Octokit.GitHubClie // Limit the number of fetched releases. options.ApiOptions.PageCount = 1; - options.ApiOptions.PageSize = 50; + options.ApiOptions.PageSize = 10; client ??= await GitHubClientProvider.Instance.GetClientForLoggedInDeveloper(true); Log.Logger()?.ReportInfo(Name, $"Updating releases for: {repository.FullName}"); @@ -771,6 +772,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. diff --git a/src/GitHubExtension/DataModel/DataObjects/Release.cs b/src/GitHubExtension/DataModel/DataObjects/Release.cs index 6f56d3e..234e047 100644 --- a/src/GitHubExtension/DataModel/DataObjects/Release.cs +++ b/src/GitHubExtension/DataModel/DataObjects/Release.cs @@ -1,155 +1,167 @@ -// 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, long repositoryId) - { - var release = CreateFromOctokitRelease(dataStore, okitRelease, repositoryId); - 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, long repositoryId) - { - var release = new Release - { - DataStore = dataStore, - InternalId = okitRelease.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(), - }; - - var repo = Repository.GetById(dataStore, repositoryId); - - if (repo != null) - { - release.RepositoryId = repo.Id; - } - - 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; - } -} +// 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, long repositoryId) + { + var release = CreateFromOctokitRelease(dataStore, okitRelease, repositoryId); + 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, long repositoryId) + { + var release = new Release + { + DataStore = dataStore, + InternalId = okitRelease.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(), + }; + + var repo = Repository.GetById(dataStore, repositoryId); + + if (repo != null) + { + release.RepositoryId = repo.Id; + } + + 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/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()); + } } From 129d748a023dd713f7aa17fec6baff5a1ba5bba5 Mon Sep 17 00:00:00 2001 From: Davide Giacometti Date: Thu, 15 Feb 2024 14:28:54 +0100 Subject: [PATCH 3/3] fix --- .../DataManager/GitHubDataManager.cs | 2 +- .../DataModel/DataObjects/Release.cs | 16 +++++----------- src/GitHubExtension/Helpers/Resources.cs | 1 + src/GitHubExtension/Strings/en-US/Resources.resw | 4 ++++ 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/GitHubExtension/DataManager/GitHubDataManager.cs b/src/GitHubExtension/DataManager/GitHubDataManager.cs index 02fa3c6..0df7a61 100644 --- a/src/GitHubExtension/DataManager/GitHubDataManager.cs +++ b/src/GitHubExtension/DataManager/GitHubDataManager.cs @@ -754,7 +754,7 @@ private async Task UpdateReleasesAsync(Repository repository, Octokit.GitHubClie continue; } - _ = Release.GetOrCreateByOctokitRelease(DataStore, release, repository.Id); + _ = Release.GetOrCreateByOctokitRelease(DataStore, release, repository); } // Remove releases from this repository that were not observed recently. diff --git a/src/GitHubExtension/DataModel/DataObjects/Release.cs b/src/GitHubExtension/DataModel/DataObjects/Release.cs index 234e047..55eca59 100644 --- a/src/GitHubExtension/DataModel/DataObjects/Release.cs +++ b/src/GitHubExtension/DataModel/DataObjects/Release.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Dapper; @@ -52,9 +52,9 @@ private DataStore? DataStore public override string ToString() => Name; - public static Release GetOrCreateByOctokitRelease(DataStore dataStore, Octokit.Release okitRelease, long repositoryId) + public static Release GetOrCreateByOctokitRelease(DataStore dataStore, Octokit.Release okitRelease, Repository repository) { - var release = CreateFromOctokitRelease(dataStore, okitRelease, repositoryId); + var release = CreateFromOctokitRelease(dataStore, okitRelease, repository); return AddOrUpdateRelease(dataStore, release); } @@ -109,12 +109,13 @@ public static void DeleteLastObservedBefore(DataStore dataStore, long repository Log.Logger()?.ReportDebug(DataStore.GetDeletedLogMessage(rowsDeleted)); } - private static Release CreateFromOctokitRelease(DataStore dataStore, Octokit.Release okitRelease, long repositoryId) + 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, @@ -124,13 +125,6 @@ private static Release CreateFromOctokitRelease(DataStore dataStore, Octokit.Rel TimeLastObserved = DateTime.UtcNow.ToDataStoreInteger(), }; - var repo = Repository.GetById(dataStore, repositoryId); - - if (repo != null) - { - release.RepositoryId = repo.Id; - } - return release; } diff --git a/src/GitHubExtension/Helpers/Resources.cs b/src/GitHubExtension/Helpers/Resources.cs index b3cf259..0b8ce7b 100644 --- a/src/GitHubExtension/Helpers/Resources.cs +++ b/src/GitHubExtension/Helpers/Resources.cs @@ -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", diff --git a/src/GitHubExtension/Strings/en-US/Resources.resw b/src/GitHubExtension/Strings/en-US/Resources.resw index ba96228..dde7ecd 100644 --- a/src/GitHubExtension/Strings/en-US/Resources.resw +++ b/src/GitHubExtension/Strings/en-US/Resources.resw @@ -529,4 +529,8 @@ Open release Shown in Widget, Tooltip text + + There are no releases in this repository. + Shown in Widget, when there are no releases + \ No newline at end of file