functions/Get-EndjinGist.ps1

# <copyright file="Get-EndjinGist.ps1" company="Endjin Limited">
# Copyright (c) Endjin Limited. All rights reserved.
# </copyright>

<#
.SYNOPSIS
    Downloads gist content from a Git repository using vendir.
 
.DESCRIPTION
    The Get-EndjinGist function downloads gist content from a specified group and name
    using the vendir tool. It reads available gists from a YAML configuration file,
    generates a vendir configuration, and synchronizes the content to a local directory.
 
.PARAMETER Group
    The group name containing the gist. This must match a key in the Gist Map configuration file.
 
.PARAMETER Name
    The name of the specific gist to download within the specified group.
 
.PARAMETER DestinationBaseDir
    The destination path for the downloaded gist content. The remainder of the destination path is
    derived from the Gist 'Group' and 'Name'.
 
.PARAMETER GistMapPath
    The path to a 'Gist Map' configuration file. Defaults to the configuration file
    distributed with the module (gist-map.yml).
 
.EXAMPLE
    Get-EndjinGist -Group "azure" -Name "deploy-script"
 
    Downloads the 'deploy-script' gist from the 'azure' group to the default location.
 
.EXAMPLE
    Get-EndjinGist -Group "common" -Name "logging" -Verbose
 
    Downloads the 'logging' gist from the 'common' group with verbose output enabled.
 
.NOTES
    The 'vendir' tool is resolved automatically: if found in the system PATH it
    is used directly; otherwise a module-local copy is downloaded on first use.
    The function creates a temporary .endjin-gists.yml file which is cleaned up
    after successful execution (unless Verbose mode is enabled).
 
.LINK
    https://carvel.dev/vendir/
#>

function Get-EndjinGist {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, Position=0)]
        [string] $Group,

        [Parameter(Mandatory, Position=1)]
        [string] $Name,

        [Parameter()]
        [string] $DestinationBaseDir = '.endjin',

        [Parameter()]
        [ValidateScript({Test-Path $_ -PathType Leaf})]
        [string] $GistMapPath = (Join-Path $PSScriptRoot '..' 'gist-map.yml')
    )

    begin {
        Set-StrictMode -Version Latest

        $vendirPath = _Get-VendirPath

        $availableGists = Get-Content -Path $GistMapPath -Raw | ConvertFrom-Yaml
        Write-Verbose "Available gists:`n$($availableGists | ConvertTo-Json -Depth 5)"

        if (!$availableGists.ContainsKey($Group)) {
            throw "Unknown gist group: $Group"
        }

        $vendirConfig = [ordered]@{
            apiVersion = 'vendir.k14s.io/v1alpha1'
            kind = 'Config'
            directories = @()
        }

        # Track gists that have movePaths for post-download processing
        $gistsWithMovePaths = [System.Collections.Generic.List[hashtable]]::new()
    }

    process {
        $gistConfig = $availableGists[$Group] | Where-Object { $_.name -eq $Name }
        if (!$gistConfig) {
            throw "Unknown gist '$Name' in group '$Group'"
        }

        Write-Information "⚙️ Processing '$Name' from group '$Group'"
        $directoryConfig =  [ordered]@{
            path = "$DestinationBaseDir/$Group/$Name"
            contents = @(
                @{
                    path = '.'
                    git = [ordered]@{
                        url = $gistConfig.source
                        ref = $gistConfig.ref
                    }
                    includePaths = [array]($gistConfig.includePaths)
                }
            )
        }

        if ($gistConfig.ContainsKey('sourceDirectory')) {
            $directoryConfig.contents[0] += @{ newRootPath = $gistConfig.sourceDirectory }
        }

        $vendirConfig.directories += [array]$directoryConfig

        # Track gists with movePaths for post-download processing
        if ($gistConfig.ContainsKey('movePaths')) {
            $gistsWithMovePaths.Add(@{
                Config = $gistConfig
                DestinationPath = "$DestinationBaseDir/$Group/$Name"
            })
        }
    }

    end {
        $vendirFilename = '.endjin-gists.yml'
        $outputPath = Join-Path $PWD $vendirFilename

        try {
            ConvertTo-Yaml -Data $vendirConfig -OutFile $outputPath -Force
            Write-Verbose "Generated vendir.yml:`n$(Get-Content -Raw $outputPath)"

            Write-Information "⌛ Downloading gists..."
            if ($PSCmdlet.ShouldProcess($vendirFilename, "Run vendir sync")) {
                $PSNativeCommandUseErrorActionPreference = $false
                $result = Invoke-Command { & $vendirPath sync --file $vendirFilename 2>&1 }
                $result | Write-Verbose

                if ($LASTEXITCODE -ne 0) {
                    if ($result -imatch 'Access is denied') {
                        Write-Host -f Red @"
Operation failed due to potential file contention with VSCode, please add the following to your VSCode settings:
`n`"files.watcherExclude`": {
`t`"**/.vendir-tmp-*/**`": true
}`n
"@

                        throw "vendir sync failed with an access denied error. If using VSCode, this may be caused by file watcher contention (see above).`n$result"
                    }
                    else {
                        throw "Operation failed with exit code $LASTEXITCODE.`n$result"
                    }
                }
            }

            # Process movePaths for gists that have them
            if ($gistsWithMovePaths.Count -gt 0) {
                # Determine repo root for ${repoRoot} variable expansion
                $repoRoot = & git rev-parse --show-toplevel 2>$null
                if (-not $repoRoot) {
                    $repoRoot = $PWD.Path
                }

                foreach ($gistInfo in $gistsWithMovePaths) {
                    $gistDestPath = Join-Path $PWD $gistInfo.DestinationPath

                    foreach ($movePath in $gistInfo.Config.movePaths) {
                        $fromPattern = $movePath.from
                        $toPath = $movePath.to -replace '\$\{repoRoot\}', $repoRoot

                        # Find the base directory from the pattern (part before wildcards)
                        $fromBase = ($fromPattern -split '[*?]')[0].TrimEnd('/', '\')
                        $fromBasePath = Join-Path $gistDestPath $fromBase

                        if (-not (Test-Path $fromBasePath)) {
                            Write-Verbose "Source path not found: $fromBasePath"
                            continue
                        }

                        # Handle file vs directory
                        if (Test-Path $fromBasePath -PathType Leaf) {
                            # Direct file move — no recursion needed
                            $destFile = $toPath
                            # If toPath looks like a directory (ends with / or \), append the filename
                            if ($toPath -match '[/\\]$') {
                                $destFile = Join-Path $toPath (Split-Path -Leaf $fromBasePath)
                            }
                            $destDir = Split-Path -Parent $destFile
                            if (-not (Test-Path $destDir)) {
                                New-Item -ItemType Directory -Path $destDir -Force | Out-Null
                            }
                            if ($PSCmdlet.ShouldProcess("$fromBasePath -> $destFile", "Move file")) {
                                Move-Item -Path $fromBasePath -Destination $destFile -Force
                                Write-Verbose "Moved: $fromBasePath -> $destFile"
                            }
                        }
                        else {
                            # Directory logic — Get-ChildItem -Recurse
                            $filesToMove = Get-ChildItem -Path $fromBasePath -File -Recurse -ErrorAction SilentlyContinue

                            foreach ($file in $filesToMove) {
                                # Calculate relative path from the 'from' base to preserve structure
                                $relativePath = [System.IO.Path]::GetRelativePath($fromBasePath, $file.FullName)
                                $destFile = Join-Path $toPath $relativePath

                                # Ensure destination directory exists
                                $destDir = Split-Path -Parent $destFile
                                if (-not (Test-Path $destDir)) {
                                    New-Item -ItemType Directory -Path $destDir -Force | Out-Null
                                }

                                # Move file (overwrites if exists)
                                if ($PSCmdlet.ShouldProcess("$($file.FullName) -> $destFile", "Move file")) {
                                    Move-Item -Path $file.FullName -Destination $destFile -Force
                                    Write-Verbose "Moved: $($file.FullName) -> $destFile"
                                }
                            }

                            # Clean up empty directories in source
                            if (Test-Path $fromBasePath) {
                                Get-ChildItem -Path $fromBasePath -Directory -Recurse |
                                    Sort-Object { $_.FullName.Length } -Descending |
                                    Where-Object { @(Get-ChildItem $_.FullName -Force).Count -eq 0 } |
                                    Remove-Item -Force

                                # Remove base dir if empty
                                if (@(Get-ChildItem $fromBasePath -Force).Count -eq 0) {
                                    Remove-Item $fromBasePath -Force
                                }
                            }
                        }
                    }

                    Write-Information "📁 Moved files for '$($gistInfo.Config.name)'"
                }
            }

            Write-Information "✅ Completed successfully."
        }
        finally {
            if ($VerbosePreference -eq 'SilentlyContinue') {
                Remove-Item $outputPath -ErrorAction SilentlyContinue
                Remove-Item (Join-Path (Split-Path -Parent $outputPath) 'vendir.lock.yml') -ErrorAction SilentlyContinue
            }
        }
    }
}
New-Alias -Name gist -Value Get-EndjinGist