functions/_getExtensionFromGit.ps1
|
# <copyright file="_getExtensionFromGit.ps1" company="Endjin Limited"> # Copyright (c) Endjin Limited. All rights reserved. # </copyright> function _getExtensionFromGit { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)] [string] $Name, [Parameter(Mandatory)] [uri] $RepositoryUri, [Parameter()] [string] $RepositoryFolderPath = 'module', [Parameter(Mandatory)] [string] $TargetPath, [Parameter()] [string] $GitRef, [Parameter()] [string] $MinimumVersion, [Parameter()] [switch] $PreRelease, [Parameter()] [string] $LockedCommit ) # Records where an installed extension came from, so that later runs know which commit is installed $sourceInfoFileName = '.zf-source.json' # Whether an installed copy came from the requested repository and folder. Ignore the case of the repository URL, # as well as any trailing '/' or '.git', as the lock file does. Folders in a git repository are case-sensitive. $isSameSource = { param ($sourceInfo) (_getNormalizedGitRepositoryUrl -RepositoryUrl "$($sourceInfo['repository'])") -eq (_getNormalizedGitRepositoryUrl -RepositoryUrl $RepositoryUri.OriginalString) -and "$($sourceInfo['folderPath'])" -ceq $RepositoryFolderPath } # A minimum version installs the newest release tag in its range, up to the next major version. An installed # release in the range is used as it is, so the tags are only listed when none is installed. if ($MinimumVersion -and !$GitRef) { $range = _getExtensionVersionRange -MinimumVersion $MinimumVersion $installedRelease = Get-ChildItem -Path (Join-Path $TargetPath $Name) -Directory -ErrorAction Ignore | ForEach-Object { $sourceInfoPath = Join-Path $_.FullName $sourceInfoFileName if (Test-Path $sourceInfoPath) { $sourceInfo = Get-Content -Raw -Path $sourceInfoPath | ConvertFrom-Json -AsHashtable $version = _getVersionFromGitTag -Ref "$($sourceInfo['ref'])" if ((& $isSameSource $sourceInfo) -and $version -and (_testExtensionVersionInRange -Version $version -Range $range -PreRelease:$PreRelease)) { @{ Ref = $sourceInfo['ref']; Version = $version } } } } | Sort-Object -Property { $_.Version } -Descending | Select-Object -First 1 if ($installedRelease) { $GitRef = $installedRelease.Ref } else { try { $availableReleases = @(_getGitTagVersions -RepoUrl $RepositoryUri.OriginalString) } catch { throw "Failed to list the release tags of extension $Name from $RepositoryUri repository: $($_.Exception.Message)" } $availableRelease = $availableReleases | Where-Object { _testExtensionVersionInRange -Version $_.Version -Range $range -PreRelease:$PreRelease } | Sort-Object -Property { $_.Version } -Descending | Select-Object -First 1 if (!$availableRelease) { throw ("No release of extension $Name in $RepositoryUri repository matches $($range.Text). " + "Releases are git tags named after a semantic version, e.g. '$($range.Minimum)' or 'v$($range.Minimum)'. GitHub Releases are not used.") } $GitRef = $availableRelease.Ref } Write-Verbose "Extension '$Name' $($range.Text) resolved to '$GitRef'" } elseif (!$GitRef) { throw "A 'GitRef' or 'MinimumVersion' is required to install extension $Name from $RepositoryUri repository" } # Check whether module is already installed. Copies from different repositories (e.g. a fork and its upstream) or # folders are installed in different folders, so they never overwrite each other's files. $installFolderName = _getGitExtensionFolderName -RepositoryUri $RepositoryUri -RepositoryFolderPath $RepositoryFolderPath -GitRef $GitRef $existingExtensionPath,$existingExtensionVersion = Get-InstalledExtensionDetails -Name $Name -TargetPath $TargetPath -GitRefAsFolderName $installFolderName # Every install records its source once all of its files are in place, so a folder without this record is an # install that did not complete. Earlier versions of ZeroFailed did not use this folder name. $installedCommit = $null $installedSourceInfo = $null if ($existingExtensionPath) { $sourceInfoPath = Join-Path $existingExtensionPath $sourceInfoFileName if (Test-Path $sourceInfoPath) { $installedSourceInfo = Get-Content -Raw -Path $sourceInfoPath | ConvertFrom-Json -AsHashtable $installedCommit = $installedSourceInfo['commit'] } } $installRequired = $false if (!$existingExtensionPath) { Write-Verbose "Extension '$Name' not found locally." $installRequired = $true } elseif ($existingExtensionVersion -ne $installFolderName) { Write-Verbose "Extension '$Name' found locally but version mismatch detected. Found: '$existingExtensionVersion'; Required: '$installFolderName' [$GitRef]" $installRequired = $true } elseif (!$installedSourceInfo) { Write-Verbose "Extension '$Name' found locally but its install did not complete, as it has no '$sourceInfoFileName' file" $installRequired = $true } elseif ($installedSourceInfo.ContainsKey('repository') -and !(& $isSameSource $installedSourceInfo)) { # The folder name includes a short hash of the source, so this only happens if two sources' hashes collide or # the folder was changed by hand Write-Verbose ("Extension '$Name' found locally but it was installed from a different source. " + "Found: '$($installedSourceInfo['repository'])' ($($installedSourceInfo['folderPath'])); Required: '$RepositoryUri' ($RepositoryFolderPath)") $installRequired = $true } elseif ($LockedCommit -and $installedCommit -ne $LockedCommit) { Write-Verbose "Extension '$Name' found locally but its commit does not match the lock file. Found: '$installedCommit'; Required: '$LockedCommit'" $installRequired = $true } # Handle getting the module from the repository if ($installRequired) { Write-Host "Installing extension $Name from $RepositoryUri" -f Cyan # The source record is written last, so an install that stops part way (e.g. with Ctrl+C) leaves a folder without # it, which is installed again on the next run. The files are copied straight into the install folder rather than # renaming a staging folder, as editors and antivirus scanners often have newly written files open, which stops # their folder being renamed on Windows. $destinationPath = Join-Path $TargetPath $Name $installFolderName $copyArgs = @{ RepoUrl = $RepositoryUri DestinationPath = $destinationPath RepoFolderPath = $RepositoryFolderPath GitRef = $GitRef } if ($LockedCommit) { $copyArgs.Commit = $LockedCommit } try { # Mark an existing install as incomplete before deleting it, in case it can only be partly deleted if (Test-Path $destinationPath) { $existingSourceInfoPath = Join-Path $destinationPath $sourceInfoFileName if (Test-Path $existingSourceInfoPath) { Remove-Item -Path $existingSourceInfoPath -Force -ErrorAction Stop } _removeFolderWithRetry -Path $destinationPath } $installedCommit = _copyFolderFromGitRepo @copyArgs [ordered]@{ repository = $RepositoryUri.OriginalString folderPath = $RepositoryFolderPath ref = $GitRef commit = $installedCommit } | ConvertTo-Json | Set-Content -Path (Join-Path $destinationPath $sourceInfoFileName) -ErrorAction Stop } catch { throw "Failed to install extension $Name ($GitRef) from $RepositoryUri repository: $($_.Exception.Message)" } $existingExtensionPath,$existingExtensionVersion = Get-InstalledExtensionDetails -Name $Name -TargetPath $TargetPath -GitRefAsFolderName $installFolderName if (!$existingExtensionPath) { throw "Failed to install extension $Name ($GitRef) from $RepositoryUri repository" } Write-Host "INSTALLED MODULE: $Name ($existingExtensionVersion)" -f Cyan } else { Write-Host "FOUND MODULE: $Name ($existingExtensionVersion)" -f Cyan } # Return the additional extension metadata that this function has populated $additionalMetadata = @{ Path = $existingExtensionPath Enabled = $true ResolvedVersion = $installedCommit } # Record which release tag a minimum version resolved to, so that the lock file can install it again if ($MinimumVersion) { $additionalMetadata.ResolvedRef = $GitRef } return $additionalMetadata } |