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
32 changes: 31 additions & 1 deletion src/rbmanager/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO.Compression;
using System.Reflection;
using System.Text.RegularExpressions;

namespace RbManager;

Expand Down Expand Up @@ -208,11 +209,40 @@ internal static string Resolve(string query)
{
[var single] => single!,
[] => throw new InvalidOperationException($"no installed ruby matches '{query}'"),
_ => throw new InvalidOperationException(
_ => HighestRevision(matches!) ?? throw new InvalidOperationException(
$"'{query}' is ambiguous: {string.Join(", ", matches)}"),
};
}

// ruby-<version>[-<n>]-<platform>: a reissued release package
// (SIGNING.md in ruby/actions) differs from the original only by the
// numeric revision after the version. The revision never collides
// with a prerelease segment, which starts with a letter (rc1,
// preview1), and dev snapshot names never parse here because their
// date/commit segments sit between the version and the platform.
private static readonly Regex PackageName = new(
@"^ruby-(?<ver>\d+\.\d+\.\d+(?:-[a-z][a-z0-9]*)?)(?:-(?<rev>\d+))?-(?<plat>(?:x64|x86|arm64)-.+)$",
RegexOptions.IgnoreCase);

// When every match is the same ruby version on the same platform and
// they differ only in revision, the newest reissue wins; the
// superseded packages stay reachable by their full names. Anything
// else (different versions, dev snapshots, foreign names) stays
// ambiguous.
private static string? HighestRevision(string[] matches)
{
Match[] parsed = matches.Select(n => PackageName.Match(n)).ToArray();
if (parsed.Any(m => !m.Success)) return null;
bool sameRuby = parsed
.DistinctBy(m => $"{m.Groups["ver"].Value}|{m.Groups["plat"].Value}",
StringComparer.OrdinalIgnoreCase)
.Count() == 1;
if (!sameRuby) return null;
return parsed
.MaxBy(m => m.Groups["rev"].Success ? int.Parse(m.Groups["rev"].Value) : 0)!
.Value;
}

internal static string? CurrentTarget()
{
var info = new DirectoryInfo(Current);
Expand Down
50 changes: 50 additions & 0 deletions tests/rbmanager.Tests/ResolveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,54 @@ public void NoRubiesDirectory_NoMatch()
var ex = Assert.Throws<InvalidOperationException>(() => Program.Resolve("anything"));
Assert.Equal("no installed ruby matches 'anything'", ex.Message);
}

// Reissued packages (ruby/actions SIGNING.md): same version and
// platform differing only in the trailing numeric revision resolve
// to the newest reissue instead of erroring as ambiguous.

private const string R345 = "ruby-3.4.5-x64-mswin64_140";
private const string R345r1 = "ruby-3.4.5-1-x64-mswin64_140";
private const string R345r2 = "ruby-3.4.5-2-x64-mswin64_140";

[Fact]
public void RevisionsOfSameVersion_PickHighest()
{
using var sb = new RbSandbox();
sb.Seed(R345, R345r2, R345r1);
Assert.Equal(R345r2, Program.Resolve("3.4.5"));
}

[Fact] // revisions compare numerically, not lexicographically
public void RevisionsCompareNumerically()
{
using var sb = new RbSandbox();
sb.Seed(R345r2, "ruby-3.4.5-10-x64-mswin64_140");
Assert.Equal("ruby-3.4.5-10-x64-mswin64_140", Program.Resolve("3.4.5"));
}

[Fact] // the superseded original stays reachable by its full name
public void SupersededOriginal_FullNameStillResolves()
{
using var sb = new RbSandbox();
sb.Seed(R345, R345r1);
Assert.Equal(R345, Program.Resolve(R345));
}

[Fact] // revision preference never crosses version boundaries
public void RevisionPreference_DifferentVersionsStayAmbiguous()
{
using var sb = new RbSandbox();
sb.Seed(R345, R345r1, "ruby-3.4.51-x64-mswin64_140"); // also contains "3.4.5"
var ex = Assert.Throws<InvalidOperationException>(() => Program.Resolve("3.4.5"));
Assert.Contains("is ambiguous", ex.Message);
}

[Fact] // a prerelease is a different ruby, not a revision of the release
public void RevisionPreference_PrereleaseStaysAmbiguous()
{
using var sb = new RbSandbox();
sb.Seed("ruby-3.4.0-x64-mswin64_140", "ruby-3.4.0-rc1-x64-mswin64_140");
var ex = Assert.Throws<InvalidOperationException>(() => Program.Resolve("3.4.0"));
Assert.Contains("is ambiguous", ex.Message);
}
}
Loading