Skip to content
Open
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
55 changes: 47 additions & 8 deletions arcup/arcup
Original file line number Diff line number Diff line change
Expand Up @@ -261,17 +261,24 @@ download_error() {
version_gt() {
[ "$1" = "$2" ] && return 1

# Remove 'v' prefix if present
local ver1="${1#v}"
local ver2="${2#v}"
ver1="${ver1%%-*}"
ver2="${ver2%%-*}"

# Remove 'v' prefix and strip build metadata (+...) up front. SemVer ignores
# build metadata for precedence, and stripping it first prevents a '-' inside
# build metadata (e.g. 1.0.0+build-1) from being misread as a prerelease.
local v1="${1#v}"; v1="${v1%%+*}"
local v2="${2#v}"; v2="${v2%%+*}"

# Split into core "major.minor.patch" and optional prerelease.
local core1="${v1%%-*}" pre1=""
[ "$v1" = "$core1" ] || pre1="${v1#*-}"
local core2="${v2%%-*}" pre2=""
[ "$v2" = "$core2" ] || pre2="${v2#*-}"

local major1 minor1 patch1 major2 minor2 patch2
IFS=. read -r major1 minor1 patch1 <<EOF
$ver1
$core1
EOF
IFS=. read -r major2 minor2 patch2 <<EOF
$ver2
$core2
EOF

[ "$major1" -gt "$major2" ] && return 0
Expand All @@ -281,6 +288,38 @@ EOF
[ "$patch1" -gt "$patch2" ] && return 0
[ "$patch1" -lt "$patch2" ] && return 1

# Core versions are equal: apply SemVer prerelease precedence (semver.org section 11).
# A version WITHOUT a prerelease outranks the same version WITH one.
[ -z "$pre1" ] && [ -z "$pre2" ] && return 1
[ -z "$pre1" ] && return 0
[ -z "$pre2" ] && return 1

# Both have a prerelease: compare dot-separated identifiers left to right.
# `read -ra` splits on IFS without pathname (glob) expansion.
local LC_ALL=C
local -a a1 a2
IFS=. read -ra a1 <<< "$pre1"
IFS=. read -ra a2 <<< "$pre2"
local max=${#a1[@]}
[ "${#a2[@]}" -gt "$max" ] && max=${#a2[@]}
local i id1 id2
for ((i = 0; i < max; i++)); do
id1="${a1[i]-}"; id2="${a2[i]-}"
[ -z "$id1" ] && return 1 # fewer identifiers -> lower precedence
[ -z "$id2" ] && return 0 # more identifiers -> higher precedence
[ "$id1" = "$id2" ] && continue
if [[ "$id1" =~ ^[0-9]+$ ]] && [[ "$id2" =~ ^[0-9]+$ ]]; then
[ "$id1" -gt "$id2" ] && return 0
[ "$id1" -lt "$id2" ] && return 1
elif [[ "$id1" =~ ^[0-9]+$ ]]; then
return 1 # numeric identifiers rank lower than alphanumeric
elif [[ "$id2" =~ ^[0-9]+$ ]]; then
return 0
else
[[ "$id1" > "$id2" ]] && return 0
[[ "$id1" < "$id2" ]] && return 1
fi
done
return 1
}

Expand Down
82 changes: 82 additions & 0 deletions arcup/version_gt_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
#
# Tests for version_gt() in ./arcup (SemVer precedence, semver.org section 11).
#
# Usage:
# bash arcup/version_gt_test.sh
#
# It sources ./arcup with ARCUP_SKIP_MAIN=1 so only the function definitions load
# (the installer's main flow is skipped) and then exercises version_gt().

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export ARCUP_SKIP_MAIN=1
# shellcheck source=./arcup disable=SC1091
source "$HERE/arcup"
# arcup runs under `set -euo pipefail`; relax it so the harness controls flow.
set +eu +o pipefail 2>/dev/null || true

pass=0
fail=0

# assert_gt A B -> expect version_gt to report A > B (exit 0)
assert_gt() {
if version_gt "$1" "$2"; then
pass=$((pass + 1))
else
echo "FAIL: expected '$1' > '$2'"
fail=$((fail + 1))
fi
}

# assert_le A B -> expect version_gt to report NOT A > B (exit non-zero)
assert_le() {
if version_gt "$1" "$2"; then
echo "FAIL: expected NOT '$1' > '$2'"
fail=$((fail + 1))
else
pass=$((pass + 1))
fi
}

# Reported bug (#205): prerelease vs stable and prerelease vs prerelease
assert_gt "0.3.0" "0.3.0-rc.1"
assert_le "0.3.0-rc.1" "0.3.0"
assert_gt "0.3.0-rc.2" "0.3.0-rc.1"
assert_le "0.3.0-rc.1" "0.3.0-rc.2"

# Core major.minor.patch comparisons (with and without 'v' prefix)
assert_gt "1.0.0" "0.9.9"
assert_le "0.9.9" "1.0.0"
assert_gt "v1.2.4" "v1.2.3"
assert_le "1.2.3" "1.2.3"
assert_gt "1.3.0" "1.2.9"
assert_gt "2.0.0-rc.1" "1.9.9"

# Full SemVer 11.4 precedence chain:
# 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta
# < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0
assert_gt "1.0.0-alpha.1" "1.0.0-alpha"
assert_gt "1.0.0-alpha.beta" "1.0.0-alpha.1"
assert_gt "1.0.0-beta" "1.0.0-alpha.beta"
assert_gt "1.0.0-beta.2" "1.0.0-beta"
assert_gt "1.0.0-beta.11" "1.0.0-beta.2"
assert_gt "1.0.0-rc.1" "1.0.0-beta.11"
assert_gt "1.0.0" "1.0.0-rc.1"

# Build metadata is ignored for precedence
assert_le "1.0.0+build.9" "1.0.0+build.1"
assert_le "1.0.0" "1.0.0+build.1"

# Build metadata containing a hyphen must NOT be treated as a prerelease
assert_le "1.0.0+build-123" "1.0.0"
assert_le "1.0.0" "1.0.0+build-123"
assert_gt "1.0.1+build-1" "1.0.0"

# Prerelease combined with build metadata: build metadata is ignored, so
# precedence is decided purely by the prerelease identifiers.
assert_le "1.0.0-rc.1+build-123" "1.0.0-rc.1"
assert_le "1.0.0-rc.1" "1.0.0-rc.1+build-123"
assert_gt "1.0.0-rc.2+build-1" "1.0.0-rc.1+build-99"

echo "version_gt: ${pass} passed, ${fail} failed"
[ "${fail}" -eq 0 ]