SCOrchDev-GitIntegration.psm1

#requires -Version 3 -Modules SCOrchDev-Exception, SCOrchDev-File, SCOrchDev-Utility
if(-not $env:Path -match '([^;]*Git\\cmd);')
{
    Throw-Exception -Type 'gitExeNotFound' `
                    -Message 'Could not find the git executable in the local computer''s path'
}
$gitEXE = 'git.exe'

<#
    .Synopsis
        Tags a current tag line and compares it to the passed
        commit and repository. If the commit is not the same
        update the tag line and return new version
     
    .Parameter TagLine
        The current tag string from an SMA runbook
 
    .Parameter CurrentCommit
        The current commit string
 
    .Parameter RepositoryName
        The name of the repository that is being processed
#>

Function New-ChangesetTagLine
{
    Param([Parameter(Mandatory=$false)][string] $TagLine = [string]::EmptyString,
          [Parameter(Mandatory=$true)][string]  $CurrentCommit,
          [Parameter(Mandatory=$true)][string]  $RepositoryName)

    $NewVersion = $False
    if(($TagLine -as [string]) -match 'CurrentCommit:([^;]+);')
    {
        if($Matches[1] -ne $CurrentCommit)
        {
            $NewVersion = $True
            $TagLine = $TagLine.Replace($Matches[1],$CurrentCommit) 
        }
    }
    else
    {
        Write-Verbose -Message "[$TagLine] Did not have a current commit tag."
        $TagLine = "CurrentCommit:$($CurrentCommit);$($TagLine)"
        $NewVersion = $True
    }
    if(($TagLine -as [string]) -match 'RepositoryName:([^;]+);')
    {
        if($Matches[1] -ne $RepositoryName)
        {
            $NewVersion = $True
            $TagLine = $TagLine.Replace($Matches[1],$RepositoryName) 
        }
    }
    else
    {
        Write-Verbose -Message "[$TagLine] Did not have a RepositoryName tag."
        $TagLine = "RepositoryName:$($RepositoryName);$($TagLine)"
        $NewVersion = $True
    }
    return (ConvertTo-JSON -InputObject @{'TagLine' = $TagLine ;
                                          'NewVersion' = $NewVersion } `
                           -Compress)
}
<#
    .Synopsis
        Returns all variables in a JSON settings file
 
    .Parameter FilePath
        The path to the JSON file containing settings
#>

Function Get-GlobalFromFile
{
    Param([Parameter(Mandatory=$false)]
          [string] 
          $FilePath = [string]::EmptyString,
          
          [ValidateSet('Variables','Schedules')]
          [Parameter(Mandatory=$false)]
          [string] 
          $GlobalType = 'Variables')

    $ReturnInformation = @{}
    try
    {
        $SettingsJSON = (Get-Content -Path $FilePath) -as [string]
        $SettingsObject = ConvertFrom-Json -InputObject $SettingsJSON
        $SettingsHashTable = ConvertFrom-PSCustomObject -InputObject $SettingsObject
        
        if(-not ($SettingsHashTable.ContainsKey($GlobalType)))
        {
            Throw-Exception -Type 'GlobalTypeNotFound' `
                            -Message 'Global Type not found in settings file.' `
                            -Property @{ 'FilePath' = $FilePath ;
                                         'GlobalType' = $GlobalType ;
                                         'SettingsJSON' = $SettingsJSON }
        }

        $GlobalTypeObject = $SettingsHashTable."$GlobalType"
        $GlobalTypeHashTable = ConvertFrom-PSCustomObject -InputObject $GlobalTypeObject -ErrorAction SilentlyContinue

        if(-not $GlobalTypeHashTable)
        {
            Throw-Exception -Type 'SettingsNotFound' `
                            -Message 'Settings of specified type not found in file' `
                            -Property @{ 'FilePath' = $FilePath ;
                                         'GlobalType' = $GlobalType ;
                                         'SettingsJSON' = $SettingsJSON }
        }

        foreach($Key in $GlobalTypeHashTable.Keys)
        {
            $ReturnInformation.Add($key, $GlobalTypeHashTable."$Key") | Out-Null
        }
                
    }
    catch
    {
        Write-Exception -Exception $_ -Stream Warning
    }

    return (ConvertTo-JSON -InputObject $ReturnInformation -Compress)
}
<#
    .Synopsis
        Updates a Global RepositoryInformation string with the new commit version
        for the target repository
 
    .Parameter RepositoryInformation
        The JSON representation of a repository
 
    .Parameter RepositoryName
        The name of the repository to update
 
    .Paramter Commit
        The new commit to store
#>

Function Set-RepositoryInformationCommitVersion
{
    Param([Parameter(Mandatory=$false)][string] $RepositoryInformation = [string]::EmptyString,
          [Parameter(Mandatory=$false)][string] $RepositoryName = [string]::EmptyString,
          [Parameter(Mandatory=$false)][string] $Commit = [string]::EmptyString)
    
    $_RepositoryInformation = (ConvertFrom-JSON -InputObject $RepositoryInformation)
    $_RepositoryInformation."$RepositoryName".CurrentCommit = $Commit

    return (ConvertTo-Json -InputObject $_RepositoryInformation -Compress)
}
Function Get-GitRepositoryWorkflowName
{
    Param([Parameter(Mandatory=$false)][string] $Path = [string]::EmptyString)

    $RunbookNames = @()
    $RunbookFiles = Get-ChildItem -Path $Path `
                                  -Filter '*.ps1' `
                                  -Recurse `
                                  -File
    foreach($RunbookFile in $RunbookFiles)
    {
        $RunbookNames += Get-WorkflowNameFromFile -FilePath $RunbookFile.FullName
    }
    $RunbookNames
}
Function Get-GitRepositoryVariableName
{
    Param([Parameter(Mandatory=$false)][string] $Path = [string]::EmptyString)

    $RunbookNames = @()
    $RunbookFiles = Get-ChildItem -Path $Path `
                                  -Filter '*.json' `
                                  -Recurse `
                                  -File
    foreach($RunbookFile in $RunbookFiles)
    {
        $RunbookNames += Get-WorkflowNameFromFile -FilePath $RunbookFile.FullName
    }
    Return $RunbookNames
}
Function Get-GitRepositoryAssetName
{
    Param([Parameter(Mandatory=$false)][string] $Path = [string]::EmptyString)

    $Assets = @{ 'Variable' = @() ;
                 'Schedule' = @() }
    $AssetFiles = Get-ChildItem -Path $Path `
                                  -Filter '*.json' `
                                  -Recurse `
                                  -File
    
    foreach($AssetFile in $AssetFiles)
    {
        $VariableJSON = Get-GlobalFromFile -FilePath $AssetFile.FullName -GlobalType Variables
        $ScheduleJSON = Get-GlobalFromFile -FilePath $AssetFile.FullName -GlobalType Schedules
        if($VariableJSON)
        {
            Foreach($VariableName in (ConvertFrom-PSCustomObject -InputObject (ConvertFrom-JSON -InputObject $VariableJSON)).Keys)
            {
                $Assets.Variable += $VariableName
            }
        }
        if($ScheduleJSON)
        {
            Foreach($ScheduleName in (ConvertFrom-PSCustomObject -InputObject (ConvertFrom-JSON -InputObject $ScheduleJSON)).Keys)
            {
                $Assets.Schedule += $ScheduleName
            }
        }
    }
    Return $Assets
}
<#
    .Synopsis
        Groups all files that will be processed.
        # TODO put logic for import order here
    .Parameter Files
        The files to sort
    .Parameter RepositoryInformation
#>

Function Group-RepositoryFile
{
    Param([Parameter(Mandatory=$True)] $Files,
          [Parameter(Mandatory=$True)] $RepositoryInformation)
    Write-Verbose -Message 'Starting [Group-RepositoryFile]'
    $_Files = ConvertTo-Hashtable -InputObject $Files -KeyName FileExtension
    $ReturnObj = @{ 'ScriptFiles' = @() ;
                    'SettingsFiles' = @() ;
                    'ModuleFiles' = @() ;
                    'CleanRunbooks' = $False ;
                    'CleanAssets' = $False ;
                    'CleanModules' = $False ;
                    'ModulesUpdated' = $False }

    # Process PS1 Files
    try
    {
        $PowerShellScriptFiles = ConvertTo-HashTable -InputObject $_Files.'.ps1' -KeyName 'FileName'
        Write-Verbose -Message 'Found Powershell Files'
        foreach($ScriptName in $PowerShellScriptFiles.Keys)
        {
            if($PowerShellScriptFiles."$ScriptName".ChangeType -contains 'M' -or
               $PowerShellScriptFiles."$ScriptName".ChangeType -contains 'A')
            {
                foreach($Path in $PowerShellScriptFiles."$ScriptName".FullPath)
                {
                    if($Path -like "$($RepositoryInformation.Path)\$($RepositoryInformation.RunbookFolder)\*")
                    {
                        $ReturnObj.ScriptFiles += $Path
                        break
                    }
                }            
            }
            else
            {
                $ReturnObj.CleanRunbooks = $True
            }
        }
    }
    catch
    {
        Write-Verbose -Message 'No Powershell Files found'
    }
    try
    {
        # Process Settings Files
        $SettingsFiles = ConvertTo-HashTable -InputObject $_Files.'.json' -KeyName 'FileName'
        Write-Verbose -Message 'Found Settings Files'
        foreach($SettingsFileName in $SettingsFiles.Keys)
        {
            if($SettingsFiles."$SettingsFileName".ChangeType -contains 'M' -or
               $SettingsFiles."$SettingsFileName".ChangeType -contains 'A')
            {
                foreach($Path in $SettingsFiles."$SettingsFileName".FullPath)
                {
                    if($Path -like "$($RepositoryInformation.Path)\$($RepositoryInformation.GlobalsFolder)\*")
                    {
                        $ReturnObj.CleanAssets = $True
                        $ReturnObj.SettingsFiles += $Path
                        break
                    }
                }
            }
            else
            {
                $ReturnObj.CleanAssets = $True
            }
        }
    }
    catch
    {
        Write-Verbose -Message 'No Settings Files found'
    }
    try
    {
        $PSModuleFiles = ConvertTo-HashTable -InputObject $_Files.'.psd1' -KeyName 'FileName'
        Write-Verbose -Message 'Found Powershell Module Files'
        foreach($PSModuleName in $PSModuleFiles.Keys)
        {
            if($PSModuleFiles."$PSModuleName".ChangeType -contains 'M' -or
               $PSModuleFiles."$PSModuleName".ChangeType -contains 'A')
            {
                foreach($Path in $PSModuleFiles."$PSModuleName".FullPath)
                {
                    if($Path -like "$($RepositoryInformation.Path)\$($RepositoryInformation.PowerShellModuleFolder)\*")
                    {
                        $ReturnObj.ModulesUpdated = $True
                        $ReturnObj.ModuleFiles += $Path
                        break
                    }
                }
            }
            else
            {
                $ReturnObj.CleanModules = $True
            }
        }
    }
    catch
    {
        Write-Verbose -Message 'No Powershell Module Files found'
    }
    Write-Verbose -Message 'Finished [Group-RepositoryFile]'
    Return (ConvertTo-JSON -InputObject $ReturnObj -Compress)
}
<#
    .Synopsis
        Groups a list of Runbooks by the RepositoryName from the
        tag line
#>

Function Group-RunbooksByRepository
{
    Param([Parameter(Mandatory=$True)] $InputObject)
    ConvertTo-Hashtable -InputObject $InputObject `
                        -KeyName 'Tags' `
                        -KeyFilterScript { 
                            Param($KeyName)
                            if($KeyName -match 'RepositoryName:(.+)')
                            {
                                $Matches[1]
                            }
                        }
}
<#
    .Synopsis
        Groups a list of Runbooks by the RepositoryName from the
        tag line
#>

Function Group-AssetsByRepository
{
    Param([Parameter(Mandatory=$True)] $InputObject)
    ConvertTo-Hashtable -InputObject $InputObject `
                        -KeyName 'Description' `
                        -KeyFilterScript { 
                            Param($KeyName)
                            if($KeyName -match 'RepositoryName:([^;]+);')
                            {
                                $Matches[1]
                            }
                        }
}
<#
    .Synopsis
        Check the target Git Repo / Branch for any updated files.
        Ingores files in the root
     
    .Parameter RepositoryInformation
        The PSCustomObject containing repository information
#>

Function Find-GitRepositoryChange
{
    Param([Parameter(Mandatory=$true) ] $RepositoryInformation)
    
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    
    # Set current directory to the git repo location
    $CurrentLocation = Get-Location
    try
    {
        Set-Location -Path $RepositoryInformation.Path

        $ReturnObj = @{ 'CurrentCommit' = $RepositoryInformation.CurrentCommit;
                        'Files' = @() }
    
        $NewCommit = (Invoke-Expression -Command "$($gitExe) rev-parse --short HEAD") -as  [string]
        $FirstRepoCommit = (Invoke-Expression -Command "$($gitExe) rev-list --max-parents=0 HEAD") -as [string]
        $StartCommit = (Select-FirstValid -Value $RepositoryInformation.CurrentCommit, $FirstRepoCommit -FilterScript { $_ -ne -1 }) -as [string]
        $ModifiedFiles = Invoke-Expression -Command "$($gitExe) diff --name-status $StartCommit $NewCommit"
        $ReturnObj = @{ 'CurrentCommit' = $NewCommit ; 'Files' = @() }
        Foreach($File in $ModifiedFiles)
        {
            if("$($File)" -Match '([a-zA-Z])\s+(.+\/([^\./]+(\..+)))$')
            {
                $ReturnObj.Files += @{ 'FullPath' = "$($RepositoryInformation.Path)\$($Matches[2].Replace('/','\'))" ;
                                       'FileName' = $Matches[3] ;
                                       'FileExtension' = $Matches[4].ToLower()
                                       'ChangeType' = $Matches[1] }
            }
        }
    }
    catch
    {
        throw
    }
    finally
    {
        Set-Location -Path $CurrentLocation
    }
    
    return (ConvertTo-Json -InputObject $ReturnObj -Compress)
}
<#
    .Synopsis
        Updates a git repository to the latest version
     
    .Parameter RepositoryInformation
        The PSCustomObject containing repository information
#>

Function Update-GitRepository
{
    Param([Parameter(Mandatory=$true) ] $RepositoryInformation)
    
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    Write-Verbose -Message 'Starting [Update-GitRepository]'
    # Set current directory to the git repo location
    if(-Not (Test-Path -Path $RepositoryInformation.Path))
    {
        $null = New-FileItemContainer -FileItemPath $RepositoryInformation.Path
        Try
        {
            Write-Verbose -Message 'Cloneing repository'
            Invoke-Expression -Command "$gitEXE clone $($RepositoryInformation.RepositoryPath) $($RepositoryInformation.Path) --recursive"
        }
        Catch
        {
            Write-Exception -Exception $_ -Stream Warning 
        }
        
    }
    $CurrentLocation = Get-Location
    Set-Location -Path $RepositoryInformation.Path

    $BranchResults = (Invoke-Expression -Command "$gitEXE branch") -as [string]
    if(-not ($BranchResults -match '\*\s(\w+)'))
    {
        if(Test-IsNullOrEmpty -String $BranchResults)
        {
            Write-Verbose -Message 'git branch did not return output. Assuming we are on the correct branch'
        }
        else
        {
            Throw-Exception -Type 'GitTargetBranchNotFound' `
                            -Message 'git could not find any current branch' `
                            -Property @{ 'result' = $BranchResults ;
                                         'match'  = $BranchResults -match '\*\s(\w+)'}
        }
    }
    elseif($Matches[1] -ne $RepositoryInformation.Branch)
    {
        Write-Verbose -Message "Setting current branch to [$($RepositoryInformation.Branch)]"
        try
        {
            Write-Verbose -Message "Changing branch to [$($RepositoryInformation.Branch)]"
            (Invoke-Expression -Command "$gitEXE checkout $($RepositoryInformation.Branch)") | Out-Null
        }
        catch
        {
            Write-Exception -Exception $_ -Stream Warning
        }
    }
    
    try
    {
        $initialization = Invoke-Expression -Command "$gitEXE pull"
    }
    catch
    {
        $Exception = $_
        $ExceptionInformation = Get-ExceptionInfo -Exception $Exception
        Switch($ExceptionInformation.Type)
        {
            'System.Management.Automation.RemoteException'
            {
                Write-Verbose -Message "Retrieved updates $($ExceptionInformation.Message)"
            }
            Default
            {
                Write-Exception -Exception $Exception -Stream Warning
            }
        }
    }
    
    try
    {
        $initialization = Invoke-Expression -Command "$gitEXE submodule init"
    }
    catch
    {
        Write-Exception -Exception $_ -Stream Warning
    }

    try
    {
        $initialization = Invoke-Expression -Command "$gitEXE submodule update"
    }
    catch
    {
        Write-Exception -Exception $_ -Stream Warning
    }
    
    Set-Location -Path $CurrentLocation
    Write-Verbose -Message 'Finished [Update-GitRepository]'
}
Export-ModuleMember -Function * -Verbose:$false -Debug:$False