Skip to content

Commit

Permalink
Add Releases Widget (#344)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidegiacometti authored Mar 4, 2024
1 parent 40dd4ba commit 7eb898c
Show file tree
Hide file tree
Showing 16 changed files with 902 additions and 4 deletions.
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 @@ -704,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()
{
Expand All @@ -715,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.
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
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

0 comments on commit 7eb898c

Please sign in to comment.