From 63f838bad6f2141e343c8cfad36d171e3f4c2b19 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Thu, 16 Oct 2025 00:39:11 -0300 Subject: [PATCH] Add support for repository url relative URLs in readme This allows permalinks at pack time by turning non-absolute URLs to repository-relative blob+commit URLs. We don't use the branch/tag since that isn't necessarily permanent. Since the repository metadata for the package is immutable, we map the URL to an equally immutable full path URL. Fixes #641 --- src/NuGetizer.Tasks/CreatePackage.cs | 21 +++++++++++++ src/NuGetizer.Tests/CreatePackageTests.cs | 36 +++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/NuGetizer.Tasks/CreatePackage.cs b/src/NuGetizer.Tasks/CreatePackage.cs index 4f55cd3c..8fd17957 100644 --- a/src/NuGetizer.Tasks/CreatePackage.cs +++ b/src/NuGetizer.Tasks/CreatePackage.cs @@ -44,6 +44,7 @@ public class CreatePackage : Task Manifest manifest; Dictionary tokens; Regex tokensExpr; + Regex linkExpr; public override bool Execute() { @@ -230,6 +231,26 @@ void GeneratePackage(Stream output = null) { // replace readme with includes replaced. var replaced = ReplaceTokens(IncludesResolver.Process(readmeFile.Source, message => Log.LogWarningCode("NG001", message))); + + if (manifest.Metadata.Repository?.Type == "git" && + !string.IsNullOrEmpty(manifest.Metadata.Repository?.Commit) && + Uri.TryCreate(manifest.Metadata.Repository.Url, UriKind.Absolute, out var uri) && + uri.Host.EndsWith("github.com")) + { + // expr to match markdown links. use named groups to capture the link text and url. + linkExpr ??= new Regex(@"\[(?[^\]]+)\]\((?[^)]+)\)", RegexOptions.None); + var repoUrl = manifest.Metadata.Repository.Url.TrimEnd('/'); + replaced = linkExpr.Replace(replaced, match => + { + var url = match.Groups["url"].Value; + if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) + return match.Value; + + var newUrl = $"{repoUrl}/blob/{manifest.Metadata.Repository.Commit}/{url.TrimStart('/')}"; + return $"[{match.Groups["text"].Value}]({newUrl})"; + }); + } + if (!replaced.Equals(File.ReadAllText(readmeFile.Source), StringComparison.Ordinal)) { var temp = Path.GetTempFileName(); diff --git a/src/NuGetizer.Tests/CreatePackageTests.cs b/src/NuGetizer.Tests/CreatePackageTests.cs index fd6a3b71..c9813088 100644 --- a/src/NuGetizer.Tests/CreatePackageTests.cs +++ b/src/NuGetizer.Tests/CreatePackageTests.cs @@ -300,6 +300,42 @@ public void when_readme_has_include_and_tokens_then_replacements_applied() Assert.Contains("NuGetizer", readme); } + [Fact] + public void when_readme_has_relativeurl_then_expands_github_url() + { + var content = Path.GetTempFileName(); + File.WriteAllText(content, "See [license](license.txt)."); + task.Contents = new[] + { + new TaskItem(content, new Metadata + { + { MetadataName.PackageId, task.Manifest.GetMetadata("Id") }, + { MetadataName.PackFolder, PackFolderKind.None }, + { MetadataName.PackagePath, "readme.md" } + }), + }; + + task.Manifest.SetMetadata("Readme", "readme.md"); + task.Manifest.SetMetadata("RepositoryType", "git"); + task.Manifest.SetMetadata("RepositoryUrl", "https://github.com/devlooped/nugetizer"); + task.Manifest.SetMetadata("RepositorySha", "9dc2cb5de"); + + createPackage = true; + ExecuteTask(out var manifest); + + Assert.NotNull(manifest); + + Assert.Equal("readme.md", manifest.Metadata.Readme); + + var file = manifest.Files.FirstOrDefault(f => Path.GetFileName(f.Target) == manifest.Metadata.Readme); + Assert.NotNull(file); + Assert.True(File.Exists(file.Source)); + + var readme = File.ReadAllText(file.Source); + + Assert.Contains("[license](https://github.com/devlooped/nugetizer/blob/9dc2cb5de/license.txt)", readme); + } + [Fact] public void when_creating_package_with_simple_dependency_then_contains_dependency_group() {