VeilVer.psm1
#Region './Private/Get-GitBlobHash.ps1' -1 function Get-GitBlobHash { [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Path must exist and be a file.')] [string]$Path ) $BlobHash = Invoke-GitCommand 'hash-object', '-t', 'blob', $Path Write-Output $BlobHash } #EndRegion './Private/Get-GitBlobHash.ps1' 13 #Region './Private/Get-GitBlobTag.ps1' -1 function Get-GitBlobTag { [CmdletBinding()] param ( # Does not need to exist anymore, but must be a valid path [Parameter(Mandatory, ParameterSetName = 'Path')] [ValidateScript({ Test-Path $_ -IsValid }, ErrorMessage = 'Must be a valid path format, but does not need to exist (anymore).')] [string]$RelativeRootPath, [Parameter(ParameterSetName = 'All')] [switch]$All ) # Example tag: VV/demo/docs/Contoso/Doc1.md/v1.0.0 # Gets the pattern of tags with wildcard if ($All.IsPresent) { $TagPattern = Get-GitBlobTagName -All } else { $TagPattern = Get-GitBlobTagName -RelativeRootPath $RelativeRootPath -Pattern } # 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 # Get the hash of the blob that the tag points to / contains $Hash = (Invoke-GitCommand 'rev-list', '--objects', $Tag | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and $_ -notmatch $Tag }).Trim() [pscustomobject]@{ 'Path' = [string]::IsNullOrWhiteSpace($RelativeRootPath) ? [Regex]::Match($Tag,'^@VV/(?<file>.*)/v[\d.]+$').Groups['file'].Value : $RelativeRootPath 'Tag' = $Tag 'Hash' = $Hash '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-GitBlobTag.ps1' 57 #Region './Private/Get-GitBlobTagName.ps1' -1 function Get-GitBlobTagName { [CmdletBinding()] param ( [Parameter(Mandatory, ParameterSetName = 'Path')] [Parameter(Mandatory, ParameterSetName = 'Pattern')] [ValidateScript({ Test-Path $_ -IsValid }, ErrorMessage = 'Must be a valid path format, but does not need to exist (anymore).')] [string]$RelativeRootPath, [Parameter(Mandatory, ParameterSetName = 'Path')] [version]$Version, [Parameter(Mandatory, ParameterSetName = 'Pattern')] [switch]$Pattern, [Parameter(ParameterSetName = 'All')] [switch]$All ) if ($All.IsPresent) { $OutputString = "@VV/*" } elseif ($Pattern) { $OutputString = "@VV/$RelativeRootPath/v*" } else { $OutputString = "@VV/$RelativeRootPath/v$Version" } Write-Output $OutputString } #EndRegion './Private/Get-GitBlobTagName.ps1' 29 #Region './Private/Get-GitBranchDefaultRemote.ps1' -1 function Get-GitBranchDefaultRemote { [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [string]$Branch = (Invoke-GitCommand 'branch', '--show-current') ) # Get the default remote of the branch $DefaultRemote = Invoke-GitCommand 'branch', '--list', $Branch, '--format=%(upstream:remotename)' if ([string]::IsNullOrWhiteSpace($DefaultRemote)) { throw "Did not find a configured remote for the branch '$Branch'." } Write-Output $DefaultRemote } #EndRegion './Private/Get-GitBranchDefaultRemote.ps1' 17 #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/Get-RelativeRootFilePath.ps1' -1 function Get-RelativeRootFilePath { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -IsValid }, ErrorMessage = 'Must be a valid path format, but does not need to exist (anymore).')] [string]$Path ) Push-Location (Get-GitRepoRoot) # Trim slashes from relative path $RelativePath = (Resolve-Path $Path -Relative).TrimStart('.\').TrimStart('./') Pop-Location Write-Output $RelativePath } #EndRegion './Private/Get-RelativeRootFilePath.ps1' 18 #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 ) $null = Invoke-GitCommand 'tag', '--delete', $Tag } #EndRegion './Private/Remove-GitBlobTag.ps1' 10 #Region './Private/Test-GitBlobTag.ps1' -1 function Test-GitBlobTag { [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$Tag ) try { $null = Invoke-GitCommand 'tag', '--list', $Tag } catch {} # Returns true if the tag exists $LASTEXITCODE -eq 0 } #EndRegion './Private/Test-GitBlobTag.ps1' 15 #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(DefaultParameterSetName = 'Path')] param ( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Path')] [Parameter(Mandatory, ParameterSetName = 'Checkout')] [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Path must exist and be a file.')] [string]$Path, [Parameter(ParameterSetName = 'Path')] [Parameter(Mandatory, ParameterSetName = 'Checkout')] [ValidateNotNullOrEmpty()] [version]$Version, [Parameter(Mandatory, ParameterSetName = 'Checkout')] [switch]$Checkout, [Parameter(ParameterSetName = 'Checkout')] [switch]$Force, [Parameter(ParameterSetName = 'All')] [switch]$All ) if ($All.IsPresent) { $Tags = Get-GitBlobTag -All $Paths = $Tags | ForEach-Object { [Regex]::Match($_.Tag,'^@VV/(?<file>.*)/v[\d.]+$').Groups['file'].Value } | Select-Object -Unique $Paths | ForEach-Object { Get-VVVersion -Path $_ -ErrorAction SilentlyContinue } | Write-Output return } # Get all tags based on file names $FileNames = Get-GitFileHistoryNames -Path $Path $Tags = $FileNames | ForEach-Object { Get-GitBlobTag -RelativeRootPath $_ } if ($Tags.Count -eq 0) { Write-Warning "No hidden version tags found for the file '$Path'." return } # If a version is specified, return the tag with the version if ($null -ne $Version -and -not $Checkout.IsPresent) { $TagInfo = $Tags | Where-Object Version -eq $Version if ($null -eq $TagInfo) { throw "The specified version '$Version' of file '$Path' does not exist." } return $TagInfo } # If checkout is specified, checkout the version if ($Checkout.IsPresent) { if ((Test-GitFileIsModified -Path $Path) -and -not $Force.IsPresent) { throw "The file '$Path' has been modified. Please commit or discard the changes before checking out a version, or override the file using the -Force parameter." } $TagInfo = $Tags | Where-Object Version -eq $Version if ($null -eq $TagInfo) { throw "The specified version '$Version' of file '$Path' does not exist." } $FileContent = Invoke-GitCommand 'show', $TagInfo.Hash Set-Content -Path $Path -Value $FileContent -Force:$Force.IsPresent Write-Verbose "Checked out version '$Version' of '$Path' successfully." return } # Return all tags if no version is specified Write-Output $Tags } #EndRegion './Public/Get-VVVersion.ps1' 73 #Region './Public/Import-VVVersion.ps1' -1 function Import-VVVersion { [Alias('Pull-VVVersion')] [CmdletBinding(DefaultParameterSetName = 'All')] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Tag')] [ValidatePattern('^@VV', ErrorMessage = 'Tag must start with the prefix "@VV".')] [string]$Tag, [Parameter(ParameterSetName = 'Tag')] [Parameter(ParameterSetName = 'All')] [ValidateNotNullOrEmpty()] [string]$Remote = (Get-GitBranchDefaultRemote), [Parameter(ParameterSetName = 'Tag')] [Parameter(ParameterSetName = 'All')] [switch]$Force ) # Get all VeilVer tags from the git remote $RemoteTags = Invoke-GitCommand 'ls-remote', '--tags', $Remote, 'refs/tags/@VV*' | Where-Object { -not $_.EndsWith('^{}') } | ForEach-Object { $_.Split("`t")[-1] } if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('Tag') -and $RemoteTags -notcontains "refs/tags/$Tag") { throw "The tag '$Tag' does not exist on the remote '$Remote'." } # Get all VeilVer tags from the local git repository $LocalTags = Invoke-GitCommand 'tag', '--list', '@VV*' $NewRemoteTags = $RemoteTags | ForEach-Object { $_.TrimStart('refs/tags/') } | Where-Object { $_ -notin $LocalTags } switch ($PSCmdlet.ParameterSetName) { 'All' { # Fetch all VeilVer tags from the git remote # Overwrite local different tags with the same name if -Force is specified $Pattern = $Force.IsPresent ? '+refs/tags/@VV*:refs/tags/@VV*' : 'refs/tags/@VV*:refs/tags/@VV*' $null = Invoke-GitCommand 'fetch', $Remote, $Pattern, '--quiet' if ($NewRemoteTags.Count -eq 0) { Write-Warning "No new hidden version tags found on the remote '$Remote'." return } Write-Verbose @" Fetched the following hidden version tag(s): - $($NewRemoteTags -join "`n- ") "@ } 'Tag' { # Overwrite local tags with the same name if -Force is specified $Pattern = $Force.IsPresent ? "+refs/tags/${Tag}:refs/tags/${Tag}" : "refs/tags/${Tag}:refs/tags/${Tag}" $null = Invoke-GitCommand 'fetch', $Remote, $Pattern, '--quiet' Write-Verbose "Fetched the hidden version tag '$Tag' from the remote '$Remote'." } } } #EndRegion './Public/Import-VVVersion.ps1' 58 #Region './Public/Push-VVVersion.ps1' -1 function Push-VVVersion { [CmdletBinding(DefaultParameterSetName = 'Path')] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Path')] [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Path must exist and be a file.')] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Path')] [ValidateNotNullOrEmpty()] [version]$Version, [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Tag')] [ValidatePattern('^@VV', ErrorMessage = 'Tag must start with the prefix "@VV".')] [string]$Tag, [Parameter(ParameterSetName = 'Tag')] [Parameter(ParameterSetName = 'Path')] [ValidateNotNullOrEmpty()] [string]$Remote = (Get-GitBranchDefaultRemote) ) switch ($PSCmdlet.ParameterSetName) { 'Path' { $Tag = (Get-VVVersion -Path $Path -Version $Version).Tag } } # Push the tag to the git remote $null = Invoke-GitCommand 'push', $Remote, 'tag', $Tag, '--quiet' Write-Verbose "Pushed tag '$Tag' to remote '$Remote'." } #EndRegion './Public/Push-VVVersion.ps1' 34 #Region './Public/Remove-VVVersion.ps1' -1 function Remove-VVVersion { [CmdletBinding(DefaultParameterSetName = 'Path')] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Path')] [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Path must exist and be a file.')] [string]$Path, [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Path')] [version]$Version, [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Tag')] [string]$Tag ) # If parameter set name if ($PSCmdlet.ParameterSetName -eq 'Path') { # Get all tags based on file names $FileNames = Get-GitFileHistoryNames -Path $Path $Tags = $FileNames | ForEach-Object { Get-GitBlobTag -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 ) # Get the relative path of the file from the root of the repo and trim start $RelativePath = Get-RelativeRootFilePath -Path $Path $TagName = Get-GitBlobTagName -RelativeRootPath $RelativePath -Version $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 was provided, and will be used instead of current commit." } $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 = Get-GitBlobHash -Path $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' 39 #Region './Public/Update-VVVersion.ps1' -1 function Update-VVVersion { [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Path must exist and be a file.')] [string]$Path ) # Get the relative path of the file from the root of the repo and trim start $RelativePath = Get-RelativeRootFilePath -Path $Path # 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 } # Ensure the file exists if (-not (Test-Path -Path $Path)) { Write-Error "File path '$Path' does not exist." return } # Retrieve all historical names of the file $HistoricalNames = Get-GitFileHistoryNames -Path $Path # Find all tags associated with the file's historical names $Tags = $HistoricalNames | ForEach-Object { Get-GitBlobTag -RelativeRootPath $_ } # Remove old tags and recreate them with the new file path foreach ($Tag in $Tags) { if ($Tag.File -ne $Path) { Remove-VVVersion -Tag $Tag.Tag Set-VVVersion -Path $Path -Version $Tag.Version -Metadata ( $Tag.Metadata.psobject.properties | ForEach-Object -Begin { $Metahash = @{} } -Process { $Metahash[$_.Name] = $_.Value } -End { $Metahash } ) } } Write-Verbose "All git tags for '$Path' have been updated to its current file name." } #EndRegion './Public/Update-VVVersion.ps1' 45 |