diff --git a/eng/tool-version-lookup.cmd b/eng/tool-version-lookup.cmd new file mode 100644 index 0000000000..aa911e6f35 --- /dev/null +++ b/eng/tool-version-lookup.cmd @@ -0,0 +1,3 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0tool-version-lookup.ps1""" %*" +exit /b %ErrorLevel% diff --git a/eng/tool-version-lookup.ps1 b/eng/tool-version-lookup.ps1 new file mode 100644 index 0000000000..c9e8003f3c --- /dev/null +++ b/eng/tool-version-lookup.ps1 @@ -0,0 +1,386 @@ +<# +.SYNOPSIS + Correlates diagnostics tool versions with git commits and build dates. + +.DESCRIPTION + Daily builds of dotnet-trace, dotnet-dump, dotnet-counters, and dotnet-gcdump + are published to the dotnet-tools NuGet feed with stable version numbers like + 10.0.715501. The patch component encodes the build date using the Arcade SDK + formula, and the informational version (shown by --version) includes the commit + SHA after '+'. + +.EXAMPLE + eng\tool-version-lookup.ps1 decode 10.0.715501 + + Decodes the version to show its build date and OfficialBuildId. + +.EXAMPLE + eng\tool-version-lookup.ps1 decode "10.0.715501+86150ac0275658c5efc6035269499a86dee68e54" + + Decodes the version and resolves the embedded commit SHA. + +.EXAMPLE + eng\tool-version-lookup.ps1 before bda9ea7b + + Finds the latest daily build version published before a given commit. + +.EXAMPLE + eng\tool-version-lookup.ps1 after 18cf9d1 -Tool dotnet-dump + + Finds the earliest daily build version published after a given commit. + +.EXAMPLE + eng\tool-version-lookup.ps1 verify 10.0.711601 + + Checks whether a specific version exists on the dotnet-tools feed. + +.EXAMPLE + eng\tool-version-lookup.ps1 list -Last 5 + + Lists the 5 most recent daily build versions on the feed. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$true, Position=0)] + [ValidateSet("decode", "before", "after", "verify", "list")] + [string]$Command, + + [Parameter(Position=1)] + [string]$Ref, + + [string]$Date, + + [ValidateSet("dotnet-trace", "dotnet-dump", "dotnet-counters", "dotnet-gcdump")] + [string]$Tool = "dotnet-trace", + + [string]$MajorMinor, + + [int]$Last = 10 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# NuGet V3 flat container API — the simplest endpoint for listing all versions of a package. +$FeedFlat2Base = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/flat2" +# Full feed URL used in the 'dotnet tool update --add-source' install command printed for users. +$FeedBase = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" +# Arcade SDK epoch constant from Version.BeforeCommonTargets.targets. +# If Arcade changes this value, it must be updated here as well. +$VersionBaseShortDate = 19000 + +# The Arcade SDK (Version.BeforeCommonTargets.targets) encodes the OfficialBuildId +# (format: yyyyMMdd.revision) into the patch component of the version number: +# SHORT_DATE = YY*1000 + MM*50 + DD +# PATCH = (SHORT_DATE - VersionBaseShortDate) * 100 + revision +# MM*50 is used instead of MM*100 because months max at 12 (12*50=600), leaving +# room for days 1-31 without overflow into the next month's range. +function Decode-Patch([int]$Patch) { + [int]$revision = $Patch % 100 + [int]$shortDate = [math]::Floor($Patch / 100) + $VersionBaseShortDate + [int]$yy = [math]::Floor($shortDate / 1000) + [int]$remainder = $shortDate - ($yy * 1000) + [int]$mm = [math]::Floor($remainder / 50) + [int]$dd = $remainder - ($mm * 50) + return @{ Year = $yy; Month = $mm; Day = $dd; Revision = $revision } +} + +function Encode-Patch([int]$Year, [int]$Month, [int]$Day, [int]$Revision = 1) { + [int]$shortDate = $Year * 1000 + $Month * 50 + $Day + return ($shortDate - $VersionBaseShortDate) * 100 + $Revision +} + +function Format-BuildDate([int]$Patch) { + $d = Decode-Patch $Patch + return "20{0:D2}-{1:D2}-{2:D2} (rev {3})" -f $d.Year, $d.Month, $d.Day, $d.Revision +} + +# Strips the "+commitsha" metadata suffix and splits "Major.Minor.Patch". +function Parse-ToolVersion([string]$Version) { + $clean = $Version.Split("+")[0] + $parts = $clean.Split(".") + if ($parts.Length -ne 3) { return $null } + try { + return @{ + Major = [int]$parts[0] + Minor = [int]$parts[1] + Patch = [int]$parts[2] + } + } + catch { + return $null + } +} + +function Get-FeedVersions([string]$ToolName) { + $url = "$FeedFlat2Base/$ToolName/index.json" + try { + $response = Invoke-RestMethod -Uri $url + return $response.versions + } + catch { + Write-Error "Failed to query feed for ${ToolName}: $_" + exit 1 + } +} + +# Auto-detects the active major.minor series by finding the version with the +# highest patch number (most recent build), since the feed contains versions +# from multiple release branches (e.g., 6.0.x, 9.0.x, 10.0.x). +function Get-DetectedMajorMinor([string[]]$Versions) { + $bestPatch = -1 + $bestPrefix = $null + foreach ($v in $Versions) { + $parsed = Parse-ToolVersion $v + if ($parsed -and $parsed.Patch -gt $bestPatch) { + $bestPatch = $parsed.Patch + $bestPrefix = "$($parsed.Major).$($parsed.Minor)" + } + } + return $bestPrefix +} + +# Restrict to hex SHAs to prevent shell injection via git arguments. +function Validate-CommitRef([string]$CommitRef) { + if ($CommitRef -notmatch '^[a-fA-F0-9]{4,40}$') { + Write-Error "Invalid commit ref: '$CommitRef'. Expected a hex SHA." + exit 1 + } +} + +function Get-CommitDate([string]$CommitRef) { + Validate-CommitRef $CommitRef + $result = git log -1 --format="%cI" $CommitRef 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Error "Could not find commit $CommitRef" + exit 1 + } + return [DateTimeOffset]::Parse($result.Trim()) +} + +function Get-CommitInfo([string]$CommitRef) { + Validate-CommitRef $CommitRef + $result = git log -1 --format="%h %ai %s" $CommitRef 2>&1 + if ($LASTEXITCODE -ne 0) { + return "(could not resolve $CommitRef)" + } + return $result.Trim() +} + +function Resolve-MajorMinor([string[]]$Versions) { + if ($MajorMinor) { return $MajorMinor } + $detected = Get-DetectedMajorMinor $Versions + if (-not $detected) { + Write-Error "Could not determine major.minor version from feed." + exit 1 + } + return $detected +} + +function Invoke-Decode { + if (-not $Ref) { + Write-Error "Usage: tool-version-lookup.ps1 decode " + exit 1 + } + $parsed = Parse-ToolVersion $Ref + if (-not $parsed) { + Write-Error "Could not parse version '$Ref'." + exit 1 + } + + $d = Decode-Patch $parsed.Patch + Write-Host "Version: $Ref" + Write-Host ("Build date: 20{0:D2}-{1:D2}-{2:D2}" -f $d.Year, $d.Month, $d.Day) + Write-Host "Build revision: $($d.Revision)" + Write-Host ("OfficialBuildId: 20{0:D2}{1:D2}{2:D2}.{3}" -f $d.Year, $d.Month, $d.Day, $d.Revision) + + if ($Ref.Contains("+")) { + $sha = $Ref.Split("+")[1] + Write-Host "Commit SHA: $sha" + $info = Get-CommitInfo $sha + Write-Host "Commit: $info" + } +} + +function Invoke-BeforeOrAfter([bool]$IsBefore) { + $direction = if ($IsBefore) { "before" } else { "after" } + $label = if ($IsBefore) { "latest" } else { "earliest" } + + if ($Date) { + $targetDate = [DateTimeOffset]::Parse($Date) + Write-Host "Finding $label $Tool version built $direction $Date..." + } + elseif ($Ref) { + $info = Get-CommitInfo $Ref + $targetDate = Get-CommitDate $Ref + Write-Host "Commit: $info" + Write-Host "Date: $($targetDate.ToString('yyyy-MM-dd HH:mm zzz'))" + Write-Host "" + Write-Host "Finding $label $Tool version built $direction $($targetDate.ToString('yyyy-MM-dd'))..." + } + else { + Write-Error "Usage: tool-version-lookup.ps1 $direction [-Date yyyy-MM-dd]" + exit 1 + } + + $targetYY = $targetDate.Year - 2000 + $targetMM = $targetDate.Month + $targetDD = $targetDate.Day + + # For "before": use revision 0 so any build from that day is excluded + # (the build may or may not include the commit depending on timing). + # For "after": use revision 99 so any build from that day is excluded. + if ($IsBefore) { + $targetPatch = Encode-Patch $targetYY $targetMM $targetDD -Revision 0 + } + else { + $targetPatch = Encode-Patch $targetYY $targetMM $targetDD -Revision 99 + } + + $versions = Get-FeedVersions $Tool + $mm = Resolve-MajorMinor $versions + + $candidates = @() + foreach ($v in $versions) { + if (-not $v.StartsWith("$mm.")) { continue } + $parsed = Parse-ToolVersion $v + if (-not $parsed) { continue } + if ($IsBefore -and $parsed.Patch -lt $targetPatch) { + $candidates += @{ Version = $v; Patch = $parsed.Patch } + } + elseif (-not $IsBefore -and $parsed.Patch -gt $targetPatch) { + $candidates += @{ Version = $v; Patch = $parsed.Patch } + } + } + + if ($candidates.Length -eq 0) { + Write-Host "" + if (-not $IsBefore) { + Write-Host "The fix may not have been published yet." -ForegroundColor Yellow + } + Write-Error "No $Tool $mm.x versions found $direction that date." + exit 1 + } + + # @() wrapper is required: Sort-Object unwraps single-element arrays in PowerShell. + $candidates = @($candidates | Sort-Object { $_.Patch }) + + if ($IsBefore) { + $recommended = $candidates[$candidates.Length - 1] + $othersLabel = "Other recent options:" + if ($candidates.Length -ge 2) { + $start = [math]::Max(0, $candidates.Length - 4) + $end = $candidates.Length - 2 + $others = @($candidates[$start..$end]) + } + else { + $others = @() + } + } + else { + $recommended = $candidates[0] + $othersLabel = "Other options (newer):" + if ($candidates.Length -ge 2) { + $end = [math]::Min(3, $candidates.Length - 1) + $others = @($candidates[1..$end]) + } + else { + $others = @() + } + } + + $feedUrl = $FeedBase + Write-Host "" + Write-Host "Recommended version: $($recommended.Version)" + Write-Host " Built: $(Format-BuildDate $recommended.Patch)" + Write-Host "" + Write-Host "Install with:" + Write-Host " dotnet tool update $Tool -g --version $($recommended.Version) ``" + Write-Host " --add-source $feedUrl" + + if ($others -and $others.Length -gt 0) { + Write-Host "" + Write-Host $othersLabel + foreach ($c in $others) { + if ($c) { + Write-Host (" {0,-20} built {1}" -f $c.Version, (Format-BuildDate $c.Patch)) + } + } + } +} + +function Invoke-List { + $versions = Get-FeedVersions $Tool + $prefix = if ($MajorMinor) { $MajorMinor } else { Get-DetectedMajorMinor $versions } + + $filtered = $versions | Where-Object { $_.StartsWith("$prefix.") } + $filtered = $filtered | Sort-Object { (Parse-ToolVersion $_).Patch } + $show = $filtered | Select-Object -Last $Last + + Write-Host "Recent $Tool $prefix.x versions on dotnet-tools feed:" + Write-Host "" + Write-Host ("{0,-20} {1,-16} {2}" -f "Version", "Build Date", "OfficialBuildId") + Write-Host ("{0,-20} {1,-16} {2}" -f ("-" * 20), ("-" * 16), ("-" * 15)) + foreach ($v in $show) { + $parsed = Parse-ToolVersion $v + if ($parsed) { + $d = Decode-Patch $parsed.Patch + $dateStr = "20{0:D2}-{1:D2}-{2:D2}" -f $d.Year, $d.Month, $d.Day + $buildId = "20{0:D2}{1:D2}{2:D2}.{3}" -f $d.Year, $d.Month, $d.Day, $d.Revision + Write-Host ("{0,-20} {1,-16} {2}" -f $v, $dateStr, $buildId) + } + } +} + +function Invoke-Verify { + if (-not $Ref) { + Write-Error "Usage: tool-version-lookup.ps1 verify " + exit 1 + } + $versions = Get-FeedVersions $Tool + + if ($versions -contains $Ref) { + $parsed = Parse-ToolVersion $Ref + if ($parsed) { + Write-Host "[OK] $Tool $Ref exists on the feed" + Write-Host " Built: $(Format-BuildDate $parsed.Patch)" + } + else { + Write-Host "[OK] $Tool $Ref exists on the feed" + } + } + else { + Write-Host "[NOT FOUND] $Tool $Ref NOT found on the feed" -ForegroundColor Red + + $parsed = Parse-ToolVersion $Ref + if ($parsed) { + $nearby = @() + foreach ($v in $versions) { + $vp = Parse-ToolVersion $v + if ($vp -and $vp.Major -eq $parsed.Major -and $vp.Minor -eq $parsed.Minor) { + if ([math]::Abs($vp.Patch - $parsed.Patch) -lt 500) { + $nearby += $v + } + } + } + if ($nearby.Length -gt 0) { + Write-Host "" + Write-Host " Nearby versions:" + $nearby | Select-Object -Last 5 | ForEach-Object { + $vp = Parse-ToolVersion $_ + Write-Host (" {0,-20} built {1}" -f $_, (Format-BuildDate $vp.Patch)) + } + } + } + exit 1 + } +} + +switch ($Command) { + "decode" { Invoke-Decode } + "before" { Invoke-BeforeOrAfter -IsBefore $true } + "after" { Invoke-BeforeOrAfter -IsBefore $false } + "verify" { Invoke-Verify } + "list" { Invoke-List } +} diff --git a/eng/tool-version-lookup.sh b/eng/tool-version-lookup.sh new file mode 100644 index 0000000000..38a103344a --- /dev/null +++ b/eng/tool-version-lookup.sh @@ -0,0 +1,411 @@ +#!/usr/bin/env bash +# Correlates diagnostics tool versions with git commits and build dates. +# +# Usage: +# eng/tool-version-lookup.sh decode 10.0.715501 +# eng/tool-version-lookup.sh before [--tool dotnet-trace] +# eng/tool-version-lookup.sh after [--tool dotnet-trace] +# eng/tool-version-lookup.sh verify 10.0.711601 [--tool dotnet-trace] +# eng/tool-version-lookup.sh list [--tool dotnet-trace] [--last 10] + +set -euo pipefail + +# NuGet V3 flat container API — the simplest endpoint for listing all versions of a package. +FEED_FLAT2_BASE="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/flat2" +# Full feed URL used in the 'dotnet tool update --add-source' install command printed for users. +FEED_URL="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" +# Arcade SDK epoch constant from Version.BeforeCommonTargets.targets. +# If Arcade changes this value, it must be updated here as well. +VERSION_BASE_SHORT_DATE=19000 +VALID_TOOLS="dotnet-trace dotnet-dump dotnet-counters dotnet-gcdump" + +# Defaults +TOOL="dotnet-trace" +MAJOR_MINOR="" +LAST_COUNT=10 + +die() { echo "Error: $*" >&2; exit 1; } + +validate_tool() { + local tool="$1" + for valid in $VALID_TOOLS; do + if [ "$tool" = "$valid" ]; then return 0; fi + done + die "Invalid tool '$tool'. Valid options: $VALID_TOOLS" +} + +# Restrict to hex SHAs to prevent shell injection via git arguments. +validate_commit_ref() { + local ref="$1" + if ! printf '%s\n' "$ref" | grep -qE '^[a-fA-F0-9]{4,40}$'; then + die "Invalid commit ref: '$ref'. Expected a hex SHA." + fi +} + +# The Arcade SDK (Version.BeforeCommonTargets.targets) encodes the OfficialBuildId +# (format: yyyyMMdd.revision) into the patch component of the version number: +# SHORT_DATE = YY*1000 + MM*50 + DD +# PATCH = (SHORT_DATE - VersionBaseShortDate) * 100 + revision +# MM*50 is used instead of MM*100 because months max at 12 (12*50=600), leaving +# room for days 1-31 without overflow into the next month's range. +decode_patch() { + local patch=$1 + PATCH_REV=$((patch % 100)) + local short_date=$((patch / 100 + VERSION_BASE_SHORT_DATE)) + PATCH_YY=$((short_date / 1000)) + local remainder=$((short_date - PATCH_YY * 1000)) + PATCH_MM=$((remainder / 50)) + PATCH_DD=$((remainder - PATCH_MM * 50)) +} + +encode_patch() { + local yy=$1 mm=$2 dd=$3 rev=${4:-1} + local short_date=$((yy * 1000 + mm * 50 + dd)) + echo $(( (short_date - VERSION_BASE_SHORT_DATE) * 100 + rev )) +} + +format_build_date() { + decode_patch "$1" + printf "20%02d-%02d-%02d (rev %d)" "$PATCH_YY" "$PATCH_MM" "$PATCH_DD" "$PATCH_REV" +} + +# Strips the "+commitsha" metadata suffix and splits "Major.Minor.Patch". +parse_version() { + local ver="${1%%+*}" + VER_MAJOR="" VER_MINOR="" VER_PATCH="" + IFS='.' read -r VER_MAJOR VER_MINOR VER_PATCH <<< "$ver" + if [ -z "$VER_MAJOR" ] || [ -z "$VER_MINOR" ] || [ -z "$VER_PATCH" ]; then + return 1 + fi + # Validate they're integers + case "$VER_MAJOR$VER_MINOR$VER_PATCH" in + *[!0-9]*) return 1 ;; + esac + return 0 +} + +get_feed_versions() { + local tool="$1" + local url="$FEED_FLAT2_BASE/$tool/index.json" + if command -v jq >/dev/null 2>&1; then + curl -sfL "$url" | jq -r '.versions[]' + else + # Fallback: the flat2 response is {"versions":["x.y.z",...]}, simple enough to parse. + curl -sfL "$url" | tr ',' '\n' | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"' + fi +} + +# Auto-detects the active major.minor series by finding the version with the +# highest patch number (most recent build), since the feed contains versions +# from multiple release branches (e.g., 6.0.x, 9.0.x, 10.0.x). +detect_major_minor() { + local best_patch=-1 + local best_prefix="" + local v + while IFS= read -r v; do + if parse_version "$v"; then + if [ "$VER_PATCH" -gt "$best_patch" ]; then + best_patch=$VER_PATCH + best_prefix="$VER_MAJOR.$VER_MINOR" + fi + fi + done + echo "$best_prefix" +} + +resolve_major_minor() { + if [ -n "$MAJOR_MINOR" ]; then + echo "$MAJOR_MINOR" + return + fi + local versions="$1" + echo "$versions" | detect_major_minor +} + +get_commit_date_iso() { + validate_commit_ref "$1" + local result + result=$(git log -1 --format="%cI" "$1" 2>/dev/null) || die "Could not find commit $1" + echo "$result" +} + +get_commit_info() { + validate_commit_ref "$1" + git log -1 --format="%h %ai %s" "$1" 2>/dev/null || echo "(could not resolve $1)" +} + +# Extract YYYY MM DD from ISO date string +parse_iso_date() { + local iso="$1" + ISO_YEAR="${iso:0:4}" + ISO_MONTH="${iso:5:2}" + ISO_DAY="${iso:8:2}" + # Remove leading zeros for arithmetic + ISO_MONTH=$((10#$ISO_MONTH)) + ISO_DAY=$((10#$ISO_DAY)) +} + +cmd_decode() { + local version="$1" + parse_version "$version" || die "Could not parse version '$version'." + + decode_patch "$VER_PATCH" + echo "Version: $version" + printf "Build date: 20%02d-%02d-%02d\n" "$PATCH_YY" "$PATCH_MM" "$PATCH_DD" + echo "Build revision: $PATCH_REV" + printf "OfficialBuildId: 20%02d%02d%02d.%d\n" "$PATCH_YY" "$PATCH_MM" "$PATCH_DD" "$PATCH_REV" + + if [[ "$version" == *"+"* ]]; then + local sha="${version#*+}" + echo "Commit SHA: $sha" + local info + info=$(get_commit_info "$sha") + echo "Commit: $info" + fi +} + +cmd_before_or_after() { + local is_before="$1" + local ref="$2" + local direction label + + if [ "$is_before" = "true" ]; then + direction="before" + label="latest" + else + direction="after" + label="earliest" + fi + + local target_iso + if [ -n "$DATE_ARG" ]; then + if ! echo "$DATE_ARG" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then + die "Invalid date format '$DATE_ARG'. Expected YYYY-MM-DD." + fi + target_iso="${DATE_ARG}T00:00:00+00:00" + echo "Finding $label $TOOL version built $direction $DATE_ARG..." + elif [ -n "$ref" ]; then + local info + info=$(get_commit_info "$ref") + target_iso=$(get_commit_date_iso "$ref") + echo "Commit: $info" + echo "Date: $target_iso" + echo "" + parse_iso_date "$target_iso" + printf "Finding %s %s version built %s %04d-%02d-%02d...\n" "$label" "$TOOL" "$direction" "$ISO_YEAR" "$ISO_MONTH" "$ISO_DAY" + else + die "Usage: tool-version-lookup.sh $direction [--date YYYY-MM-DD]" + fi + + parse_iso_date "$target_iso" + local target_yy=$((ISO_YEAR - 2000)) + + # For "before": use revision 0 so any build from that day is excluded + # (the build may or may not include the commit depending on timing). + # For "after": use revision 99 so any build from that day is excluded. + local target_patch + if [ "$is_before" = "true" ]; then + target_patch=$(encode_patch "$target_yy" "$ISO_MONTH" "$ISO_DAY" 0) + else + target_patch=$(encode_patch "$target_yy" "$ISO_MONTH" "$ISO_DAY" 99) + fi + + local versions + versions=$(get_feed_versions "$TOOL") + local mm + mm=$(resolve_major_minor "$versions") + [ -n "$mm" ] || die "Could not determine major.minor version from feed." + + local candidates=() + local v + while IFS= read -r v; do + if [[ "$v" != "$mm."* ]]; then continue; fi + if parse_version "$v"; then + if [ "$is_before" = "true" ] && [ "$VER_PATCH" -lt "$target_patch" ]; then + candidates+=("$VER_PATCH:$v") + elif [ "$is_before" = "false" ] && [ "$VER_PATCH" -gt "$target_patch" ]; then + candidates+=("$VER_PATCH:$v") + fi + fi + done <<< "$versions" + + if [ ${#candidates[@]} -eq 0 ]; then + echo "" + if [ "$is_before" = "false" ]; then + echo "The fix may not have been published yet." >&2 + fi + die "No $TOOL $mm.x versions found $direction that date." + fi + + # Sort candidates by patch number + IFS=$'\n' sorted=($(printf '%s\n' "${candidates[@]}" | sort -t: -k1 -n)); unset IFS + + local recommended recommended_patch + if [ "$is_before" = "true" ]; then + recommended="${sorted[-1]}" + else + recommended="${sorted[0]}" + fi + recommended_patch="${recommended%%:*}" + recommended_ver="${recommended#*:}" + + echo "" + echo "Recommended version: $recommended_ver" + echo " Built: $(format_build_date "$recommended_patch")" + echo "" + echo "Install with:" + echo " dotnet tool update $TOOL -g --version $recommended_ver \\" + echo " --add-source $FEED_URL" + + if [ ${#sorted[@]} -ge 2 ]; then + echo "" + if [ "$is_before" = "true" ]; then + echo "Other recent options:" + local start=$(( ${#sorted[@]} - 4 )) + [ "$start" -lt 0 ] && start=0 + local end=$(( ${#sorted[@]} - 2 )) + for ((i=start; i<=end; i++)); do + local entry="${sorted[$i]}" + local p="${entry%%:*}" + local ver="${entry#*:}" + printf " %-20s built %s\n" "$ver" "$(format_build_date "$p")" + done + else + echo "Other options (newer):" + local end=$(( ${#sorted[@]} - 1 )) + [ "$end" -gt 3 ] && end=3 + for ((i=1; i<=end; i++)); do + local entry="${sorted[$i]}" + local p="${entry%%:*}" + local ver="${entry#*:}" + printf " %-20s built %s\n" "$ver" "$(format_build_date "$p")" + done + fi + fi +} + +cmd_verify() { + local version="$1" + local versions + versions=$(get_feed_versions "$TOOL") + + if echo "$versions" | grep -qxF "$version"; then + parse_version "$version" + echo "[OK] $TOOL $version exists on the feed" + echo " Built: $(format_build_date "$VER_PATCH")" + else + echo "[NOT FOUND] $TOOL $version NOT found on the feed" >&2 + + if parse_version "$version"; then + local target_major=$VER_MAJOR target_minor=$VER_MINOR target_patch=$VER_PATCH + local nearby=() + while IFS= read -r v; do + if parse_version "$v"; then + if [ "$VER_MAJOR" -eq "$target_major" ] && [ "$VER_MINOR" -eq "$target_minor" ]; then + local diff=$(( VER_PATCH - target_patch )) + [ "$diff" -lt 0 ] && diff=$(( -diff )) + if [ "$diff" -lt 500 ]; then + nearby+=("$v") + fi + fi + fi + done <<< "$versions" + + if [ ${#nearby[@]} -gt 0 ]; then + echo "" >&2 + echo " Nearby versions:" >&2 + printf '%s\n' "${nearby[@]}" | tail -5 | while IFS= read -r v; do + parse_version "$v" + printf " %-20s built %s\n" "$v" "$(format_build_date "$VER_PATCH")" >&2 + done + fi + fi + exit 1 + fi +} + +cmd_list() { + local versions + versions=$(get_feed_versions "$TOOL") + local prefix + prefix=$(resolve_major_minor "$versions") + + local filtered + filtered=$(echo "$versions" | grep "^${prefix}\." | while IFS= read -r v; do + if parse_version "$v"; then + echo "$VER_PATCH:$v" + fi + done | sort -t: -k1 -n | tail -"$LAST_COUNT") + + echo "Recent $TOOL $prefix.x versions on dotnet-tools feed:" + echo "" + printf "%-20s %-16s %s\n" "Version" "Build Date" "OfficialBuildId" + printf "%-20s %-16s %s\n" "--------------------" "----------------" "---------------" + + echo "$filtered" | while IFS=: read -r patch ver; do + decode_patch "$patch" + printf "%-20s 20%02d-%02d-%02d 20%02d%02d%02d.%d\n" \ + "$ver" "$PATCH_YY" "$PATCH_MM" "$PATCH_DD" \ + "$PATCH_YY" "$PATCH_MM" "$PATCH_DD" "$PATCH_REV" + done +} + +# --- Argument parsing --- +DATE_ARG="" +POSITIONAL=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --tool) TOOL="$2"; validate_tool "$TOOL"; shift 2 ;; + --date) DATE_ARG="$2"; shift 2 ;; + --major-minor) MAJOR_MINOR="$2"; shift 2 ;; + --last) LAST_COUNT="$2"; shift 2 ;; + --help|-h) + echo "Usage: tool-version-lookup.sh [ref] [options]" + echo "" + echo "Commands:" + echo " decode Decode a tool version to its build date" + echo " before Find latest feed version built before a commit/date" + echo " after Find earliest feed version built after a commit/date" + echo " verify Check if a version exists on the feed" + echo " list List recent versions on the feed" + echo "" + echo "Options:" + echo " --tool Tool name (default: dotnet-trace)" + echo " --date Use date instead of commit ref (before/after)" + echo " --major-minor Filter to specific major.minor (auto-detected)" + echo " --last Number of versions to show in list (default: 10)" + exit 0 + ;; + *) POSITIONAL+=("$1"); shift ;; + esac +done + +COMMAND="${POSITIONAL[0]:-}" +REF_ARG="${POSITIONAL[1]:-}" + +case "$COMMAND" in + decode) + [ -n "$REF_ARG" ] || die "Usage: tool-version-lookup.sh decode " + cmd_decode "$REF_ARG" + ;; + before) + cmd_before_or_after "true" "$REF_ARG" + ;; + after) + cmd_before_or_after "false" "$REF_ARG" + ;; + verify) + [ -n "$REF_ARG" ] || die "Usage: tool-version-lookup.sh verify " + cmd_verify "$REF_ARG" + ;; + list) + cmd_list + ;; + "") + die "No command specified. Use --help for usage." + ;; + *) + die "Unknown command '$COMMAND'. Use --help for usage." + ;; +esac