Skip to content
Draft
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
405 changes: 207 additions & 198 deletions Blueprints.App/App.axaml

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Blueprints.App/Models/ApprovedSourceImportItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public sealed record ApprovedSourceImportItem(
string? Description,
bool IsDone,
SourceArtifactKind SourceKind,
string SourceReference);
string SourceReference,
string? TargetVersionName = null);
6 changes: 6 additions & 0 deletions Blueprints.App/Models/ProjectDeletionResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Blueprints.App.Models;

public sealed record ProjectDeletionResult(
string ProjectName,
bool LocalWorkspaceRootRemoved,
bool SharedWorkspaceRootRemoved);
3 changes: 2 additions & 1 deletion Blueprints.App/Models/SourceDiscoveryCandidate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ public sealed record SourceDiscoveryCandidate(
string SourceReference,
string SourceContext,
double Confidence,
ProviderReference? ProviderReference = null);
ProviderReference? ProviderReference = null,
string? SuggestedVersionName = null);
15 changes: 14 additions & 1 deletion Blueprints.App/Models/SourceImportProposal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ public SourceImportProposal(
public string ProviderReferenceSummary =>
Candidate.ProviderReference?.DisplaySummary ?? "Unclassified source reference";

public string? SuggestedVersionName => Candidate.SuggestedVersionName;

public string DestinationHint => TargetVersion?.Name ??
(string.IsNullOrWhiteSpace(SuggestedVersionName)
? "Uses fallback release"
: $"Detected release: {SuggestedVersionName}");

public string ConfidenceSummary => $"{Candidate.Confidence:P0} confidence";

public bool IsDuplicate { get; }
Expand Down Expand Up @@ -84,6 +91,12 @@ public bool IsDone
public WorkspaceVersionCard? TargetVersion
{
get => _targetVersion;
set => SetProperty(ref _targetVersion, value);
set
{
if (SetProperty(ref _targetVersion, value))
{
OnPropertyChanged(nameof(DestinationHint));
}
}
}
}
70 changes: 61 additions & 9 deletions Blueprints.App/Services/GitHubRestSourceProviderReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,23 @@ public HostedSourceDiscoveryResult Read(
ValidateRepositoryName(repositoryName);

var token = _credentialSource.GetGitHubToken();
var issuesTask = ReadRestAsync(
var issuesTask = ReadRestPagesAsync(
$"repos/{repositoryName}/issues?state=all&per_page=100",
100,
repositoryName,
"issues",
GitHubSourceJsonParser.ParseIssues,
token);
var pullRequestsTask = ReadRestAsync(
var pullRequestsTask = ReadRestPagesAsync(
$"repos/{repositoryName}/pulls?state=all&per_page=100",
100,
repositoryName,
"pull requests",
GitHubSourceJsonParser.ParsePullRequests,
token);
var releasesTask = ReadRestAsync(
var releasesTask = ReadRestPagesAsync(
$"repos/{repositoryName}/releases?per_page=50",
50,
repositoryName,
"releases",
GitHubSourceJsonParser.ParseReleases,
Expand Down Expand Up @@ -110,26 +113,69 @@ candidate.ProviderReference is null ||
!projectIssueIds.Contains(candidate.ProviderReference.Identifier))
.ToArray();

return new HostedSourceDiscoveryResult(
SourceDiscoveryCandidate[] candidates =
[
.. unlinkedIssues,
.. pullRequests.Candidates,
.. releases.Candidates,
.. projectItems.Candidates,
],
];
var boundedCandidates = candidates
.Take(HostedSourceProviderContract.MaximumCandidatesPerDiscovery)
.ToArray();
var limitWarnings = candidates.Length > boundedCandidates.Length
? new[] { $"GitHub discovery was limited to {HostedSourceProviderContract.MaximumCandidatesPerDiscovery} items." }
: [];

return new HostedSourceDiscoveryResult(
boundedCandidates,
[
.. issues.Warnings,
.. pullRequests.Warnings,
.. releases.Warnings,
.. projectItems.Warnings,
.. limitWarnings,
],
issues.Candidates.Count,
projectItems.Candidates.Count,
pullRequests.Candidates.Count,
releases.Candidates.Count);
}

private async Task<ProviderReadResult> ReadRestAsync(
private async Task<ProviderReadResult> ReadRestPagesAsync(
string relativeUri,
int pageSize,
string repositoryName,
string sourceName,
Func<string, string, IReadOnlyList<SourceDiscoveryCandidate>> parser,
string? token)
{
var candidates = new List<SourceDiscoveryCandidate>();
var warnings = new List<string>();
var maximumPages = (int)Math.Ceiling(
HostedSourceProviderContract.MaximumCandidatesPerDiscovery / (double)pageSize);
for (var page = 1; page <= maximumPages; page++)
{
var separator = relativeUri.Contains('?', StringComparison.Ordinal) ? '&' : '?';
var result = await ReadRestPageAsync(
$"{relativeUri}{separator}page={page}",
repositoryName,
sourceName,
parser,
token)
.ConfigureAwait(false);
candidates.AddRange(result.Candidates);
warnings.AddRange(result.Warnings);
if (result.RawItemCount < pageSize || result.Warnings.Count > 0)
{
break;
}
}

return new ProviderReadResult(candidates, warnings, candidates.Count);
}

private async Task<ProviderReadResult> ReadRestPageAsync(
string relativeUri,
string repositoryName,
string sourceName,
Expand All @@ -149,7 +195,11 @@ private async Task<ProviderReadResult> ReadRestAsync(
return Failure(sourceName, response.StatusCode);
}

return new ProviderReadResult(parser(body, repositoryName), []);
using var document = JsonDocument.Parse(body);
var rawItemCount = document.RootElement.ValueKind == JsonValueKind.Array
? document.RootElement.GetArrayLength()
: 0;
return new ProviderReadResult(parser(body, repositoryName), [], rawItemCount);
}
catch (Exception exception) when (
exception is HttpRequestException or
Expand All @@ -159,7 +209,8 @@ JsonException or
{
return new ProviderReadResult(
[],
[$"GitHub {sourceName} were skipped: {SafeMessage(exception.Message)}"]);
[$"GitHub {sourceName} were skipped: {SafeMessage(exception.Message)}"],
0);
}
}

Expand Down Expand Up @@ -313,5 +364,6 @@ private static string SafeMessage(string message)

private sealed record ProviderReadResult(
IReadOnlyList<SourceDiscoveryCandidate> Candidates,
IReadOnlyList<string> Warnings);
IReadOnlyList<string> Warnings,
int RawItemCount = 0);
}
6 changes: 4 additions & 2 deletions Blueprints.App/Services/GitHubSourceJsonParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,8 @@ private static SourceDiscoveryCandidate ParseRelease(
ProviderReferenceKind.Release,
repositoryName,
tag,
url));
url),
tag.TrimStart('v', 'V'));
}

private static IReadOnlyList<string> ReadReleaseAssetNames(JsonElement release)
Expand Down Expand Up @@ -352,7 +353,8 @@ private static SourceDiscoveryCandidate ParseIssue(
ProviderReferenceKind.Issue,
repositoryName,
$"#{number}",
url));
url),
milestone);
}

private static IReadOnlyList<string> ReadProjectNames(JsonElement issue)
Expand Down
68 changes: 59 additions & 9 deletions Blueprints.App/Services/GitLabRestSourceProviderReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,26 +56,30 @@ public HostedSourceDiscoveryResult Read(

var project = Uri.EscapeDataString(repository.RepositoryName);
var token = _credentialSource.GetGitLabToken();
var issuesTask = ReadAsync(
var issuesTask = ReadPagesAsync(
$"projects/{project}/issues?scope=all&per_page=100",
100,
repository.RepositoryName,
"issues",
GitLabSourceJsonParser.ParseIssues,
token);
var mergeRequestsTask = ReadAsync(
var mergeRequestsTask = ReadPagesAsync(
$"projects/{project}/merge_requests?scope=all&state=all&per_page=100",
100,
repository.RepositoryName,
"merge requests",
GitLabSourceJsonParser.ParseMergeRequests,
token);
var releasesTask = ReadAsync(
var releasesTask = ReadPagesAsync(
$"projects/{project}/releases?per_page=50",
50,
repository.RepositoryName,
"releases",
GitLabSourceJsonParser.ParseReleases,
token);
var milestonesTask = ReadAsync(
var milestonesTask = ReadPagesAsync(
$"projects/{project}/milestones?per_page=100",
100,
repository.RepositoryName,
"milestones",
GitLabSourceJsonParser.ParseMilestones,
Expand All @@ -91,26 +95,67 @@ public HostedSourceDiscoveryResult Read(
var mergeRequests = mergeRequestsTask.Result;
var releases = releasesTask.Result;
var milestones = milestonesTask.Result;
return new HostedSourceDiscoveryResult(
SourceDiscoveryCandidate[] candidates =
[
.. issues.Candidates,
.. mergeRequests.Candidates,
.. releases.Candidates,
.. milestones.Candidates,
],
];
var boundedCandidates = candidates
.Take(HostedSourceProviderContract.MaximumCandidatesPerDiscovery)
.ToArray();
var limitWarnings = candidates.Length > boundedCandidates.Length
? new[] { $"GitLab discovery was limited to {HostedSourceProviderContract.MaximumCandidatesPerDiscovery} items." }
: [];
return new HostedSourceDiscoveryResult(
boundedCandidates,
[
.. issues.Warnings,
.. mergeRequests.Warnings,
.. releases.Warnings,
.. milestones.Warnings,
.. limitWarnings,
],
issues.Candidates.Count,
milestones.Candidates.Count,
mergeRequests.Candidates.Count,
releases.Candidates.Count);
}

private async Task<ProviderReadResult> ReadAsync(
private async Task<ProviderReadResult> ReadPagesAsync(
string relativeUri,
int pageSize,
string repositoryName,
string sourceName,
Func<string, string, IReadOnlyList<SourceDiscoveryCandidate>> parser,
string? token)
{
var candidates = new List<SourceDiscoveryCandidate>();
var warnings = new List<string>();
var maximumPages = (int)Math.Ceiling(
HostedSourceProviderContract.MaximumCandidatesPerDiscovery / (double)pageSize);
for (var page = 1; page <= maximumPages; page++)
{
var separator = relativeUri.Contains('?', StringComparison.Ordinal) ? '&' : '?';
var result = await ReadPageAsync(
$"{relativeUri}{separator}page={page}",
repositoryName,
sourceName,
parser,
token)
.ConfigureAwait(false);
candidates.AddRange(result.Candidates);
warnings.AddRange(result.Warnings);
if (result.RawItemCount < pageSize || result.Warnings.Count > 0)
{
break;
}
}
return new ProviderReadResult(candidates, warnings, candidates.Count);
}

private async Task<ProviderReadResult> ReadPageAsync(
string relativeUri,
string repositoryName,
string sourceName,
Expand All @@ -134,7 +179,11 @@ private async Task<ProviderReadResult> ReadAsync(
{
return Failure(sourceName, response.StatusCode);
}
return new ProviderReadResult(parser(body, repositoryName), []);
using var document = System.Text.Json.JsonDocument.Parse(body);
var rawItemCount = document.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array
? document.RootElement.GetArrayLength()
: 0;
return new ProviderReadResult(parser(body, repositoryName), [], rawItemCount);
}
catch (Exception exception) when (
exception is HttpRequestException or
Expand Down Expand Up @@ -217,5 +266,6 @@ private static string SafeMessage(string message)

private sealed record ProviderReadResult(
IReadOnlyList<SourceDiscoveryCandidate> Candidates,
IReadOnlyList<string> Warnings);
IReadOnlyList<string> Warnings,
int RawItemCount = 0);
}
9 changes: 6 additions & 3 deletions Blueprints.App/Services/GitLabSourceJsonParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ milestone is null
ProviderReferenceKind.Issue,
repositoryName,
$"#{iid}",
ReadString(issue, "web_url")));
ReadString(issue, "web_url")),
milestone);
})
.ToArray();
}
Expand Down Expand Up @@ -117,7 +118,8 @@ public static IReadOnlyList<SourceDiscoveryCandidate> ParseReleases(
ProviderReferenceKind.Release,
repositoryName,
tag,
ReadNestedString(release, "_links", "self")));
ReadNestedString(release, "_links", "self")),
tag.TrimStart('v', 'V'));
})
.ToArray();
}
Expand Down Expand Up @@ -154,7 +156,8 @@ public static IReadOnlyList<SourceDiscoveryCandidate> ParseMilestones(
ProviderReferenceKind.Project,
repositoryName,
iid.ToString(System.Globalization.CultureInfo.InvariantCulture),
$"https://gitlab.com/{repositoryName}/-/milestones/{iid}"));
$"https://gitlab.com/{repositoryName}/-/milestones/{iid}"),
title);
})
.ToArray();
}
Expand Down
2 changes: 1 addition & 1 deletion Blueprints.App/Services/HostedSourceProviderContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ namespace Blueprints.App.Services;
public static class HostedSourceProviderContract
{
public const int CurrentVersion = 1;
public const int MaximumCandidatesPerDiscovery = 500;
public const int MaximumCandidatesPerDiscovery = 5_000;
public const int MaximumWarningsPerDiscovery = 64;
}
Loading
Loading