Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Releases Widget #344

Merged
merged 3 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/GitHubExtension/DataManager/GitHubDataManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Repository> GetRepositories()
{
ValidateDataStore();
Expand Down Expand Up @@ -703,6 +726,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);
davidegiacometti marked this conversation as resolved.
Show resolved Hide resolved
}

// Removes unused data from the datastore.
private void PruneObsoleteData()
{
Expand All @@ -714,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.
Expand Down
4 changes: 3 additions & 1 deletion src/GitHubExtension/DataManager/IGitHubDataManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using GitHubExtension.DataModel;
Expand All @@ -25,6 +25,8 @@ public interface IGitHubDataManager : IDisposable

Task UpdatePullRequestsForLoggedInDeveloperIdsAsync();

Task UpdateReleasesForRepositoryAsync(string owner, string name, RequestOptions? options = null);

IEnumerable<Repository> GetRepositories();

IEnumerable<User> GetDeveloperUsers();
Expand Down
161 changes: 161 additions & 0 deletions src/GitHubExtension/DataModel/DataObjects/Release.cs
davidegiacometti marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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<Release> 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<Release>(sql, param, null) ?? Enumerable.Empty<Release>();
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<Release>(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));
}
}
17 changes: 17 additions & 0 deletions src/GitHubExtension/DataModel/DataObjects/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,23 @@ public IEnumerable<Issue> Issues
}
}

[Write(false)]
[Computed]
public IEnumerable<Release> Releases
{
get
{
if (DataStore == null)
{
return Enumerable.Empty<Release>();
}
else
{
return Release.GetAllForRepository(DataStore, this) ?? Enumerable.Empty<Release>();
}
}
}

public IEnumerable<Issue> GetIssuesForQuery(string query)
{
if (DataStore == null)
Expand Down
18 changes: 17 additions & 1 deletion src/GitHubExtension/DataModel/GitHubDataStoreSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (" +
Expand Down Expand Up @@ -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<string> SchemaSqlsValue = new()
{
Expand All @@ -269,5 +284,6 @@ public GitHubDataStoreSchema()
Search,
SearchIssue,
Review,
Release,
};
}
11 changes: 11 additions & 0 deletions src/GitHubExtension/GitHubExtension.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
<None Remove="Widgets\Templates\GitHubMentionedInTemplate.json" />
<None Remove="Widgets\Templates\GitHubPullsConfigurationTemplate.json" />
<None Remove="Widgets\Templates\GitHubPullsTemplate.json" />
<None Remove="Widgets\Templates\GitHubReleasesConfigurationTemplate.json" />
<None Remove="Widgets\Templates\GitHubReleasesTemplate.json" />
<None Remove="Widgets\Templates\GitHubReviewConfigurationTemplate.json" />
<None Remove="Widgets\Templates\GitHubReviewTemplate.json" />
<None Remove="Widgets\Templates\GitHubSignInTemplate.json" />
Expand All @@ -38,9 +40,15 @@
<Content Include="Widgets\Templates\GitHubAssignedTemplate.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Widgets\Templates\GitHubReleasesConfigurationTemplate.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Widgets\Templates\GitHubIssuesConfigurationTemplate.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Widgets\Templates\GitHubReleasesTemplate.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Widgets\Templates\GitHubIssuesTemplate.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
Expand Down Expand Up @@ -137,6 +145,9 @@
<Content Update="Widgets\Assets\pulls.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Widgets\Assets\releases.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Widgets\Assets\ReviewRequestedScreenshotDark.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
Expand Down
5 changes: 4 additions & 1 deletion src/GitHubExtension/Helpers/Resources.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using DevHome.Logging;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
};
}
}
Loading
Loading