VeilVer.psm1

#Region './Private/Get-GitBlobTags.ps1' -1

function Get-GitBlobTags {
    [CmdletBinding()]
    param (
        # Does not need to exist anymore, but must be a valid path
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path $_ -IsValid }, ErrorMessage = 'Must be a valid path format, but does not need to exist (anymore).')]
        [string]$RelativeRootPath
    )

    # Example tag: VV/demo/docs/Contoso/Doc1.md/v1.0.0
    $TagPattern = "VV/$RelativeRootPath/v*"

    # Get tags with version data split by semicolon, sorted by version in descending order
    $Tags = Invoke-GitCommand 'tag', '--list', '--format=%(refname:short);%(contents)', '--sort=-version:refname', $TagPattern |
        Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
        ForEach-Object {
            $Tag, $Base64Data = $_ -split ';'
            
            $TagVersionString = ($Tag -split '/')[-1].TrimStart('v')
            $TagVersion = [version]$TagVersionString

            # The tag command returns an empty line as part of the tag message, so we need to filter it out
            $JsonData = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Base64Data))
            $Metadata = $JsonData | ConvertFrom-Json

            [pscustomobject]@{
                'File' = $RelativeRootPath
                'Tag' = $Tag
                'Version' = $TagVersion
                'Metadata' = $Metadata
            }
        }

    if ($Tags.Count -eq 0) {
        return
    }

    Write-Verbose @"
Found the following hidden version tag(s) for the path '$RelativeRootPath':
- $(($Tags | ForEach-Object { $_.Version }) -join "`n- ")
"@


    Write-Output $Tags
}
#EndRegion './Private/Get-GitBlobTags.ps1' 45
#Region './Private/Get-GitCurrentCommit.ps1' -1

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

    Invoke-GitCommand 'rev-parse', '--verify', 'HEAD'
}
#EndRegion './Private/Get-GitCurrentCommit.ps1' 7
#Region './Private/Get-GitFileHistoryNames.ps1' -1

function Get-GitFileHistoryNames {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    # Get all names that the file has had by looking at commits that have changed the file
    # Automatically sorted by most recent (current) name first
    # Empty format string to only get the file names
    $FileNames = Invoke-GitCommand 'log', '--format=', '--name-only', '--follow', '--', $Path | Select-Object -Unique

    Write-Verbose @"
Found the following file path(s) of the file from the commit history:
- $($FileNames -join "`n- ")
"@


    Write-Output $FileNames
}
#EndRegion './Private/Get-GitFileHistoryNames.ps1' 20
#Region './Private/Get-GitRepoRoot.ps1' -1

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

    Invoke-GitCommand 'rev-parse', '--show-toplevel'
}
#EndRegion './Private/Get-GitRepoRoot.ps1' 7
#Region './Private/Invoke-GitCommand.ps1' -1

function Invoke-GitCommand {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$Arguments
    )

    Test-GitInstallation -ErrorAction Stop

    Write-Verbose "Invoking 'git $Arguments'."

    & git $Arguments

    if ($LASTEXITCODE -ne 0) {
        throw "Command 'git $Arguments' failed with exit code $LASTEXITCODE."
    }
}
#EndRegion './Private/Invoke-GitCommand.ps1' 18
#Region './Private/Remove-GitBlobTag.ps1' -1

function Remove-GitBlobTag {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Tag
    )

    Invoke-GitCommand 'tag', '--delete', $Tag
}
#EndRegion './Private/Remove-GitBlobTag.ps1' 10
#Region './Private/Test-GitFileIsModified.ps1' -1

function Test-GitFileIsModified {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Path must exist and be a file.')]
        [string]$Path
    )

    try {
        $null = Invoke-GitCommand 'diff-index', 'HEAD', '--quiet', '--', $Path
    } catch {}
    
    # Returns true if the file has been modified
    $LASTEXITCODE -eq 1
}
#EndRegion './Private/Test-GitFileIsModified.ps1' 16
#Region './Private/Test-GitInstallation.ps1' -1

function Test-GitInstallation {
    [CmdletBinding()]
    param()

    if (-not (Get-Command "git" -ErrorAction SilentlyContinue)) {
        throw 'No installation of git was found, please install git to use this module.'
    }
}
#EndRegion './Private/Test-GitInstallation.ps1' 9
#Region './Public/Get-VVVersion.ps1' -1

function Get-VVVersion {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Path must exist and be a file.')]
        [string]$Path
    )

    # Get all tags based on file names
    $FileNames = Get-GitFileHistoryNames -Path $Path
    Write-Verbose @"
Found the following file path(s) of the file through the commit history:
- $($FileNames -join "`n- ")
"@

    $Tags = $FileNames | ForEach-Object {
        Get-GitBlobTags -RelativeRootPath $_
    }

    if ($Tags.Count -eq 0) {
        Write-Warning "No hidden version tags found for the file '$Path'."
        return
    }

    Write-Output $Tags
}
#EndRegion './Public/Get-VVVersion.ps1' 26
#Region './Public/Remove-VVVersion.ps1' -1

function Remove-VVVersion {
    [CmdletBinding(DefaultParameterSetName = 'FileVersion')]
    param (
        [Parameter(Mandatory, ParameterSetName = 'FileVersion')]
        [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Path must exist and be a file.')]
        [string]$Path,

        [Parameter(Mandatory, ParameterSetName = 'FileVersion')]
        [version]$Version,

        [Parameter(Mandatory, ParameterSetName = 'Tag')]
        [string]$Tag
    )

    # If parameter set name
    if ($PSCmdlet.ParameterSetName -eq 'FileVersion') {
        # Get all tags based on file names
        $FileNames = Get-GitFileHistoryNames -Path $Path

        $Tags = $FileNames | ForEach-Object {
            Get-GitBlobTags -RelativeRootPath $_
        }

        $Tag = $Tags | Where-Object { $_.Version -eq $Version } | Select-Object -ExpandProperty Tag
        
        if ($null -eq $Tag) {
            Write-Warning "No hidden version tags found for the file '$Path' with version '$Version'."
            return
        }
    }

    try {
        Remove-GitBlobTag -Tag $Tag -ErrorAction Stop
    
        Write-Verbose "Successfully removed the hidden version tag '$Tag'."
    }
    catch {
        throw "Failed to remove the hidden version tag '$Tag'."
    }
}
#EndRegion './Public/Remove-VVVersion.ps1' 41
#Region './Public/Set-VVVersion.ps1' -1

function Set-VVVersion {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Path must exist and be a file.')]
        [string]$Path,

        [Parameter(Mandatory)]
        [version]$Version,

        [Parameter(Mandatory)]
        [hashtable]$Metadata
    )

    Push-Location (Get-GitRepoRoot)

    # Get the relative path of the file from the root of the repo and trim start
    $RelativePath = (Resolve-Path $Path -Relative).TrimStart('.\').TrimStart('./')
    $TagName = "VV/$RelativePath/v$Version"

    # Ensure that the file has no pending changes, since we are tagging the file content together with the commit and don't want any discrepancies
    if (Test-GitFileIsModified -Path $RelativePath) {
        Write-Warning "The file '$RelativePath' has been modified. Please commit the changes before setting the version."
        return
    }

    # Set extra metadata for the tag
    if ($Metadata.ContainsKey('Commit')) { Write-Warning "The 'Commit' key is reserved and will be overwritten." }
    $Metadata['Commit'] = Get-GitCurrentCommit
    
    # Assemble metadata, convert to JSON and then to Base64
    $JsonMetadata = $Metadata | ConvertTo-Json -Compress -Depth 20
    $Base64Metadata = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($JsonMetadata))

    # Tag the file with the version data as json in the tag message
    $BlobHash = Invoke-GitCommand 'hash-object', '-t', 'blob', $Path
    Invoke-GitCommand 'tag', '-a', $TagName, $BlobHash, '-m', $Base64Metadata -ErrorAction Stop

    Write-Verbose "Hidden tag '$TagName' has been created for '$RelativePath'."
}
#EndRegion './Public/Set-VVVersion.ps1' 41