Skip to content

Commit f562c9a

Browse files
Enes Yağızclaude
andcommitted
fix(packages): keep and find the GitHub repository of NuGet and pub.dev packages
14 of 325 cached packages had no GitHub repository, so "View on GitHub" was missing and their notes were never looked for on GitHub. Three causes: - A metadata refresh overwrote the stored repository with the registry's value. NuGet's metadata never carries one, so a repository found while scraping was wiped on the next refresh (Newtonsoft.Json: source "GitHub", repository empty). - NuGet metadata ignored the nuspec. It now reads <repository url> from the flat container (Newtonsoft.Json, System.Text.Json, uuid) and falls back to a GitHub project URL (Bogus, Duende). - A pub.dev page links no repository when the pubspec names GitHub only as its homepage (hive). The pub.dev API's pubspec repository or homepage is used then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent edc3af4 commit f562c9a

4 files changed

Lines changed: 67 additions & 4 deletions

File tree

‎src/backend/PkgDiff.Application/Services/PackageCacheService.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,8 @@ private async Task SaveFreshMetadataToCache(
263263
cached.Authors = metadata.Authors;
264264
cached.IconUrl = metadata.IconUrl;
265265
cached.ProjectUrl = metadata.ProjectUrl;
266-
cached.GitHubRepositoryUrl = metadata.GitHubRepositoryUrl;
266+
// A repository found while scraping release notes is kept when the registry names none
267+
cached.GitHubRepositoryUrl = metadata.GitHubRepositoryUrl ?? cached.GitHubRepositoryUrl;
267268
cached.AllVersionsJson = JsonSerializer.Serialize(metadata.Versions.Select(v => v.Version));
268269
cached.CachedAt = DateTime.UtcNow;
269270
cached.ExpiresAt = DateTime.UtcNow.AddDays(14); // Extended from 7 to 14 days

‎src/backend/PkgDiff.Infrastructure/Services/NugetService.cs‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
using PkgDiff.Application.Services;
99
using PkgDiff.Domain.Interfaces;
1010
using PkgDiff.Shared.DTOs;
11+
using PkgDiff.Shared.Validation;
12+
using System.Xml;
13+
using System.Xml.Linq;
1114

1215
namespace PkgDiff.Infrastructure.Services;
1316

@@ -16,14 +19,19 @@ public class NugetService : INugetService
1619
private readonly SourceRepository _repository;
1720
private readonly SourceCacheContext _cache;
1821
private readonly IVersionFilterService _versionFilterService;
22+
private readonly IHttpClientFactory _httpClientFactory;
1923
private readonly ILogger<NugetService> _logger;
2024

21-
public NugetService(IVersionFilterService versionFilterService, ILogger<NugetService> logger)
25+
public NugetService(
26+
IVersionFilterService versionFilterService,
27+
IHttpClientFactory httpClientFactory,
28+
ILogger<NugetService> logger)
2229
{
2330
var packageSource = new PackageSource("https://api.nuget.org/v3/index.json");
2431
_repository = Repository.Factory.GetCoreV3(packageSource);
2532
_cache = new SourceCacheContext();
2633
_versionFilterService = versionFilterService;
34+
_httpClientFactory = httpClientFactory;
2735
_logger = logger;
2836
}
2937

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

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

152161
return new PackageMetadata
153162
{
@@ -156,7 +165,9 @@ public async Task<PackageMetadata> GetPackageMetadataAsync(string packageId)
156165
Description = latest.Description,
157166
Authors = latest.Authors,
158167
IconUrl = latest.IconUrl?.ToString(),
159-
ProjectUrl = latest.ProjectUrl?.ToString(),
168+
ProjectUrl = projectUrl,
169+
GitHubRepositoryUrl = await GetNuspecRepositoryUrlAsync(packageId, latest.Identity.Version)
170+
?? GitHubRepositoryUrl(projectUrl),
160171
Versions = filteredMetadata
161172
.Select(m => new { m.Identity.Version, m.Published })
162173
.DistinctBy(x => x.Version.ToString())
@@ -170,4 +181,27 @@ public async Task<PackageMetadata> GetPackageMetadataAsync(string packageId)
170181
.ToList()
171182
};
172183
}
184+
185+
// The nuspec's <repository>, which the metadata API leaves out
186+
private async Task<string?> GetNuspecRepositoryUrlAsync(string packageId, NuGetVersion version)
187+
{
188+
var id = PackageUrlEncoder.EscapeSegment(packageId.ToLowerInvariant());
189+
var normalized = PackageUrlEncoder.EscapeSegment(version.ToNormalizedString().ToLowerInvariant());
190+
try
191+
{
192+
var nuspec = await _httpClientFactory.CreateClient()
193+
.GetStringAsync($"https://api.nuget.org/v3-flatcontainer/{id}/{normalized}/{id}.nuspec");
194+
var repository = XDocument.Parse(nuspec).Descendants().FirstOrDefault(e => e.Name.LocalName == "repository");
195+
return GitHubRepositoryUrl(repository?.Attribute("url")?.Value);
196+
}
197+
catch (Exception ex) when (ex is HttpRequestException or XmlException
198+
|| (ex is TaskCanceledException && !WorkCancellation.Token.IsCancellationRequested))
199+
{
200+
_logger.LogWarning(ex, "Could not read the nuspec of {PackageId} {Version}", packageId, version);
201+
return null;
202+
}
203+
}
204+
205+
private static string? GitHubRepositoryUrl(string? url) =>
206+
GitHubUrls.IsGitHub(url) ? GitHubUrls.FromRepositoryField(url) : null;
173207
}

‎src/backend/PkgDiff.Infrastructure/Services/PubDevService.cs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ public async Task<string> GetReleaseNotesAsync(string packageId, string version)
166166
}
167167
}
168168
}
169+
170+
repositoryUrl ??= await GetPubspecGitHubUrlAsync(packageId);
169171
}
170172
catch (Exception ex)
171173
{
@@ -175,6 +177,31 @@ public async Task<string> GetReleaseNotesAsync(string packageId, string version)
175177
return (publisher, description, repositoryUrl);
176178
}
177179

180+
// The page links no repository when the pubspec names its GitHub repository only as the homepage
181+
private async Task<string?> GetPubspecGitHubUrlAsync(string packageId)
182+
{
183+
try
184+
{
185+
var json = await _httpClient.GetStringAsync($"https://pub.dev/api/packages/{PackageUrlEncoder.EscapeSegment(packageId)}");
186+
using var doc = JsonDocument.Parse(json);
187+
if (!doc.RootElement.TryGetProperty("latest", out var latest) || !latest.TryGetProperty("pubspec", out var pubspec))
188+
return null;
189+
190+
foreach (var field in new[] { "repository", "homepage" })
191+
{
192+
if (pubspec.TryGetProperty(field, out var value) && value.ValueKind == JsonValueKind.String
193+
&& GitHubUrls.IsGitHub(value.GetString()))
194+
return value.GetString();
195+
}
196+
return null;
197+
}
198+
catch (Exception ex) when (ex is HttpRequestException or JsonException)
199+
{
200+
_logger.LogWarning(ex, "Could not read the pubspec of {PackageId} from the pub.dev API", packageId);
201+
return null;
202+
}
203+
}
204+
178205
public async Task<List<(string Version, DateTime? PublishDate)>> GetAllVersionsAsync(string packageId)
179206
{
180207
var versions = new List<(string, DateTime?)>();

‎src/backend/PkgDiff.Tests/Smoke/LiveRegistrySmokeTests.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ public async Task NuGet_LiveSmoke()
8080
{
8181
SkipUnlessOptedIn();
8282
var filter = await RegistrySmokeTests.CreateSeededVersionFilterAsync();
83-
var service = new NugetService(filter, NullLogger<NugetService>.Instance);
83+
await using var provider = CreateProvider();
84+
var service = new NugetService(filter, provider.GetRequiredService<IHttpClientFactory>(), NullLogger<NugetService>.Instance);
8485

8586
var versions = await service.GetVersionsInRangeAsync(
8687
"Newtonsoft.Json",

0 commit comments

Comments
 (0)