Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ private async Task SaveFreshMetadataToCache(
cached.Authors = metadata.Authors;
cached.IconUrl = metadata.IconUrl;
cached.ProjectUrl = metadata.ProjectUrl;
cached.GitHubRepositoryUrl = metadata.GitHubRepositoryUrl;
// A repository found while scraping release notes is kept when the registry names none
cached.GitHubRepositoryUrl = metadata.GitHubRepositoryUrl ?? cached.GitHubRepositoryUrl;
cached.AllVersionsJson = JsonSerializer.Serialize(metadata.Versions.Select(v => v.Version));
cached.CachedAt = DateTime.UtcNow;
cached.ExpiresAt = DateTime.UtcNow.AddDays(14); // Extended from 7 to 14 days
Expand Down
38 changes: 36 additions & 2 deletions src/backend/PkgDiff.Infrastructure/Services/NugetService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
using PkgDiff.Application.Services;
using PkgDiff.Domain.Interfaces;
using PkgDiff.Shared.DTOs;
using PkgDiff.Shared.Validation;
using System.Xml;
using System.Xml.Linq;

namespace PkgDiff.Infrastructure.Services;

Expand All @@ -16,14 +19,19 @@ public class NugetService : INugetService
private readonly SourceRepository _repository;
private readonly SourceCacheContext _cache;
private readonly IVersionFilterService _versionFilterService;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<NugetService> _logger;

public NugetService(IVersionFilterService versionFilterService, ILogger<NugetService> logger)
public NugetService(
IVersionFilterService versionFilterService,
IHttpClientFactory httpClientFactory,
ILogger<NugetService> logger)
{
var packageSource = new PackageSource("https://api.nuget.org/v3/index.json");
_repository = Repository.Factory.GetCoreV3(packageSource);
_cache = new SourceCacheContext();
_versionFilterService = versionFilterService;
_httpClientFactory = httpClientFactory;
_logger = logger;
}

Expand Down Expand Up @@ -148,6 +156,7 @@ public async Task<PackageMetadata> GetPackageMetadataAsync(string packageId)

// Use filtered metadata for latest version
var latest = filteredMetadata.OrderByDescending(m => m.Identity.Version).First();
var projectUrl = latest.ProjectUrl?.ToString();

return new PackageMetadata
{
Expand All @@ -156,7 +165,9 @@ public async Task<PackageMetadata> GetPackageMetadataAsync(string packageId)
Description = latest.Description,
Authors = latest.Authors,
IconUrl = latest.IconUrl?.ToString(),
ProjectUrl = latest.ProjectUrl?.ToString(),
ProjectUrl = projectUrl,
GitHubRepositoryUrl = await GetNuspecRepositoryUrlAsync(packageId, latest.Identity.Version)
?? GitHubRepositoryUrl(projectUrl),
Versions = filteredMetadata
.Select(m => new { m.Identity.Version, m.Published })
.DistinctBy(x => x.Version.ToString())
Expand All @@ -170,4 +181,27 @@ public async Task<PackageMetadata> GetPackageMetadataAsync(string packageId)
.ToList()
};
}

// The nuspec's <repository>, which the metadata API leaves out
private async Task<string?> GetNuspecRepositoryUrlAsync(string packageId, NuGetVersion version)
{
var id = PackageUrlEncoder.EscapeSegment(packageId.ToLowerInvariant());
var normalized = PackageUrlEncoder.EscapeSegment(version.ToNormalizedString().ToLowerInvariant());
try
{
var nuspec = await _httpClientFactory.CreateClient()
.GetStringAsync($"https://api.nuget.org/v3-flatcontainer/{id}/{normalized}/{id}.nuspec");
var repository = XDocument.Parse(nuspec).Descendants().FirstOrDefault(e => e.Name.LocalName == "repository");
return GitHubRepositoryUrl(repository?.Attribute("url")?.Value);
}
catch (Exception ex) when (ex is HttpRequestException or XmlException
|| (ex is TaskCanceledException && !WorkCancellation.Token.IsCancellationRequested))
{
_logger.LogWarning(ex, "Could not read the nuspec of {PackageId} {Version}", packageId, version);
return null;
}
}

private static string? GitHubRepositoryUrl(string? url) =>
GitHubUrls.IsGitHub(url) ? GitHubUrls.FromRepositoryField(url) : null;
}
27 changes: 27 additions & 0 deletions src/backend/PkgDiff.Infrastructure/Services/PubDevService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ public async Task<string> GetReleaseNotesAsync(string packageId, string version)
}
}
}

repositoryUrl ??= await GetPubspecGitHubUrlAsync(packageId);
}
catch (Exception ex)
{
Expand All @@ -175,6 +177,31 @@ public async Task<string> GetReleaseNotesAsync(string packageId, string version)
return (publisher, description, repositoryUrl);
}

// The page links no repository when the pubspec names its GitHub repository only as the homepage
private async Task<string?> GetPubspecGitHubUrlAsync(string packageId)
{
try
{
var json = await _httpClient.GetStringAsync($"https://pub.dev/api/packages/{PackageUrlEncoder.EscapeSegment(packageId)}");
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("latest", out var latest) || !latest.TryGetProperty("pubspec", out var pubspec))
return null;

foreach (var field in new[] { "repository", "homepage" })
{
if (pubspec.TryGetProperty(field, out var value) && value.ValueKind == JsonValueKind.String
&& GitHubUrls.IsGitHub(value.GetString()))
return value.GetString();
}
return null;
}
catch (Exception ex) when (ex is HttpRequestException or JsonException)
{
_logger.LogWarning(ex, "Could not read the pubspec of {PackageId} from the pub.dev API", packageId);
return null;
}
}

public async Task<List<(string Version, DateTime? PublishDate)>> GetAllVersionsAsync(string packageId)
{
var versions = new List<(string, DateTime?)>();
Expand Down
3 changes: 2 additions & 1 deletion src/backend/PkgDiff.Tests/Smoke/LiveRegistrySmokeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ public async Task NuGet_LiveSmoke()
{
SkipUnlessOptedIn();
var filter = await RegistrySmokeTests.CreateSeededVersionFilterAsync();
var service = new NugetService(filter, NullLogger<NugetService>.Instance);
await using var provider = CreateProvider();
var service = new NugetService(filter, provider.GetRequiredService<IHttpClientFactory>(), NullLogger<NugetService>.Instance);

var versions = await service.GetVersionsInRangeAsync(
"Newtonsoft.Json",
Expand Down