From 22e55af9e3d6ce600f9ee740c169b6386558ee1f Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Mon, 17 Nov 2025 17:56:58 -0300 Subject: [PATCH] Allow full gist and blob urls to be used to run Since the full URL is just a longer form of our ref, we can synthesize one from it. --- src/Core/RemoteRef.cs | 20 +++++++- src/Tests/DownloadTests.cs | 27 ++++++++++ src/Tests/{Tests.cs => RemoteRefTests.cs} | 57 ++++++++++++++++++++++ src/gist/Program.cs | 7 ++- src/gist/Properties/launchSettings.json | 4 ++ src/gist/gist.csproj | 2 + src/runfile/Properties/launchSettings.json | 8 +++ 7 files changed, 123 insertions(+), 2 deletions(-) rename src/Tests/{Tests.cs => RemoteRefTests.cs} (87%) diff --git a/src/Core/RemoteRef.cs b/src/Core/RemoteRef.cs index 00473d7..621728f 100644 --- a/src/Core/RemoteRef.cs +++ b/src/Core/RemoteRef.cs @@ -15,7 +15,25 @@ public override string ToString() => public static bool TryParse(string value, [NotNullWhen(true)] out RemoteRef? remote) { - Match GetMatch(string input) + // Convenience case for some common URL formats pasted from browser + if (Uri.TryCreate(value, UriKind.Absolute, out var uri)) + { + var path = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (uri.Host == "github.com" && path is [var ghOwner, var ghRepo, "blob", var ghReference, ..]) + { + value = $"{uri.Host}/{ghOwner}/{ghRepo}@{ghReference}:{string.Join('/', path[4..])}"; + } + else if (uri.Host == "gist.github.com" && path is [var gistOwner, var gistId, ..]) + { + value = $"{uri.Host}/{gistOwner}/{gistId}"; + } + else if (uri.Host == "gitlab.com" && path is [var glOwner, var glRepo, "-", "blob", var glReference, ..]) + { + value = $"{uri.Host}/{glOwner}/{glRepo}@{glReference}:{string.Join('/', path[5..])}"; + } + } + + static Match GetMatch(string input) { // Try Azure DevOps first since it is more specific var match = ParseAzureDevOpsExp().Match(input); diff --git a/src/Tests/DownloadTests.cs b/src/Tests/DownloadTests.cs index 7cfd898..b2d54d7 100644 --- a/src/Tests/DownloadTests.cs +++ b/src/Tests/DownloadTests.cs @@ -4,6 +4,32 @@ namespace Devlooped.Tests; public class DownloadTests { + [Theory] + [InlineData("https://gist.github.com/kzu/0ac826dc7de666546aaedd38e5965381")] + [InlineData("https://github.com/kzu/runfile/blob/main/run.cs")] + [InlineData("https://gitlab.com/kzu/runcs/-/blob/main/program.cs?ref_type=heads")] + public async Task DownloadPublicUnchanged(string value) + { + Assert.True(RemoteRef.TryParse(value, out var location)); + + var provider = DownloadProvider.Create(location); + var contents = await provider.GetAsync(location); + + Assert.True(contents.IsSuccessStatusCode); + + var etag = contents.Headers.ETag?.ToString(); + + location = location with + { + ETag = etag, + ResolvedUri = contents.OriginalUri + }; + + var refresh = await provider.GetAsync(location); + + Assert.Equal(HttpStatusCode.NotModified, refresh.StatusCode); + } + [LocalTheory] // Requires being authenticated with private kzu's GH repo [InlineData("github.com/kzu/runfile")] @@ -13,6 +39,7 @@ public class DownloadTests [InlineData("github.com/kzu/runfile@211de7614")] [InlineData("github.com/kzu/runfile@211de761455")] // Requires running the CLI app once against this private repo and saving a PAT + [InlineData("https://gitlab.com/kzu/runfile/-/blob/main/program.cs?ref_type=heads")] [InlineData("gitlab.com/kzu/runfile")] [InlineData("gitlab.com/kzu/runfile@v0.1.0")] [InlineData("gitlab.com/kzu/runfile@dev")] diff --git a/src/Tests/Tests.cs b/src/Tests/RemoteRefTests.cs similarity index 87% rename from src/Tests/Tests.cs rename to src/Tests/RemoteRefTests.cs index 315ec60..639d049 100644 --- a/src/Tests/Tests.cs +++ b/src/Tests/RemoteRefTests.cs @@ -475,4 +475,61 @@ public void TryParse_WithInvalidOwnerRepoCharacters_ReturnsFalse(string input) Assert.False(success); Assert.Null(result); } + + [Theory] + [InlineData("https://github.com/owner/repo/blob/main/file.txt", "github.com", "owner", "repo", "main", "file.txt")] + [InlineData("https://github.com/microsoft/vscode/blob/main/src/vs/workbench/workbench.main.ts", "github.com", "microsoft", "vscode", "main", "src/vs/workbench/workbench.main.ts")] + [InlineData("https://github.com/octocat/Hello-World/blob/master/README", "github.com", "octocat", "Hello-World", "master", "README")] + [InlineData("https://github.com/owner/repo/blob/develop/path/to/file.cs", "github.com", "owner", "repo", "develop", "path/to/file.cs")] + [InlineData("https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Console/src/System/Console.cs", "github.com", "dotnet", "runtime", "v8.0.0", "src/libraries/System.Console/src/System/Console.cs")] + public void TryParse_GitHubBlobUrls_WorksCorrectly(string input, string expectedHost, string expectedOwner, string expectedRepo, string expectedRef, string expectedPath) + { + var success = RemoteRef.TryParse(input, out var result); + + Assert.True(success); + Assert.NotNull(result); + Assert.Equal(expectedHost, result.Host); + Assert.Equal(expectedOwner, result.Owner); + Assert.Equal(expectedRepo, result.Repo); + Assert.Equal(expectedRef, result.Ref); + Assert.Equal(expectedPath, result.Path); + Assert.NotEmpty(result.TempPath); + } + + [Theory] + [InlineData("https://gist.github.com/username/123456789", "gist.github.com", "username", "123456789", null, null)] + [InlineData("https://gist.github.com/octocat/6cad326836d38bd6", "gist.github.com", "octocat", "6cad326836d38bd6", null, null)] + [InlineData("https://gist.github.com/devlooped/abc123def", "gist.github.com", "devlooped", "abc123def", null, null)] + public void TryParse_GistUrls_WorksCorrectly(string input, string expectedHost, string expectedOwner, string expectedRepo, string? expectedRef, string? expectedPath) + { + var success = RemoteRef.TryParse(input, out var result); + + Assert.True(success); + Assert.NotNull(result); + Assert.Equal(expectedHost, result.Host); + Assert.Equal(expectedOwner, result.Owner); + Assert.Equal(expectedRepo, result.Repo); + Assert.Equal(expectedRef, result.Ref); + Assert.Equal(expectedPath, result.Path); + Assert.NotEmpty(result.TempPath); + } + + [Theory] + [InlineData("https://gitlab.com/owner/repo/-/blob/main/file.txt", "gitlab.com", "owner", "repo", "main", "file.txt")] + [InlineData("https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/main.js", "gitlab.com", "gitlab-org", "gitlab", "master", "app/assets/javascripts/main.js")] + [InlineData("https://gitlab.com/inkscape/inkscape/-/blob/1.3.x/src/ui/widget/color-scales.cpp", "gitlab.com", "inkscape", "inkscape", "1.3.x", "src/ui/widget/color-scales.cpp")] + [InlineData("https://gitlab.com/fdroid/fdroidclient/-/blob/master/app/src/main/java/org/fdroid/fdroid/installer/Installer.java", "gitlab.com", "fdroid", "fdroidclient", "master", "app/src/main/java/org/fdroid/fdroid/installer/Installer.java")] + public void TryParse_GitLabBlobUrls_WorksCorrectly(string input, string expectedHost, string expectedOwner, string expectedRepo, string expectedRef, string expectedPath) + { + var success = RemoteRef.TryParse(input, out var result); + + Assert.True(success); + Assert.NotNull(result); + Assert.Equal(expectedHost, result.Host); + Assert.Equal(expectedOwner, result.Owner); + Assert.Equal(expectedRepo, result.Repo); + Assert.Equal(expectedRef, result.Ref); + Assert.Equal(expectedPath, result.Path); + Assert.NotEmpty(result.TempPath); + } } \ No newline at end of file diff --git a/src/gist/Program.cs b/src/gist/Program.cs index 020a009..a1af550 100644 --- a/src/gist/Program.cs +++ b/src/gist/Program.cs @@ -28,7 +28,12 @@ if (alias != null) args = [.. parsed.UnmatchedTokens]; -if (args.Length == 0 || !RemoteRef.TryParse("gist.github.com/" + args[0], out var location)) +RemoteRef? location = default; +var validRef = args.Length > 0 && + (RemoteRef.TryParse("gist.github.com/" + args[0], out location) || + RemoteRef.TryParse(args[0], out location)); + +if (args.Length == 0 || !validRef || location is null) { AnsiConsole.MarkupLine( $""" diff --git a/src/gist/Properties/launchSettings.json b/src/gist/Properties/launchSettings.json index f5a605f..074ba3e 100644 --- a/src/gist/Properties/launchSettings.json +++ b/src/gist/Properties/launchSettings.json @@ -6,6 +6,10 @@ "gist": { "commandName": "Project", "commandLineArgs": "kzu/0ac826dc7de666546aaedd38e5965381" + }, + "gist url": { + "commandName": "Project", + "commandLineArgs": "https://gist.github.com/kzu/0ac826dc7de666546aaedd38e5965381" } } } \ No newline at end of file diff --git a/src/gist/gist.csproj b/src/gist/gist.csproj index 05aaf19..1f15084 100644 --- a/src/gist/gist.csproj +++ b/src/gist/gist.csproj @@ -46,4 +46,6 @@ + + diff --git a/src/runfile/Properties/launchSettings.json b/src/runfile/Properties/launchSettings.json index 82b8d52..bf41f8c 100644 --- a/src/runfile/Properties/launchSettings.json +++ b/src/runfile/Properties/launchSettings.json @@ -31,6 +31,14 @@ "commandName": "Project", "commandLineArgs": "clean", "workingDirectory": "C:\\Code\\WhatsApp" + }, + "damian": { + "commandName": "Project", + "commandLineArgs": "https://github.com/DamianEdwards/runfile/blob/main/flat/filepath.cs" + }, + "gitlab url": { + "commandName": "Project", + "commandLineArgs": "https://gitlab.com/kzu/runfile/-/blob/main/program.cs?ref_type=heads" } } } \ No newline at end of file