_Private/_Private.ps1

function _GetAbsolutePath 
{
    [CmdletBinding()]
    Param
    (
        $Path, 

        [switch]$CreateFolder
    )

    Process
    {
        $Path = [System.IO.Path]::Combine($pwd.Path, $Path)
        $Path = [System.IO.Path]::GetFullPath($Path)

        $folder = Split-Path $Path -Parent

        if ((-not (Test-Path $folder)) -and ($CreateFolder.IsPresent))
        {
            New-Item $folder -ItemType Directory -Force | _Log
        }

        return $Path
    }
}
Function _GetJsonPatchDocument
{
    [CmdletBinding()]
    [OutputType([Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchDocument])]
    Param
    (
        $operations
    )
    
    $doc = New-Object 'Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchDocument'

    foreach($op in $operations)
    {
        if ($op -is [Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchOperation])
        {
            $doc.Add($op)
            continue
        }

        $jsonOp = New-Object 'Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchOperation'
        $jsonOp.Operation = $op.Operation
        $jsonOp.Path = $op.Path
        $jsonOp.Value = $op.Value

        $doc.Add($jsonOp)
    }

    Write-Output -NoEnumerate $doc
}
Function _GetRegistryValue
{
    Param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        $Path,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        $Value
    )

    Process
    {
        return Get-ItemProperty -Path $Path | Select-Object -ExpandProperty $Value
    }
}
Function _GetRestClient
{
    [CmdletBinding()]
    [OutputType('Microsoft.VisualStudio.Services.WebApi.VssHttpClientBase')]
    Param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [string]
        $Type,

        [Parameter()]
        [object] 
        $Collection
    )

    Process
    {
        $tpc = Get-TfsTeamProjectCollection -Collection $Collection

        return _InvokeGenericMethod -InputObject $tpc -MethodName GetClient -GenericType $Type
    }
}
Function _GetStructureGroup($StructureGroup)
{
    Write-Warning $MyInvocation.InvocationName

    if($StructureGroup -is [Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.TreeStructureGroup])
    {
        _Log "StructureGroup already set to '$StructureGroup'; returning"

        return $StructureGroup
    }

    _Log "Getting structure group from call stack"
    
    foreach($frame in Get-PSCallStack)
    {
        _Log "Command '$($frame.Command)'"

        if ($frame.Command.EndsWith('Area'))
        {
            return [Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.TreeStructureGroup]::Areas
        }
        elseif ($frame.Command.EndsWith('Iteration'))
        {
            return [Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.TreeStructureGroup]::Iterations
        }
    }

    throw "Invalid or missing StructureGroup argument"
}
Function _ImportRequiredAssembly($assemblyName)
{
    $libPath = (Join-Path $PSScriptRoot "../lib" -Resolve)

    if($assemblyName -eq '*')
    {
        $assemblies = (Get-ChildItem "$libPath/*.dll" -Exclude 'Microsoft.WitDataStore*.*','TfsCmdletsLib.dll').BaseName
        $assemblies += (Get-ChildItem "$libPath/TfsCmdletsLib.dll").BaseName
    }
    else
    {
        $assemblies = (Get-ChildItem "$libPath/$assemblyName.dll").BaseName
    }
    
    foreach($asm in $assemblies)
    {
        Write-Verbose "Loading assembly $asm from folder $libPath"

        try
        {
            Add-Type -Path (Join-Path $libPath "$asm.dll")
        }
        catch
        {
            Write-Warning "Error loading assembly '$asm': $($_.Exception.Message)"
        }
    }
}

Function _TestLoadedAssembly($assemblyName)
{
    try
    {
        $asm = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -like "$assemblyName,*"

        return $asm -is [System.Reflection.Assembly]
    }
    catch
    {
        return $false
    }
}
function _InvokeGenericMethod
{
    [CmdletBinding(DefaultParameterSetName = 'Instance')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Instance')]
        $InputObject,

        [Parameter(Mandatory = $true, ParameterSetName = 'Static')]
        [Type]
        $Type,

        [Parameter(Mandatory = $true)]
        [string]
        $MethodName,

        [Parameter(Mandatory = $true)]
        [Type[]]
        $GenericType,

        [Object[]]
        $ArgumentList
    )

    process
    {
        switch ($PSCmdlet.ParameterSetName)
        {
            'Instance'
            {
                $_type  = $InputObject.GetType()
                $object = $InputObject
                $flags  = [System.Reflection.BindingFlags] 'Instance, Public'
            }

            'Static'
            {
                $_type  = $Type
                $object = $null
                $flags  = [System.Reflection.BindingFlags] 'Static, Public'
            }
        }

        if ($null -ne $ArgumentList)
        {
            $argList = $ArgumentList.Clone()
        }
        else
        {
            $argList = @()
        }

        $params = @{
            Type         = $_type
            BindingFlags = $flags
            MethodName   = $MethodName
            GenericType  = $GenericType
            ArgumentList = [ref]$argList
        }

        $method = _GetGenericMethod @params

        if ($null -eq $method)
        {
            Write-Error "No matching method was found"
            return
        }

        return [PSGenericMethods.MethodInvoker]::InvokeMethod($method, $object, $argList)
    } 
}

function _GetGenericMethod
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Type]
        $Type,

        [Parameter(Mandatory = $true)]
        [string]
        $MethodName,

        [Parameter(Mandatory = $true)]
        [Type[]]
        $GenericType,

        [ref]
        $ArgumentList,

        [System.Reflection.BindingFlags]
        $BindingFlags = [System.Reflection.BindingFlags]::Default,

        [switch]
        $WithCoercion
    )

    if ($null -eq $ArgumentList.Value)
    {
        $originalArgList = @()
    }
    else
    {
        $originalArgList = @($ArgumentList.Value)
    }

    foreach ($method in $Type.GetMethods($BindingFlags))
    {
        $argList = $originalArgList.Clone()

        if (-not $method.IsGenericMethod -or $method.Name -ne $MethodName) { continue }
        if ($GenericType.Count -ne $method.GetGenericArguments().Count) { continue }

        if (_TestGenericMethodParameters -MethodInfo $method -ArgumentList ([ref]$argList) -GenericType $GenericType -WithCoercion:$WithCoercion)
        {
            $ArgumentList.Value = $argList
            return $method.MakeGenericMethod($GenericType)
        }
    }

    if (-not $WithCoercion)
    {
        $null = $PSBoundParameters.Remove('WithCoercion')
        return _GetGenericMethod @PSBoundParameters -WithCoercion
    }

}

function _TestGenericMethodParameters
{
    [CmdletBinding()]
    param (
        [System.Reflection.MethodInfo] $MethodInfo,

        [ref]
        $ArgumentList,

        [Parameter(Mandatory = $true)]
        [Type[]]
        $GenericType,

        [switch]
        $WithCoercion
    )

    if ($null -eq $ArgumentList.Value)
    {
        $argList = @()
    }
    else
    {
        $argList = @($ArgumentList.Value)
    }

    $parameterList = $MethodInfo.GetParameters()

    $arrayType = $null

    $_HasParamsArray = _HasParamsArray -ParameterList $parameterList

    if ($parameterList.Count -lt $argList.Count -and -not $_HasParamsArray)
    {
        return $false
    }

    $methodGenericType = $MethodInfo.GetGenericArguments()

    for ($i = 0; $i -lt $argList.Count; $i++)
    {
        $params = @{
            ArgumentList       = $argList
            ParameterList      = $ParameterList
            WithCoercion       = $WithCoercion
            RuntimeGenericType = $GenericType
            MethodGenericType  = $methodGenericType
            Index              = [ref]$i
            ArrayType          = [ref]$arrayType
        }

        $isOk = _TryMatchParameter @params

        if (-not $isOk) { return $false }
    }

    $defaults = New-Object System.Collections.ArrayList

    for ($i = $argList.Count; $i -lt $parameterList.Count; $i++)
    {
        if (-not $parameterList[$i].HasDefaultValue)  { return $false }
        $null = $defaults.Add($parameterList[$i].DefaultValue)
    }

    # When calling a method with a params array using MethodInfo, you have to pass in the array; the
    # params argument approach doesn't work.

    if ($_HasParamsArray)
    {
        $firstArrayIndex = $parameterList.Count - 1
        $lastArrayIndex = $argList.Count - 1

        $newArgList = $argList[0..$firstArrayIndex]
        $newArgList[$firstArrayIndex] = $argList[$firstArrayIndex..$lastArrayIndex] -as $arrayType
        $argList = $newArgList
    }

    $ArgumentList.Value = $argList + $defaults.ToArray()

    return $true

} 

function _TryMatchParameter
{
    param (
        [System.Reflection.ParameterInfo[]]
        $ParameterList,

        [object[]]
        $ArgumentList,

        [Type[]]
        $MethodGenericType,

        [Type[]]
        $RuntimeGenericType,

        [switch]
        $WithCoercion,

        [ref] $Index,
        [ref] $ArrayType
    )

    $params = @{
        ParameterType = $ParameterList[$Index.Value].ParameterType
        RuntimeType   = $RuntimeGenericType
        GenericType   = $MethodGenericType
    }

    $runtimeType = _ResolveRuntimeType @params

    if ($null -eq $runtimeType)
    {
        throw "Could not determine runtime type of parameter '$($ParameterList[$Index.Value].Name)'"
    }

    $_IsParamsArray = _IsParamsArray -ParameterInfo $ParameterList[$Index.Value]

    if ($_IsParamsArray)
    {
        $ArrayType.Value = $runtimeType
        $runtimeType     = $runtimeType.GetElementType()
    }

    do
    {
        $isOk = _TryMatchArgument @PSBoundParameters -RuntimeType $runtimeType
        if (-not $isOk) { return $false }

        if ($_IsParamsArray) { $Index.Value++ }
    }
    while ($_IsParamsArray -and $Index.Value -lt $ArgumentList.Count)

    return $true
}

function _TryMatchArgument
{
    param (
        [System.Reflection.ParameterInfo[]]
        $ParameterList,

        [object[]]
        $ArgumentList,

        [Type[]]
        $MethodGenericType,

        [Type[]]
        $RuntimeGenericType,

        [switch]
        $WithCoercion,

        [ref] $Index,
        [ref] $ArrayType,

        [Type] $RuntimeType
    )

    Function _GetType($object)
    {
        if ($null -eq $object) { return $null }
        return $object.GetType()
    }

    $argValue = $ArgumentList[$Index.Value]
    $argType = _GetType $argValue

    $isByRef = $RuntimeType.IsByRef
    if ($isByRef)
    {
        if ($ArgumentList[$Index.Value] -isnot [ref]) { return $false }

        $RuntimeType = $RuntimeType.GetElementType()
        $argValue = $argValue.Value
        $argType = _GetType $argValue
    }

    $isNullNullable = $false

    while ($RuntimeType.FullName -like 'System.Nullable``1*')
    {
        if ($null -eq $argValue)
        {
            $isNullNullable = $true
            break
        }

        $RuntimeType = $RuntimeType.GetGenericArguments()[0]
    }

    if ($isNullNullable) { continue }

    if ($null -eq $argValue)
    {
        return -not $RuntimeType.IsValueType
    }
    else
    {
        if ($argType -ne $RuntimeType)
        {
            $argValue = $argValue -as $RuntimeType
            if (-not $WithCoercion -or $null -eq $argValue)  { return $false }
        }

        if ($isByRef)
        {
            $ArgumentList[$Index.Value].Value = $argValue
        }
        else
        {
            $ArgumentList[$Index.Value] = $argValue
        }
    }

    return $true
}
function _HasParamsArray([System.Reflection.ParameterInfo[]] $ParameterList)
{
    return $ParameterList.Count -gt 0 -and (_IsParamsArray -ParameterInfo $ParameterList[-1])
}

function _IsParamsArray([System.Reflection.ParameterInfo] $ParameterInfo)
{
    return @($ParameterInfo.GetCustomAttributes([System.ParamArrayAttribute], $true)).Count -gt 0
}

function _ResolveRuntimeType
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Type]
        $ParameterType,

        [Parameter(Mandatory = $true)]
        [Type[]]
        $RuntimeType,

        [Parameter(Mandatory = $true)]
        [Type[]]
        $GenericType
    )

    if ($ParameterType.IsByRef)
    {
        $elementType = _ResolveRuntimeType -ParameterType $ParameterType.GetElementType() -RuntimeType $RuntimeType -GenericType $GenericType
        return $elementType.MakeByRefType()
    }
    elseif ($ParameterType.IsGenericParameter)
    {
        for ($i = 0; $i -lt $GenericType.Count; $i++)
        {
            if ($ParameterType -eq $GenericType[$i])
            {
                return $RuntimeType[$i]
            }
        }
    }
    elseif ($ParameterType.IsArray)
    {
        $arrayType = $ParameterType
        $elementType = _ResolveRuntimeType -ParameterType $ParameterType.GetElementType() -RuntimeType $RuntimeType -GenericType $GenericType

        if ($ParameterType.GetElementType().IsGenericParameter)
        {
            $arrayRank = $arrayType.GetArrayRank()

            if ($arrayRank -eq 1)
            {
                $arrayType = $elementType.MakeArrayType()
            }
            else
            {
                $arrayType = $elementType.MakeArrayType($arrayRank)
            }
        }

        return $arrayType
    }
    elseif ($ParameterType.ContainsGenericParameters)
    {
        $genericArguments = $ParameterType.GetGenericArguments()
        $runtimeArguments = New-Object System.Collections.ArrayList

        foreach ($argument in $genericArguments)
        {
            $null = $runtimeArguments.Add((_ResolveRuntimeType -ParameterType $argument -RuntimeType $RuntimeType -GenericType $GenericType))
        }

        $definition = $ParameterType
        if (-not $definition.IsGenericTypeDefinition)
        {
            $definition = $definition.GetGenericTypeDefinition()
        }

        return $definition.MakeGenericType($runtimeArguments.ToArray())
    }
    else
    {
        return $ParameterType
    }
}

if (-not ([System.Management.Automation.PSTypeName]'PSGenericMethods.MethodInvoker').Type)
{
    Add-Type -ErrorAction SilentlyContinue -TypeDefinition @'
    namespace PSGenericMethods
    {
        using System;
        using System.Reflection;
        using System.Management.Automation;
 
        public static class MethodInvoker
        {
            public static object InvokeMethod(MethodInfo method, object target, object[] arguments)
            {
                if (method == null) { throw new ArgumentNullException("method"); }
 
                object[] args = null;
 
                if (arguments != null)
                {
                    args = (object[])arguments.Clone();
                    for (int i = 0; i < args.Length; i++)
                    {
                        PSObject pso = args[i] as PSObject;
                        if (pso != null)
                        {
                            args[i] = pso.BaseObject;
                        }
 
                        PSReference psref = args[i] as PSReference;
 
                        if (psref != null)
                        {
                            args[i] = psref.Value;
                        }
                    }
                }
 
                object result = method.Invoke(target, args);
 
                for (int i = 0; i < arguments.Length; i++)
                {
                    PSReference psref = arguments[i] as PSReference;
 
                    if (psref != null)
                    {
                        psref.Value = args[i];
                    }
                }
 
                return result;
            }
        }
    }
'@

}
Function _InvokeScriptBlock($ScriptBlock, $Computer, $Credentials, $ArgumentList)
{
    if (-not $Computer)
    {
        return Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $ArgumentList
    }
    elseif ($Computer -is [System.Management.Automation.Runspaces.PSSession])
    {
        return Invoke-Command -ScriptBlock $scriptBlock -Session $Computer -ArgumentList $ArgumentList
    }

    return Invoke-Command -ScriptBlock $scriptBlock -ComputerName $Computer -Credential $Credential -ArgumentList $ArgumentList
}
Function _IsWildcard($Item)
{
    return $Item -match '\*|\[.+\]'
}
Function _Log
{
    Param
    (
        [Parameter(ValueFromPipeline=$true)]
        [object]
        $Message,

        [Parameter()]
        [string]
        $Caller,

        [Parameter()]
        [switch]
        $Force
    )

    if(($VerbosePreference -ne 'Continue') -and (-not $Force.IsPresent))
    {
        # No verbose set. Exit now to avoid expensive/unnecessary calls to Get-PSCallStack and Write-Verbose
        return
    }

    if(-not $Caller)
    {
        $caller = _GetLogCallStack
    }

    Write-Verbose "[$([DateTime]::Now.ToString('HH:mm:ss.ffff'))] [$caller] $Message"
}

Function _GetLogCallStack
{
    $cs = [System.Collections.Stack]::new()

    foreach($frame in Get-PSCallStack)
    {
        if ($frame.Command -in @('_Log', '_GetLogCallStack', '', $null))
        {
            continue
        }
        
        $cs.Push($frame.Command.Trim('_'))

        if ($frame.Command -like '*-*')
        {
            break
        }
    }

    return $cs.ToArray() -join ':'
}

Function _DumpObj
{
    Param
    (
        [Parameter(ValueFromPipeline=$true, Position=0)]
        [object]
        $InputObject,

        [Parameter()]
        $Depth = 5
    )

    return $InputObject | ConvertTo-Json -Depth $Depth -Compress
}
function _NewDictionary
{
    #requires -Version 2.0

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=1)]
        [ValidateNotNull()]
        [hashtable]
        $InputObject,

        [Parameter(Position=0)]
        [ValidateCount(2,2)]
        [Type[]]
        $Types = @([string], [object])
    )

    process
    {
        $KeyType = $Types[0]
        $ValueType = $Types[1]

        $outputObject = New-Object "System.Collections.Generic.Dictionary[[$($KeyType.FullName)],[$($ValueType.FullName)]]"

        foreach ($entry in $InputObject.GetEnumerator())
        {
            $newKey = $entry.Key -as $KeyType
            
            if ($null -eq $newKey)
            {
                throw 'Could not convert key "{0}" of type "{1}" to type "{2}"' -f
                      $entry.Key,
                      $entry.Key.GetType().FullName,
                      $KeyType.FullName
            }
            elseif ($outputObject.ContainsKey($newKey))
            {
                throw "Duplicate key `"$newKey`" detected in input object."
            }

            $outputObject.Add($newKey, $entry.Value -as $ValueType)
        }

        Write-Output $outputObject
    }
}
Function _NewScriptBlock($EntryPoint, [string[]]$Dependency)
{
    $entryPoint = (Get-Item "function:$EntryPoint").Definition.Trim()
    $paramSection = $entryPoint.Substring(0, $entryPoint.IndexOf("`n"))
    $bodySection = $entryPoint.Substring($paramSection.Length) + "`n`n"
    
    $body = $paramSection

    foreach($depFn in $Dependency)
    {
        $f = Get-Item "function:$depFn"

        $body += "Function $f `n{`n"
        $body += $f.Definition 
        $body += "`n}`n`n"
    }

    $body += $bodySection

    return [scriptblock]::Create($body)
}
Function _NormalizeNodePath
{
    Param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [AllowEmptyString()]
        [string]
        $Path, 

        [Parameter(Mandatory=$true)]
        [string]
        $Project, 

        [Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.TreeStructureGroup]
        $Scope, 

        [switch]
        $IncludeScope,

        [switch]
        $ExcludePath,

        [switch]
        $IncludeLeadingBackslash,

        [switch]
        $IncludeTrailingBackslash,

        [switch]
        $IncludeTeamProject
    )

    _Log "Normalizing path '$Path' with arguments $(_DumpObj $PSBoundParameters)"

    $newPath = ''

    $scopeName = $Scope.ToString().TrimEnd('s')

    if ($IncludeLeadingBackslash) { $newPath += '\' }
    if ($IncludeTeamProject) { $newPath += $Project + '\' }
    if ($IncludeScope) { $newPath += $scopeName + '\' }

    if(-not $ExcludePath.IsPresent)
    {
        $Path = $Path.Trim(' ', '\')

        if ($Path -like "$Project\$scopeName\*")
        {
            $Path = $Path.Substring("$Project\$scopeName\".Length)
        }
        if ($Path -like "$Project\*")
        {
            $Path = $Path.Substring($Path.IndexOf('\'))
        }
        elseif ($Path -eq $Project)
        {
            $Path = ''
        }

        $newPath += $Path
    }

    if ($newPath.EndsWith('\') -and (-not $IncludeTrailingBackslash.IsPresent))
    { 
        $newPath = $newPath.TrimEnd('\')
    }

    _Log "Normalized path: $newPath"

    return $newPath -replace '\\{2,}', '\'
}
Function _RegisterAssemblyResolver
{
    if ($TfsCmdletsDebugStartup)
    {
        Write-Host "Entering TfsCmdlets startup debug mode" -ForegroundColor DarkYellow
    
        # For some reason, setting VerbosePreference here breaks the script. Using Set-Alias to work around it
        Set-Alias Write-Verbose Write-Host -Option Private
    }
    
    Write-Verbose 'Registering custom Assembly Resolver'

    if (-not ([System.Management.Automation.PSTypeName]'TfsCmdlets.AssemblyResolver').Type)
    {
        Write-Verbose "Compiling $PSEdition version of the assembly resolver"

        $sourcePath = (Join-Path $PSScriptRoot "../_cs/$($PSEdition)AssemblyResolver.cs" -Resolve)
        $sourceText = (Get-Content $sourcePath -Raw)

        Add-Type -TypeDefinition $sourceText -Language CSharp

        $libPath = (Join-Path $PSScriptRoot '../Lib' -Resolve)
        $assemblies = [System.Collections.Generic.Dictionary[string,string]]::new()

        Write-Verbose "Enumerating assemblies from $libPath"

        foreach($f in (Get-ChildItem $libPath -Filter '*.dll'))
        {
            Write-Verbose "Adding $f to list of private assemblies"
            $assemblies.Add($f.BaseName, $f.FullName)
        }

        Write-Verbose 'Calling AssemblyResolver.Register()'
        [TfsCmdlets.AssemblyResolver]::Register($assemblies, [bool]($TfsCmdletsDebugStartup))
    }
    else
    {
        Write-Verbose 'Custom Assembly Resolver already registered; skipping'
    }
}
Function _TestGuid([string]$guid)
{
    if([string]::IsNullOrEmpty($guid))
    {
        return $false
    }

    $parsedGuid = [guid]::Empty

    return [guid]::TryParse($guid, [ref] $parsedGuid)
}
Function _TestRegistryValue
{
    Param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        $Path,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        $Value
    )

    Process
    {
        try
        {
            _GetRegistryValue -Path $Path -Value $Value | Out-Null
            return $true
        }
        finally {}

        return $false

    }
}
Function _Throw
{
    Param
    (
        [Parameter(ValueFromPipeline=$true, Position=0)]
        [object]
        $Message,

        [Parameter(Position=1)]
        [object]
        $Exceptions
    )

    $caller = (Get-PSCallStack)[1].Command

    if ($Exceptions)
    {
        $Message += "`n`nAdditional error information: $($Exceptions | ForEach-Object{ "$_"})"
    }

    throw "[$caller] $Message`n`n"
}