Test-GitOpsDrift.ps1

Function Test-GitOpsDrift {
    param(
        [ValidateScript( {
                Test-Path $_ -PathType Container
            })]
        [Parameter()]
        [String]
        $Build = $env:TEMP,

        [ValidateScript( {
                Test-Path $_ -PathType Container
            })]
        [Parameter(Mandatory = $true)]
        [String]
        $Source,

        [Parameter(Mandatory = $true)]
        [String]
        $Destination,

        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $ToSession,

        [Parameter()]
        [String]
        $GitTag,

        [Parameter()]
        [Regex]
        $Exclude = '^.*\.md$',

        [Parameter()]
        [String]
        $TemplateExtension = ".eps1",

        [Parameter()]
        [String]
        $SpectoCommon = ".specto",

        [Parameter()]
        [String]
        $SpectoSignature
    )
    Process {
        $InvCmdParams = @{}
        If ($PSBoundParameters['ToSession']) {
            $InvCmdParams.Session = $ToSession
        }
        If (-not $(Invoke-Command -ScriptBlock { Test-Path $using:Destination } @InvCmdParams)) {
            Write-Error "$Destination directory not found."
        }
        $SourceDirectory = Get-Item $Source
        Write-Debug "$($SourceDirectory.FullName)"
        $BuildDirectory = Get-Item $Build
        $DestinationDirectory = Invoke-Command -ScriptBlock { Get-Item $using:Destination } @InvCmdParams
        Push-Location $SourceDirectory
        Try {
            $Files = @{}
            If ($PSBoundParameters['GitTag']) {
                git diff --relative --name-status $GitTag HEAD `
                | Tee-Object -Variable GitStatusCmdOutput `
                | Write-Debug
            }
            Else {
                git diff --relative --name-status HEAD^ HEAD `
                | Tee-Object -Variable GitStatusCmdOutput `
                | Write-Debug
            }
            ForEach ($Line in $GitStatusCmdOutput) {
                $GitStatusArray = $Line.Split("`t")
                Write-Debug $GitStatusArray.Count
                If ($GitStatusArray.Count -eq 3) {
                    $GitSrcFileInfo = [System.IO.FileInfo](Join-Path $SourceDirectory $GitStatusArray[2])
                    Write-Debug "$($GitSrcFileInfo | Select-Object -Property *)"
                    $GitSrcFile = $GitSrcFileInfo.FullName.Replace("$($SourceDirectory.FullName)", "")
                    $GitSrcFromFileInfo = [System.IO.FileInfo](Join-Path $SourceDirectory $GitStatusArray[1])
                    $Files["$GitSrcFile"] = @{
                        FullName = $GitSrcFileInfo.FullName
                        Parent = $GitSrcFileInfo.DirectoryName
                        FromFile  = $GitSrcFromFileInfo.FullName.Replace("$($SourceDirectory.FullName)", "")
                        GitStatus = $GitStatusArray[0]
                    }
                }
                ElseIf ($GitStatusArray.Count -eq 2) {
                    $GitSrcFileInfo = [System.IO.FileInfo](Join-Path $SourceDirectory $GitStatusArray[1])
                    Write-Debug "$($GitSrcFileInfo | Select-Object -Property *)"
                    $GitSrcFile = $GitSrcFileInfo.FullName.Replace("$($SourceDirectory.FullName)", "")
                    $Files["$GitSrcFile"] = @{
                        FullName = $GitSrcFileInfo.FullName
                        Parent = $GitSrcFileInfo.DirectoryName
                        FromFile  = $GitSrcFile
                        GitStatus = $GitStatusArray[0]
                    }
                }
            }
            Write-Debug "$($Files | ConvertTo-Json -Depth 100)"
            ForEach ($File in $($Files.GetEnumerator())) {
                $BuildFile = @{
                    FullName = Join-Path -Path $BuildDirectory.FullName -ChildPath $File.Key
                    Directory = Join-Path -Path $BuildDirectory.FullName -ChildPath $File.Value.Parent.Replace("$($SourceDirectory.FullName)", "")
                }
                $DestinationFile = @{
                    FullName = Join-Path -Path $DestinationDirectory.FullName -ChildPath $File.Key
                    Directory = Join-Path -Path $DestinationDirectory.FullName -ChildPath $File.Value.Parent.Replace("$($SourceDirectory.FullName)", "")
                }
                If ($File.Key.EndsWith($TemplateExtension)) {
                    $BuildFile.FullName = $BuildFile.FullName.Replace($TemplateExtension, "")
                    $DestinationFile.FullName = $DestinationFile.FullName.Replace($TemplateExtension, "")
                } ElseIf ($File.Key.Contains($SpectoCommon)) {
                    If ($SpectoSignature -and ($File.Key.EndsWith($SpectoSignature))) {
                        $BuildFile.FullName = $BuildFile.FullName.Replace($SpectoCommon, "").Replace($SpectoSignature, "")
                        $DestinationFile.FullName = $DestinationFile.FullName.Replace($SpectoCommon, "").Replace($SpectoSignature, "")
                    } Else {
                        Continue
                    }
                }
                $BuildFileHash = Get-FileHash $BuildFile.FullName -ErrorAction SilentlyContinue
                $DestinationFileHash = Invoke-Command -ScriptBlock { Get-FileHash $using:DestinationFile.FullName -ErrorAction SilentlyContinue } @InvCmdParams
                Switch ($File.Value.GitStatus) {
                    'A' {
                        # addition of a file
                        Write-Verbose "$($File.Key) was Added since last build, $($DestinationFile.FullName) hash should be null."
                        If ($null -ne $DestinationFilehash) {
                            Write-Warning "$($File.Key) was added but $($DestinationFile.FullName) already exists."
                        }
                        Break
                    }
                    'D' {
                        # deletion of a file
                        Write-Warning "$($File.Key) was Deleted since last build, consider deleting $($DestinationFile.FullName)."
                        Break
                    }
                    'M' {
                        # modification of the contents or mode of a file
                        Write-Warning "$($File.Key) was Modified since last build, $($DestinationFile.FullName) shoudl be different than Source and overwritten."
                        If ($null -ne $DestinationFilehash) {
                            Write-Warning "$($File.Key) was Modified but $($DestinationFile.FullName) was not found."
                        } ElseIf ($BuildFileHash.Hash -eq $DestinationFileHash.Hash) {
                            Write-Warning "$($File.Key) was Modified but $($DestinationFile.FullName) matches the hash of the $($Buildfile.FullName) file. File may have already been deployed."
                        }
                        Break
                    }
                    'R' {
                        # renaming of a file
                        Write-Verbose "$($File.Key) has been renamed from $($File.Value.FromFile). $($DestinationFile.FullName) should not exist. Any file generated from $($Destination.FullName) should be considered for deletion."
                        If ($null -ne $DestinationFileHash) {
                            Write-Warning "$($File.Key) was renamed from $($File.Value.FromFile), but $($DestinationFile.FullName) already exists."
                        }
                        Write-Warning "$($File.Key) was renamed from $($File.Value.FromFile). Consider deleting any file that was generated from $($File.Value.FromFile)."
                    }
                    Default {
                        # Default
                        Write-Warning "$($File.Key) has a status of $($File.Value.GitStatus), which is not handled."
                    }
                }
            }
            ForEach ($SourceFile in $(Get-ChildItem $SourceDirectory.FullName -Recurse -File)) {
                $SourceFileRelPath = $SourceFile.FullName.Replace($SourceDirectory.FullName, "")
                If ($Files.ContainsKey($SourceFileRelPath)) {
                    Write-Verbose "$SourceFileRelPath was already checked during Git Diff handling."
                    Continue
                }
                $BuildFile = @{
                    FullName  = Join-Path -Path $BuildDirectory.FullName -ChildPath $SourceFile.FullName.Replace($SourceDirectory.FullName, "")
                    Directory = Join-Path -Path $BuildDirectory.FullName -ChildPath $SourceFile.Directory.FullName.Replace($SourceDirectory.FullName, "")
                }
                $DestinationFile = @{
                    FullName  = Join-Path -Path $DestinationDirectory.FullName -ChildPath $SourceFile.FullName.Replace($SourceDirectory.FullName, "")
                    Directory = Join-Path -Path $DestinationDirectory.FullName -ChildPath $SourceFile.Directory.FullName.Replace($SourceDirectory.FullName, "")
                }
                If ($SourceFile.Name.EndsWith($TemplateExtension)) {
                    $BuildFile.FullName = $BuildFile.FullName.Replace($TemplateExtension, "")
                    $DestinationFile.FullName = $DestinationFile.FullName.Replace($TemplateExtension, "")
                }
                ElseIf ($SourceFile.Name.Contains($SpectoCommon)) {
                    If ($SpectoSignature -and ($SourceFile.Name.EndsWith($SpectoSignature))) {
                        $BuildFile.FullName = $BuildFile.FullName.Replace($SpectoCommon, "").Replace($SpectoSignature, "")
                        $DestinationFile.FullName = $DestinationFile.FullName.Replace($SpectoCommon, "").Replace($SpectoSignature, "")
                    } Else {
                        Continue
                    }
                }
                $BuildFileHash = Get-FileHash $BuildFile.FullName
                $DestinationFileHash = Invoke-Command -ScriptBlock { Get-FileHash $using:DestinationFile.FullName -ErrorAction SilentlyContinue } @InvCmdParams
                Write-Verbose "$SourceFileRelPath has no changes since the last build. $($BuildFile.FullName) should match $($DestinationFile.FullName)."
                If ($null -eq $DestinationFileHash) {
                    Write-Warning "$SourceFileRelPath has no changes since the last build. $($DesinationFile.FullName) was not found."
                }
                ElseIf ($($DestinationFileHash.Hash) -ne $($BuildFileHash.Hash)) {
                    Write-Warning "$SourceFileRelPath has no changes since the last build. $($BuildFile.FullName) does not match $($DestinationFile.FullName)."
                }
            }
        }
        Finally {
            Pop-Location
        }
    }
}