Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Semantic Versioning 2.0.0 #60

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,15 +343,15 @@ In order to help working with versions, function `Get-Version` can be called in
- To force the update of the single stream, use `IncludeStream` parameter. To do so via commit message, use `[AU package\stream]` syntax.

```powershell
PS> Get-Version 'v1.3.2.7rc1'
PS> Get-Version 'v1.3.2.7rc.1'

Version Prerelease BuildMetadata
------- ---------- -------------
1.3.2.7 rc1

PS> $version = Get-Version '1.3.2-beta2+5'
PS> $version = Get-Version -SemVer V2 '1.3.2-beta.2+5'
PS> $version.ToString(2) + ' => ' + $version.ToString()
1.3 => 1.3.2-beta2+5
1.3 => 1.3.2-beta.2+5
```

### WhatIf
Expand Down
149 changes: 100 additions & 49 deletions src/Private/AUVersion.ps1
Original file line number Diff line number Diff line change
@@ -1,71 +1,131 @@
enum SemVer {
V1
V2
EnhancedV2
}

class AUVersion : System.IComparable {
[version] $Version
[string] $Prerelease
[string] $BuildMetadata

AUVersion([version] $version, [string] $prerelease, [string] $buildMetadata) {
hidden AUVersion([version] $version, [string] $prerelease, [string] $buildMetadata) {
if (!$version) { throw 'Version cannot be null.' }
$this.Version = $version
$this.Prerelease = $prerelease
$this.Version = [AUVersion]::NormalizeVersion($version)
$this.Prerelease = [AUVersion]::NormalizePrerelease($prerelease) -join '.'
$this.BuildMetadata = $buildMetadata
}

AUVersion($input) {
if (!$input) { throw 'Input cannot be null.' }
$v = [AUVersion]::Parse($input -as [string])
$this.Version = $v.Version
$this.Prerelease = $v.Prerelease
AUVersion($value) {
if (!$value) { throw 'Input cannot be null.' }
$v = [AUVersion]::Parse($value -as [string])
$this.Version = $v.Version
$this.Prerelease = $v.Prerelease
$this.BuildMetadata = $v.BuildMetadata
}

static [AUVersion] Parse([string] $input) { return [AUVersion]::Parse($input, $true) }
static [AUVersion] Parse([string] $value) {
return [AUVersion]::Parse($value, $true)
}

static [AUVersion] Parse([string] $input, [bool] $strict) {
if (!$input) { throw 'Version cannot be null.' }
$reference = [ref] $null
if (![AUVersion]::TryParse($input, $reference, $strict)) { throw "Invalid version: $input." }
return $reference.Value
static [AUVersion] Parse([string] $value, [bool] $strict) {
return [AUVersion]::Parse($value, $strict, [SemVer]::V2)
}

static [bool] TryParse([string] $input, [ref] $result) { return [AUVersion]::TryParse($input, $result, $true) }
static [AUVersion] Parse([string] $value, [bool] $strict, [SemVer] $semver) {
if (!$value) { throw 'Version cannot be null.' }
$v = [ref] $null
if (![AUVersion]::TryParse($value, $v, $strict, $semver)) {
throw "Invalid SemVer $semver version: `"$value`"."
}
return $v.Value
}

static [bool] TryParse([string] $value, [ref] $result) {
return [AUVersion]::TryParse($value, $result, $true)
}

static [bool] TryParse([string] $value, [ref] $result, [bool] $strict) {
return [AUVersion]::TryParse($value, $result, $strict, [SemVer]::V2)
}

static [bool] TryParse([string] $input, [ref] $result, [bool] $strict) {
static [bool] TryParse([string] $value, [ref] $result, [bool] $strict, [SemVer] $semver) {
$result.Value = [AUVersion] $null
if (!$input) { return $false }
if (!$value) { return $false }
$pattern = [AUVersion]::GetPattern($strict)
if ($input -notmatch $pattern) { return $false }
$reference = [ref] $null
if (![version]::TryParse($Matches['version'], $reference)) { return $false }
$pr = $Matches['prerelease']
$bm = $Matches['buildMetadata']
if ($pr -and !$strict) { $pr = $pr.Replace(' ', '.') }
if ($bm -and !$strict) { $bm = $bm.Replace(' ', '.') }
# for now, chocolatey does only support SemVer v1 (no dot separated identifiers in pre-release):
if ($pr -and $strict -and $pr -like '*.*') { return $false }
if ($bm -and $strict -and $bm -like '*.*') { return $false }
if ($pr) { $pr = $pr.Replace('.', '') }
if ($bm) { $bm = $bm.Replace('.', '') }
#
$result.Value = [AUVersion]::new($reference.Value, $pr, $bm)
if ($value -notmatch $pattern) { return $false }
$v = [ref] $null
if (![version]::TryParse($Matches.version, $v)) { return $false }
$pr = [ref] $null
if (![AUVersion]::TryRefineIdentifiers($Matches.prerelease, $pr, $strict, $semver)) { return $false }
$bm = [ref] $null
if (![AUVersion]::TryRefineIdentifiers($Matches.buildMetadata, $bm, $strict, $semver)) { return $false }
$result.Value = [AUVersion]::new($v.Value, $pr.Value, $bm.Value)
return $true
}

hidden static [version] NormalizeVersion([version] $value) {
if ($value.Build -eq -1) {
return [version] "$value.0"
}
if ($value.Revision -eq 0) {
return [version] $value.ToString(3)
}
return $value
}

hidden static [object[]] NormalizePrerelease([string] $value) {
$result = @()
if ($value) {
$value -split '\.' | ForEach-Object {
# if identifier is exclusively numeric, cast it to an int
if ($_ -match '^\d+$') {
$result += [int] $_
} else {
$result += $_
}
}
}
return $result
}

hidden static [string] GetPattern([bool] $strict) {
$versionPattern = '(?<version>\d+(?:\.\d+){1,3})'
if ($strict) {
$identifierPattern = "[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*"
return "^$versionPattern(?:-(?<prerelease>$identifierPattern))?(?:\+(?<buildMetadata>$identifierPattern))?`$"
} else {
$identifierPattern = "[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+| \d+)*"
return "$versionPattern(?:[- ]*(?<prerelease>$identifierPattern))?(?:[+ *](?<buildMetadata>$identifierPattern))?"
$identifierPattern = "[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+| +\d+)*"
return "$versionPattern(?:(?:-| *)(?<prerelease>$identifierPattern))?(?:(?:\+| *)(?<buildMetadata>$identifierPattern))?"
}
}

hidden static [bool] TryRefineIdentifiers([string] $value, [ref] $result, [bool] $strict, [SemVer] $semver) {
$result.Value = [string] ''
if (!$value) { return $true }
if (!$strict) { $value = $value -replace ' +', '.' }
if ($semver -eq [SemVer]::V1) {
# SemVer1 means no dot-separated identifiers
if ($strict -and $value -match '\.') { return $false }
$value = $value.Replace('.', '')
} elseif ($semver -eq [SemVer]::EnhancedV2) {
# Try to improve a SemVer1 version into a SemVer2 one
# e.g. 1.24.0-beta2 becomes 1.24.0-beta.2
if ($value -match '^(?<identifier>[A-Za-z-]+)(?<digits>\d+)$') {
$value = '{0}.{1}' -f $Matches.identifier, $Matches.digits
}
}
$result.Value = $value
return $true
}

[AUVersion] WithVersion([version] $version) { return [AUVersion]::new($version, $this.Prerelease, $this.BuildMetadata) }
[AUVersion] WithVersion([version] $version) {
return [AUVersion]::new($version, $this.Prerelease, $this.BuildMetadata)
}

[int] CompareTo($obj) {
if ($obj -eq $null) { return 1 }
if ($obj -isnot [AUVersion]) { throw "AUVersion expected: $($obj.GetType())" }
if ($null -eq $obj) { return 1 }
if ($obj -isnot [AUVersion]) { throw "[AUVersion] expected, got [$($obj.GetType())]." }
$t = $this.GetParts()
$o = $obj.GetParts()
for ($i = 0; $i -lt $t.Length -and $i -lt $o.Length; $i++) {
Expand All @@ -89,8 +149,8 @@ class AUVersion : System.IComparable {

[string] ToString() {
$result = $this.Version.ToString()
if ($this.Prerelease) { $result += "-$($this.Prerelease)" }
if ($this.BuildMetadata) { $result += "+$($this.BuildMetadata)" }
if ($this.Prerelease) { $result += '-{0}' -f $this.Prerelease }
if ($this.BuildMetadata) { $result += '+{0}' -f $this.BuildMetadata }
return $result
}

Expand All @@ -100,17 +160,8 @@ class AUVersion : System.IComparable {
}

hidden [object[]] GetParts() {
$result = @($this.Version)
if ($this.Prerelease) {
$this.Prerelease -split '\.' | ForEach-Object {
# if identifier is exclusively numeric, cast it to an int
if ($_ -match '^[0-9]+$') {
$result += [int] $_
} else {
$result += $_
}
}
}
$result = , $this.Version
$result += [AUVersion]::NormalizePrerelease($this.Prerelease)
return $result
}
}
Expand Down
46 changes: 34 additions & 12 deletions src/Public/Get-Version.ps1
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Author: Thomas Démoulins <[email protected]>
# Author: Thomas Démoulins <[email protected]>

<#
.SYNOPSIS
Parses a semver-like object from a string in a flexible manner.
Parses a SemVer-like object from a string in a flexible manner.

.DESCRIPTION
This function parses a string containing a semver-like version
This function parses a string containing a SemVer-like version
and returns an object that represents both the version (with up to 4 parts)
and optionally a pre-release and a build metadata.

Expand All @@ -16,26 +16,48 @@
- extra spaces are ignored
- optional delimiters can be provided to help parsing the string

Parameter -SemVer allows to specify the max supported SemVer version:
V1 (default) or V2 (requires choco v2.0.0). EnhancedV2 is about
transforming a SemVer1-like version into a SemVer2-like one when
possible (e.g. 1.61.0-beta.0 instead of 1.61.0-beta0).

Resulting version is normalized the same way chocolatey/nuget does.
See https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#normalized-version-numbers

.EXAMPLE
Get-Version 'Current version 2.1.1 beta 2.'

Returns 2.1.1-beta2

.EXAMPLE
Get-Version -SemVer V2 'Current version 2.1.1 beta 2.'

Returns 2.1.1-beta.2

.EXAMPLE
Get-Version 'Last version: 1.2.3 beta 3.'
Get-Version -SemVer V2 '4.0.3Beta1'

Returns 1.2.3-beta3
Returns 4.0.3-Beta1

.EXAMPLE
Get-Version 'https://github.com/atom/atom/releases/download/v1.24.0-beta2/AtomSetup.exe'
Get-Version -SemVer EnhancedV2 '4.0.3Beta1'

Return 1.24.0-beta2
Returns 4.0.3-Beta.1

.EXAMPLE
Get-Version 'http://mirrors.kodi.tv/releases/windows/win32/kodi-17.6-Krypton-x86.exe' -Delimiter '-'
Get-Version 'https://dl.airserver.com/pc32/AirServer-5.6.3-x86.msi' -Delimiter '-'

Return 17.6
Returns 5.6.3
#>
function Get-Version {
[CmdletBinding()]
param(
# Supported SemVer version: V1 (default) or V2 (requires choco v2.0.0).
# EnhancedV2 allows to transform a SemVer1-like version into SemVer2-like one (e.g. 1.2.0-rc.3 instead of 1.2.0-rc3)
[ValidateSet('V1', 'V2', 'EnhancedV2')]
[string] $SemVer = 'V1',
# Version string to parse.
[Parameter(Mandatory=$true)]
[Parameter(Mandatory, Position=0)]
[string] $Version,
# Optional delimiter(s) to help locate the version in the string: the version must start and end with one of these chars.
[char[]] $Delimiter
Expand All @@ -46,10 +68,10 @@ function Get-Version {
$regex = $Version | Select-String -Pattern "[$delimiters](\d+\.\d+[^$delimiters]*)[$delimiters]" -AllMatches
foreach ($match in $regex.Matches) {
$reference = [ref] $null
if ([AUVersion]::TryParse($match.Groups[1], $reference, $false)) {
if ([AUVersion]::TryParse($match.Groups[1], $reference, $false, $SemVer)) {
return $reference.Value
}
}
}
return [AUVersion]::Parse($Version, $false)
return [AUVersion]::Parse($Version, $false, $SemVer)
}
Loading