SemanticRelease.psm1

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

# Source: src/public/Get-SemanticReleaseCommitMessage.ps1
function Get-SemanticReleaseCommitMessage {
    [CmdletBinding()]
    param(
        [string]$ProjectRoot = (Get-Location).Path
    )

    return Read-SemanticReleaseCommitMessage -ProjectRoot $ProjectRoot
}


# Source: src/public/Get-SemanticReleaseCommitRange.ps1
function Get-SemanticReleaseCommitRange {
    [CmdletBinding()]
    param(
        [string]$ProjectRoot = (Get-Location).Path
    )

    return Get-SemanticReleaseCommitRangeBasis -ProjectRoot $ProjectRoot
}


# Source: src/public/Get-SemanticReleaseLabel.ps1
function Get-SemanticReleaseLabel {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string[]]$Message
    )

    return Resolve-SemanticReleaseVersionLabel -Message $Message
}


# Source: src/public/Get-SemanticReleaseNextVersion.ps1
function Get-SemanticReleaseNextVersion {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][semver]$CurrentVersion,
        [ValidateSet('Major', 'Minor', 'Patch')]
        [string]$Label = 'Patch',
        [switch]$PreviewRelease,
        [switch]$StableRelease
    )

    return Get-SemanticReleaseVersionPlan -CurrentVersion $CurrentVersion -Label $Label -PreviewRelease:$PreviewRelease -StableRelease:$StableRelease
}


# Source: src/public/Set-SemanticReleaseJsonVersion.ps1
function Set-SemanticReleaseJsonVersion {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory)][string]$Path,
        [string]$Key = 'Version',
        [ValidateSet('Major', 'Minor', 'Patch')]
        [string]$Label = 'Patch',
        [ValidateSet('Default', 'Preview', 'Stable')]
        [string]$ReleaseType = 'Default'
    )

    $jsonData = Read-SemanticReleaseJsonData -JsonPath $Path
    if (-not $jsonData.ContainsKey($Key)) {
        throw "JSON key '$Key' was not found in $Path"
    }

    $previousVersion = [string]$jsonData[$Key]
    $isPreviewRelease = $ReleaseType -eq 'Preview'
    $isStableRelease = $ReleaseType -eq 'Stable'
    $versionPlan = Get-SemanticReleaseVersionPlan -CurrentVersion ([semver]$previousVersion) -Label $Label -PreviewRelease:$isPreviewRelease -StableRelease:$isStableRelease
    $newVersion = $versionPlan.NewVersion.ToString()
    $target = [System.IO.Path]::GetFileName($Path)
    $action = "Set $Key to $newVersion"
    $changeInfo = @{
        Path = $Path
        Key = $Key
        PreviousVersion = $previousVersion
        NewVersion = $newVersion
    }
    if (-not $PSCmdlet.ShouldProcess($target, $action)) {
        return Get-SemanticReleaseJsonVersionWriteResult -ChangeInfo $changeInfo
    }

    $jsonData[$Key] = $newVersion
    Write-SemanticReleaseJsonData -JsonPath $Path -Data $jsonData
    return Get-SemanticReleaseJsonVersionWriteResult -ChangeInfo $changeInfo -Applied
}


# Source: src/private/release/Get-SemanticReleaseCommitRangeBasis.ps1
function Get-SemanticReleaseCommitRangeBasis {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ProjectRoot
    )

    if (-not (Test-SemanticReleaseGitAvailable)) {
        return Get-SemanticReleaseCommitRangeBasisResult -ProjectRoot $ProjectRoot -IsRepository:$false
    }

    $repositoryResult = Invoke-SemanticReleaseGitCommand -ProjectRoot $ProjectRoot -Arguments @('rev-parse', '--is-inside-work-tree')
    $repositoryState = Get-SemanticReleaseGitCommandOutputText -Result $repositoryResult
    if ($repositoryResult.ExitCode -ne 0 -or $repositoryState -ne 'true') {
        return Get-SemanticReleaseCommitRangeBasisResult -ProjectRoot $ProjectRoot -IsRepository:$false
    }

    $lastTagResult = Invoke-SemanticReleaseGitCommand -ProjectRoot $ProjectRoot -Arguments @('describe', '--tags', '--abbrev=0')
    $lastTag = Get-SemanticReleaseGitCommandOutputText -Result $lastTagResult
    if ($lastTagResult.ExitCode -eq 0 -and -not [string]::IsNullOrWhiteSpace($lastTag)) {
        return Get-SemanticReleaseCommitRangeBasisResult -ProjectRoot $ProjectRoot -IsRepository -LastTag $lastTag
    }

    return Get-SemanticReleaseCommitRangeBasisResult -ProjectRoot $ProjectRoot -IsRepository
}

function Get-SemanticReleaseCommitRangeBasisResult {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ProjectRoot,
        [switch]$IsRepository,
        [string]$LastTag
    )

    $hasVersionTag = -not [string]::IsNullOrWhiteSpace($LastTag)
    $revisionRange = $null
    if ($hasVersionTag) {
        $revisionRange = "$LastTag..HEAD"
    }

    return [pscustomobject]@{
        ProjectRoot   = $ProjectRoot
        IsRepository  = [bool]$IsRepository
        HasVersionTag = $hasVersionTag
        LastTag       = $LastTag
        RevisionRange = $revisionRange
    }
}


# Source: src/private/release/Get-SemanticReleaseJsonVersionWriteResult.ps1
function Get-SemanticReleaseJsonVersionWriteResult {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$ChangeInfo,
        [switch]$Applied
    )

    return [pscustomobject]@{
        Path = [string]$ChangeInfo.Path
        Target = [System.IO.Path]::GetFileName([string]$ChangeInfo.Path)
        Key = [string]$ChangeInfo.Key
        PreviousVersion = [string]$ChangeInfo.PreviousVersion
        NewVersion = [string]$ChangeInfo.NewVersion
        Applied = [bool]$Applied
    }
}


# Source: src/private/release/Get-SemanticReleaseVersionPlan.ps1
function Get-SemanticReleaseVersionPlan {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][semver]$CurrentVersion,
        [ValidateSet('Major', 'Minor', 'Patch')]
        [string]$Label = 'Patch',
        [switch]$PreviewRelease,
        [switch]$StableRelease
    )

    $versionPart = Get-SemanticReleaseVersionPartForPlan -CurrentVersion $CurrentVersion -Label $Label -PreviewRelease:$PreviewRelease
    $releaseType = Resolve-SemanticReleasePreReleaseLabel -CurrentVersion $CurrentVersion -PreviewRelease:$PreviewRelease -StableRelease:$StableRelease
    $newVersion = [semver]::new($versionPart.Major, $versionPart.Minor, $versionPart.Patch, $releaseType, $null)

    return [pscustomobject]@{
        CurrentVersion = $CurrentVersion
        Label = $Label
        PreviewRelease = [bool]$PreviewRelease
        StableRelease = [bool]$StableRelease
        NewVersion = $newVersion
    }
}

function Get-SemanticReleaseVersionPartForPlan {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][semver]$CurrentVersion,
        [Parameter(Mandatory)][string]$Label,
        [switch]$PreviewRelease
    )

    if ($PreviewRelease) {
        if (-not [string]::IsNullOrWhiteSpace($CurrentVersion.PreReleaseLabel)) {
            return [pscustomobject]@{
                Major = $CurrentVersion.Major
                Minor = $CurrentVersion.Minor
                Patch = $CurrentVersion.Patch
            }
        }

        return Resolve-SemanticReleaseVersionPart -CurrentVersion $CurrentVersion -Label Patch
    }

    return Resolve-SemanticReleaseVersionPart -CurrentVersion $CurrentVersion -Label $Label
}


# Source: src/private/release/Read-SemanticReleaseCommitMessage.ps1
function Read-SemanticReleaseCommitMessage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ProjectRoot
    )

    $commitRangeBasis = Get-SemanticReleaseCommitRangeBasis -ProjectRoot $ProjectRoot
    if (-not $commitRangeBasis.IsRepository) {
        return @()
    }

    $format = '%s%n%b%n--END-COMMIT--'
    $logResult = Get-SemanticReleaseCommitLogResult -ProjectRoot $ProjectRoot -Format $format -CommitRangeBasis $commitRangeBasis
    return @(ConvertFrom-SemanticReleaseCommitLogResult -Result $logResult)
}

function Get-SemanticReleaseCommitLogResult {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ProjectRoot,
        [Parameter(Mandatory)][string]$Format,
        [Parameter(Mandatory)][pscustomobject]$CommitRangeBasis
    )

    if ($CommitRangeBasis.HasVersionTag) {
        return Invoke-SemanticReleaseGitCommand -ProjectRoot $ProjectRoot -Arguments @('log', $CommitRangeBasis.RevisionRange, "--format=$Format")
    }

    return Invoke-SemanticReleaseGitCommand -ProjectRoot $ProjectRoot -Arguments @('log', "--format=$Format")
}

function ConvertFrom-SemanticReleaseCommitLogResult {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][pscustomobject]$Result
    )

    if ($Result.ExitCode -ne 0 -or @($Result.Output).Count -eq 0) {
        return @()
    }

    $text = @($Result.Output) -join [Environment]::NewLine
    $commitList = $text -split '(?m)^--END-COMMIT--\r?$'
    return @($commitList | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
}


# Source: src/private/release/Resolve-SemanticReleasePreReleaseLabel.ps1
function Resolve-SemanticReleasePreReleaseLabel {
    [CmdletBinding()]
    param(
        [AllowNull()][semver]$CurrentVersion,
        [switch]$PreviewRelease,
        [switch]$StableRelease
    )

    if ($PreviewRelease) {
        return Resolve-SemanticReleasePreviewLabel -CurrentVersion $CurrentVersion
    }

    if ($StableRelease) {
        return $null
    }

    return $null
}

function Resolve-SemanticReleasePreviewLabel {
    [CmdletBinding()]
    param(
        [AllowNull()][semver]$CurrentVersion
    )

    if ($null -eq $CurrentVersion -or [string]::IsNullOrWhiteSpace($CurrentVersion.PreReleaseLabel)) {
        return 'preview'
    }

    return Get-SemanticReleaseNextPreReleaseLabel -PreReleaseLabel $CurrentVersion.PreReleaseLabel
}

function Get-SemanticReleaseNextPreReleaseLabel {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$PreReleaseLabel
    )

    $match = [regex]::Match($PreReleaseLabel, '^(?<Stem>.*?)(?<Number>\d+)?$')
    $stem = $match.Groups['Stem'].Value
    $preReleaseNumber = $match.Groups['Number'].Value

    if ([string]::IsNullOrWhiteSpace($preReleaseNumber)) {
        return "$stem$( Get-SemanticReleaseInitialPreReleaseNumber )"
    }

    return "$stem$( Get-SemanticReleaseIncrementedPreReleaseNumber -PreReleaseNumber $preReleaseNumber )"
}

function Get-SemanticReleaseInitialPreReleaseNumber {
    [CmdletBinding()]
    param()

    return '01'
}

function Get-SemanticReleaseIncrementedPreReleaseNumber {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$PreReleaseNumber
    )

    $nextNumber = [int]$PreReleaseNumber + 1
    if ($PreReleaseNumber.Length -eq 1) {
        return $nextNumber
    }

    return $nextNumber.ToString("D$( $PreReleaseNumber.Length )")
}


# Source: src/private/release/Resolve-SemanticReleaseVersionLabel.ps1
function Resolve-SemanticReleaseVersionLabel {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string[]]$Message
    )

    if ($Message | Where-Object { $_ -match '(?im)BREAKING CHANGE|^[a-z]+(\(.+\))?!:' }) {
        return 'Major'
    }

    if ($Message | Where-Object { $_ -match '(?im)^\s*feat(\(.+\))?:' }) {
        return 'Minor'
    }

    if ($Message | Where-Object { $_ -match '(?im)^\s*fix(\(.+\))?:' }) {
        return 'Patch'
    }

    return 'Patch'
}


# Source: src/private/release/Resolve-SemanticReleaseVersionPart.ps1
function Resolve-SemanticReleaseVersionPart {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][semver]$CurrentVersion,
        [ValidateSet('Major', 'Minor', 'Patch')]
        [string]$Label = 'Patch'
    )

    if (Test-SemanticReleaseShouldFinalizePrereleaseTarget -CurrentVersion $CurrentVersion -Label $Label) {
        return Get-SemanticReleaseVersionPartObject -CurrentVersion $CurrentVersion
    }

    switch ($Label) {
        'Major' {
            return [pscustomobject]@{
                Major = $CurrentVersion.Major + 1
                Minor = 0
                Patch = 0
            }
        }
        'Minor' {
            return [pscustomobject]@{
                Major = $CurrentVersion.Major
                Minor = $CurrentVersion.Minor + 1
                Patch = 0
            }
        }
        default {
            return [pscustomobject]@{
                Major = $CurrentVersion.Major
                Minor = $CurrentVersion.Minor
                Patch = $CurrentVersion.Patch + 1
            }
        }
    }
}

function Test-SemanticReleaseShouldFinalizePrereleaseTarget {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][semver]$CurrentVersion,
        [Parameter(Mandatory)][string]$Label
    )

    if ([string]::IsNullOrWhiteSpace($CurrentVersion.PreReleaseLabel)) {
        return $false
    }

    return (Get-SemanticReleaseTargetLabelForPrerelease -CurrentVersion $CurrentVersion) -eq $Label
}

function Get-SemanticReleaseTargetLabelForPrerelease {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][semver]$CurrentVersion
    )

    if ($CurrentVersion.Patch -gt 0) {
        return 'Patch'
    }

    if ($CurrentVersion.Minor -gt 0) {
        return 'Minor'
    }

    return 'Major'
}

function Get-SemanticReleaseVersionPartObject {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][semver]$CurrentVersion
    )

    return [pscustomobject]@{
        Major = $CurrentVersion.Major
        Minor = $CurrentVersion.Minor
        Patch = $CurrentVersion.Patch
    }
}


# Source: src/private/shared/Get-SemanticReleaseGitCommandOutputText.ps1
function Get-SemanticReleaseGitCommandOutputText {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][pscustomobject]$Result
    )

    return (@($Result.Output) -join [Environment]::NewLine).Trim()
}


# Source: src/private/shared/Invoke-SemanticReleaseGitCommand.ps1
function Invoke-SemanticReleaseGitCommand {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ProjectRoot,
        [Parameter(Mandatory)][string[]]$Arguments
    )

    $output = & git -C $ProjectRoot @Arguments 2> $null
    return [pscustomobject]@{
        ExitCode = $LASTEXITCODE
        Output = @($output)
    }
}


# Source: src/private/shared/Read-SemanticReleaseJsonData.ps1
function Read-SemanticReleaseJsonData {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$JsonPath
    )

    $jsonContent = Get-Content -LiteralPath $JsonPath -Raw
    if ([string]::IsNullOrWhiteSpace($jsonContent)) {
        throw "JSON file is empty: $JsonPath"
    }

    try {
        $jsonData = $jsonContent | ConvertFrom-Json -AsHashtable
    } catch {
        throw "JSON file is not valid JSON: $JsonPath. $( $_.Exception.Message )"
    }

    if ($jsonData -isnot [hashtable]) {
        throw "JSON file must contain a top-level JSON object: $JsonPath"
    }

    return $jsonData
}


# Source: src/private/shared/Test-SemanticReleaseGitAvailable.ps1
function Test-SemanticReleaseGitAvailable {
    [CmdletBinding()]
    param()

    return $null -ne (Get-Command -Name 'git' -ErrorAction SilentlyContinue)
}


# Source: src/private/shared/Write-SemanticReleaseJsonData.ps1
function Write-SemanticReleaseJsonData {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$JsonPath,
        [Parameter(Mandatory)][hashtable]$Data
    )

    $jsonContent = $Data | ConvertTo-Json -Depth 20
    Set-Content -LiteralPath $JsonPath -Value $jsonContent -Encoding utf8
}