Skip to content

Commit a5f54b8

Browse files
committed
Order pre-releases of the same version by their identifiers
Two pre-releases of one major.minor.patch fell through to "not newer", so 0.3.0-rc.2 was not an update over 0.3.0-rc.1. #205 asks for both that and the release-over-pre-release case; the previous commit covered only the latter. prerelease_gt walks the dot-separated identifiers the way SemVer 11.4 describes: numeric ones compare as numbers so rc.11 outranks rc.2, a numeric identifier ranks below an alphanumeric one, alphanumeric ones compare byte-wise under LC_ALL=C so the locale cannot decide whether beta sorts above alpha, and with every shared identifier equal the shorter list ranks lower. Build metadata is stripped before anything else. It never affects precedence, and because it may contain '-' it had to go before the pre-release split -- on main, 1.0.0+build.9 reached the numeric compare as a patch of "0+build.9" and errored out. The tests walk the precedence chain semver.org gives for 11.4 in both directions, plus the two rows #205 lists and the build-metadata cases. All of them fail on main.
1 parent e9e96c4 commit a5f54b8

2 files changed

Lines changed: 138 additions & 4 deletions

File tree

‎arcup/arcup‎

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,11 @@ version_gt() {
265265
local ver1="${1#v}"
266266
local ver2="${2#v}"
267267

268+
# Build metadata never affects precedence (SemVer 10), and it may itself
269+
# contain '-', so it has to go before the pre-release split below.
270+
ver1="${ver1%%+*}"
271+
ver2="${ver2%%+*}"
272+
268273
# Keep the pre-release tags aside: SemVer orders a release above any
269274
# pre-release of the same major.minor.patch.
270275
local pre1="" pre2=""
@@ -292,13 +297,73 @@ EOF
292297
[ "$patch1" -gt "$patch2" ] && return 0
293298
[ "$patch1" -lt "$patch2" ] && return 1
294299

295-
# Same major.minor.patch. A release outranks a pre-release of it, so
296-
# 0.3.0 is newer than 0.3.0-rc.1. Two pre-releases of the same version
297-
# are not ordered here; no caller compares them.
298-
if [ -z "$pre1" ] && [ -n "$pre2" ]; then
300+
# Same major.minor.patch. A release outranks any pre-release of it, so
301+
# 0.3.0 is newer than 0.3.0-rc.1 (SemVer 11.3). Two pre-releases are
302+
# ordered identifier by identifier (SemVer 11.4).
303+
if [ -z "$pre1" ] && [ -z "$pre2" ]; then
304+
return 1
305+
fi
306+
if [ -z "$pre1" ]; then
299307
return 0
300308
fi
309+
if [ -z "$pre2" ]; then
310+
return 1
311+
fi
312+
prerelease_gt "$pre1" "$pre2"
313+
}
314+
315+
# Order two dot-separated pre-release identifier lists (SemVer 11.4).
316+
# Returns 0 when the first ranks higher, 1 otherwise.
317+
prerelease_gt() {
318+
local a="$1" b="$2"
319+
local ia ib na nb
320+
# Byte-wise comparison for the alphanumeric case; the locale must not
321+
# decide whether "beta" sorts above "alpha".
322+
local LC_ALL=C
323+
324+
while [ -n "$a" ] || [ -n "$b" ]; do
325+
# With every shared identifier equal, the shorter list ranks lower
326+
# (11.4.4): 1.0.0-alpha < 1.0.0-alpha.1.
327+
if [ -z "$a" ]; then
328+
return 1
329+
fi
330+
if [ -z "$b" ]; then
331+
return 0
332+
fi
333+
334+
ia="${a%%.*}"
335+
ib="${b%%.*}"
336+
if [ "$a" = "$ia" ]; then a=""; else a="${a#*.}"; fi
337+
if [ "$b" = "$ib" ]; then b=""; else b="${b#*.}"; fi
301338

339+
if [ "$ia" = "$ib" ]; then
340+
continue
341+
fi
342+
343+
na=0; nb=0
344+
case "$ia" in ''|*[!0-9]*) ;; *) na=1 ;; esac
345+
case "$ib" in ''|*[!0-9]*) ;; *) nb=1 ;; esac
346+
347+
# Both numeric: compare as numbers, so rc.11 is newer than rc.2 (11.4.1).
348+
if [ "$na" = 1 ] && [ "$nb" = 1 ]; then
349+
if [ "$ia" -gt "$ib" ]; then
350+
return 0
351+
fi
352+
return 1
353+
fi
354+
# A numeric identifier ranks below an alphanumeric one (11.4.3).
355+
if [ "$na" = 1 ]; then
356+
return 1
357+
fi
358+
if [ "$nb" = 1 ]; then
359+
return 0
360+
fi
361+
# Both alphanumeric: ASCII order (11.4.2).
362+
if [[ "$ia" > "$ib" ]]; then
363+
return 0
364+
fi
365+
return 1
366+
done
302367
return 1
303368
}
304369

‎arcup/test_arcup.sh‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,73 @@ test_version_comparison() {
7676
pass "release is newer than its prerelease with v prefix"
7777
}
7878

79+
# The precedence chain semver.org gives for section 11.4, checked link by link
80+
# in both directions, plus the two pre-release comparisons #205 asks for.
81+
test_prerelease_precedence() {
82+
local chain=(
83+
"1.0.0-alpha"
84+
"1.0.0-alpha.1"
85+
"1.0.0-alpha.beta"
86+
"1.0.0-beta"
87+
"1.0.0-beta.2"
88+
"1.0.0-beta.11"
89+
"1.0.0-rc.1"
90+
"1.0.0"
91+
)
92+
local i lower higher
93+
for ((i = 1; i < ${#chain[@]}; i++)); do
94+
lower="${chain[i-1]}"
95+
higher="${chain[i]}"
96+
if ! version_gt "$higher" "$lower"; then
97+
fail "$higher is newer than $lower"
98+
fi
99+
if version_gt "$lower" "$higher"; then
100+
fail "$lower is not newer than $higher"
101+
fi
102+
done
103+
pass "orders the semver.org 11.4 chain"
104+
105+
if ! version_gt "0.3.0-rc.2" "0.3.0-rc.1"; then
106+
fail "later release candidate is newer"
107+
fi
108+
pass "later release candidate is newer"
109+
110+
if ! version_gt "1.0.0-beta.11" "1.0.0-beta.2"; then
111+
fail "numeric identifiers compare as numbers, not strings"
112+
fi
113+
pass "numeric identifiers compare as numbers, not strings"
114+
115+
if version_gt "1.0.0-rc.1" "1.0.0-rc.1"; then
116+
fail "equal prereleases are not newer"
117+
fi
118+
pass "equal prereleases are not newer"
119+
}
120+
121+
test_build_metadata_is_ignored() {
122+
if version_gt "1.0.0+build.9" "1.0.0+build.1"; then
123+
fail "build metadata does not order releases"
124+
fi
125+
pass "build metadata does not order releases"
126+
127+
if version_gt "1.0.0+build-1" "1.0.0"; then
128+
fail "a hyphen inside build metadata is not a prerelease"
129+
fi
130+
if version_gt "1.0.0" "1.0.0+build-1"; then
131+
fail "a hyphen inside build metadata is not a prerelease"
132+
fi
133+
pass "a hyphen inside build metadata is not a prerelease"
134+
135+
if ! version_gt "1.0.1+build-1" "1.0.0"; then
136+
fail "the core still compares under build metadata"
137+
fi
138+
pass "the core still compares under build metadata"
139+
140+
if ! version_gt "1.0.0-rc.2+b1" "1.0.0-rc.1+b99"; then
141+
fail "prerelease decides when both carry build metadata"
142+
fi
143+
pass "prerelease decides when both carry build metadata"
144+
}
145+
79146
test_target_mapping() {
80147
assert_eq "x86_64-unknown-linux-gnu" "$(detect_target "linux" "amd64")" "maps linux amd64 target"
81148
assert_eq "aarch64-unknown-linux-gnu" "$(detect_target "linux" "arm64")" "maps linux arm64 target"
@@ -822,6 +889,8 @@ test_install_binary_rejects_symlink() {
822889

823890
test_version_normalization
824891
test_version_comparison
892+
test_prerelease_precedence
893+
test_build_metadata_is_ignored
825894
test_target_mapping
826895
test_unknown_architecture_fails
827896
test_github_api_url_rejects_non_https

0 commit comments

Comments
 (0)