Scripts/TFS.ps1


function GetTfPath([string]$version)
{
    <#
    .SYNOPSIS
    Gets the path to the [tf.exe] tool which corresponds to the given VisualStudio version. The path
    depends on the installation location of VisualStudio for example "C:\Program Files (x86)\
    Microsoft Visual Studio\2017\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\
    Team Explorer\TF.exe".
    #>


    if(IsNullOrEmpty($version))
    {
        $version = $global:VsVersion
    }

    $vsPath = GetVsPath $version
    $path = "$vsPath\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer\TF.exe"
    return $path
}

function CanConnectToTfs([string]$tfsUrl, [string]$version)
{
    <#
    .SYNOPSIS
    Checks whether a connection to the given TFS server is possible.
    .PARAMETER tfsUrl
    The URL of the TFS server. [Optional]
    .PARAMETER version
    The VisualStudio version from which the [tf.exe] path is derived. [Optional]
    #>


    if(IsNullOrEmpty($tfsUrl))
    {
        $tfsUrl = $global:TfsUrl
    }

    if(IsNullOrEmpty($version))
    {
        $version = $global:VsVersion
    }

    $tf = GetTfPath -version "$version"

    $result = ExecuteProcess -fileName "$tf" -arguments "workspaces /collection:$tfsUrl /noprompt" -maxTime 30000
    $success = ($result.ExitCode -eq 0)
    return $success

    # Alternative:
    # & "$TfPath" workspaces /collection:$tfsUrl /noprompt *>$null
}

function GetChangedProjects()
{
    <#
    .SYNOPSIS
    Gets all cs projects impacted by the local pending changes.
    #>


    $result = GetPendingChanges
    $files = $result.Files
    $projects = New-Object System.Collections.Generic.HashSet[string]
    foreach($file in $files)
    {
        while(!(IsNullOrEmpty $file) -And (ExistsPath $file))
        {
            $file = Split-Path -parent $file
            $projectFile = Get-ChildItem -Path $file -Filter *.csproj
            $isProjectDir = !(IsNullOrEmpty $projectFile)
            if($isProjectDir)
            {
                $null = $projects.Add($projectFile.FullName)
                break
            }
        }        
    }
    Add-Member -InputObject $result -NotePropertyName Projects -NotePropertyValue $projects
    return $result
}

function ConvertServerPathToLocalPath([string]$serverPath, [string]$serverWorkspace, [string]$localWorkspace)
{
    <#
    .SYNOPSIS
    Converts a server path to a local path.
    For example the server path "$/TIA Portal/TPE/dev/Step7_Safety_T/src/S7F/FEngine" for server
    workspace "$/TIA Portal/TPE/dev/Step7_Safety_T" and local workspace "D:\WS\Step7_Safety_T" is
    converted to "D:\WS\Step7_Safety_T\src\S7F\FEngine".
    #>


    if(-not $serverWorkspace)
    {
        $serverWorkspace = $global:ServerWorkspace
    }

    if(-not $localWorkspace)
    {
        $localWorkspace = $global:LocalWorkspace
    }

    $result = $null
    if($serverPath.ToLower().StartsWith($serverWorkspace.ToLower()))
    {
        $localPath = $serverPath.SubString($serverWorkspace.Length)
        $localPath = $localPath.Replace("/","\")
        $result = $localWorkspace + $localPath
    }
    return $result
}

function ConvertLocalPathToServerPath([string]$localPath, [string]$serverWorkspace, [string]$localWorkspace)
{
    if(-not $serverWorkspace)
    {
        $serverWorkspace = $global:ServerWorkspace
    }

    if(-not $localWorkspace)
    {
        $localWorkspace = $global:LocalWorkspace
    }

    $result = $null
    if($localPath.ToLower().StartsWith($localWorkspace.ToLower()))
    {
        $serverPath = $localPath.SubString($localWorkspace.Length)
        $serverPath = $serverPath.Replace("\","/")
        $result = $serverWorkspace + $serverPath
    }
    return $result
}

function ConvertGetOutputToChangeList([string]$output)
{
    $lines = $output -split "`r`n"
    #Write-Output "Line Count:" $lines.Count
    #$lines = $lines | Where-Object { $_.Length -gt 0 }
    #Write-Output "Filled Line Count:" $lines.Count
    $localFolder = $null
    #$localFolderCount = 0
    $changeList = New-Object Collections.Generic.List[PsCustomObject]
    foreach($line in $lines)
    {
        if($line.Length -gt 0)
        {
            $foundChange = $false
            $changeType = $null
            $localPath = $null

            if($line.ToLower().StartsWith("getting "))
            {
                $foundChange = $true
                $changeType = "add"
                $localPath = $localFolder + "\" + $line.SubString(8)

                # $item = $line.SubString(8)
                # $changeList.Add(@{Path = $localFolder + "\" + $item; Adjective = "getting"})
            }
            elseif($line.ToLower().StartsWith("replacing "))
            {
                $foundChange = $true
                $changeType = "edit"
                $localPath = $localFolder + "\" + $line.SubString(10)

                # $item = $line.SubString(10)
                # $changeList.Add(@{Path = $localFolder + "\" + $item; Adjective = "replacing"})
            }
            elseif($line.ToLower().StartsWith("deleting "))
            {
                $foundChange = $true
                $changeType = "delete"
                $localPath = $localFolder + "\" + $line.SubString(9)

                # $item = $line.SubString(9)
                # $changeList.Add(@{Path = $localFolder + "\" + $item; Adjective = "deleting"})
            }
            elseif($line.EndsWith(":"))
            {
                $localFolder = $line.SubString(0, $line.Length - 1)
                #$localFolderCount++
            }

            if($foundChange)
            {
                $changeList.Add([PsCustomObject]@{LocalPath = $localPath; ChangeType = $changeType})
            }
        }
    }
    # Write-Output "ParentCount:" $parentCount
    # $gettingCount = ($changeList | Where-Object { $_.Adjective -eq "getting" }).Count
    # $replacingCount = ($changeList | Where-Object { $_.Adjective -eq "replacing" }).Count
    # $deletingCount = ($changeList | Where-Object { $_.Adjective -eq "deleting" }).Count
    # Write-Output "GettingCount:" $gettingCount
    # Write-Output "ReplacingCount:" $replacingCount
    # Write-Output "DeletingCount:" $deletingCount
    # $totalCount = $gettingCount + $replacingCount + $deletingCount + $parentCount
    # Write-Output "TotalCount:" $totalCount
    return $changeList
}

function ConvertPendingChangesOutputToChangeList([string]$output)
{
    $lines = $output -split "`r`n"
    # Variable changes contains the changed items. An items is either a files or a folder.
    $changes = New-Object Collections.Generic.List[PsCustomObject]
    # Holds the index where in a line the change type (add, edit, delete) begins.
    $changeIndex = 0
    # Holds the last server folder. In case of a deleted item, the last server folder can be
    # used to find the local folder from which the item has been deleted.
    $serverFolder = ""
    # Holds the count of added items.
    $addCount = 0
    # Holds the count of edited items.
    $editCount = 0
    # Holds the count of deleted items.
    $deleteCount = 0
    foreach($line in $lines)
    {
        if($changeIndex -eq 0)
        {
            if($line.Length -gt 0)
            {
                $index = $line.ToLower().IndexOf("change")
                if($index -gt 0)
                {
                    $changeIndex = $index
                }
            }
        }
        elseif($line.Length -ge $changeIndex)
        {
            if($line.StartsWith("$"))
            {
                $serverFolder = $line
            }
            else
            {
                $itemName = $line.SubString(0, $changeIndex).Trim()
                $line = $line.SubString($changeIndex)
                $parts = $line -split "\s+"
                if($parts -ge 1)
                {
                    $changeType = $parts[0]
                    $foundChange = $false
                    if($changeType -eq "edit")
                    {
                        $foundChange = $true
                        $localPath =  $parts[1]
                        $editCount++
                    }
                    elseif($changeType -eq "add")
                    {
                        $foundChange = $true
                        $localPath =  $parts[1]
                        $addCount++
                    }
                    elseif($changeType -eq "delete")
                    {
                        $foundChange = $true
                        if($localPath.Length -eq 0)
                        {
                            $localFolder = ConvertServerPathToLocalPath -ServerPath $serverFolder
                            $localPath = $localFolder + "\" + $itemName
                        }
                        $deleteCount++
                    }

                    if($foundChange)
                    {
                        $changes.Add([PsCustomObject]@{LocalPath = $localPath; ChangeType = $changeType})
                    }
                }    
            }
        }
    }
    return $changes
}

function GetPendingChanges()
{
    <#
    .SYNOPSIS
    Gets all pending changes on ALL workspaces.
    #>


    $tf = GetTfPath
    $result = ExecuteProcess -fileName "$tf" -arguments "vc status /collection:$TfsUrl" -maxTime 30000
    
    # Add-Member -InputObject $result -NotePropertyName AddCount -NotePropertyValue $addCount
    # Add-Member -InputObject $result -NotePropertyName EditCount -NotePropertyValue $editCount
    # Add-Member -InputObject $result -NotePropertyName DeleteCount -NotePropertyValue $deleteCount
    # Add-Member -InputObject $result -NotePropertyName Changes -NotePropertyValue $changes
    return $result
}

function PreviewGetLatest([string]$rootDir)
{
    <#
    .SYNOPSIS
    Checks the given directory for server-side changes.
    #>


    $tf = GetTfPath
    $result = ExecuteProcess -fileName "$tf" -arguments "get `"$rootDir`" /recursive /overwrite /noprompt /preview"
    return $result
}

function GetLatest([string]$rootDir)
{
    <#
    .SYNOPSIS
    Gets all server-side changes from the given directory.
    #>


    $tf = GetTfPath
    $result = ExecuteProcess -fileName "$tf" -arguments "get `"$rootDir`" /recursive /overwrite /noprompt"
    return $result
}

function PreviewGetLatestByVersion([string]$rootDir, [string]$itemVersion)
{
    <#
    .SYNOPSIS
    Previews a [tf get] of all server-side changes in the given directory regarding the given item
    version. The item version can be a changeset, a date or a label. If the item version is not
    specified, the latest item version is used.
    #>


    if(IsNullOrEmpty($itemVersion))
    {
        $itemVersion = "T"
    }

    $tf = GetTfPath
    $command = "get `"$rootDir`" /version:$itemVersion /recursive /overwrite /noprompt /preview"
    #Write-Host "command: $command"
    $result = ExecuteProcess -fileName "$tf" -arguments "$command"
    return $result
}

function GetLatestByVersion([string]$rootDir, [string]$itemVersion)
{
    <#
    .SYNOPSIS
    Performs a [tf get] of all server-side changes in the given directory regarding the given item
    version. The item version can be a changeset, a date or a label. If the item version is not
    specified, the latest item version is used.
    #>


    if(IsNullOrEmpty($itemVersion))
    {
        $itemVersion = "T"
    }

    $tf = GetTfPath
    $command = "get `"$rootDir`" /version:$itemVersion /recursive /overwrite /noprompt"
    #Write-Host "command: $command"
    $result = ExecuteProcess -fileName "$tf" -arguments "$command"
    return $result
}

function UndoPendingChanges([string]$localWorkspace, [string]$workspaceName)
{
    <#
    .SYNOPSIS
    Undoes the pending changes in the given local workspace and the given workspace name.
    #>


    if(IsNullOrEmpty($workspaceName))
    {
        $workspaceName = $global:WorkspaceName
    }

    if(IsNullOrEmpty($localWorkspace))
    {
        $localWorkspace = $global:LocalWorkspace
    }

    SetCurrentDirectory($localWorkspace)

    $tf = GetTfPath
    $result = ExecuteProcess -fileName "$tf" -arguments "undo * /recursive /noprompt /workspace:`"$workspaceName`""
    return $result
}


function ScorchWorkspace([string]$localWorkspace)
{
    <#
    .SYNOPSIS
    Scorches the local workspace.
    #>


    if(IsNullOrEmpty($localWorkspace))
    {
        $localWorkspace = $global:LocalWorkspace
    }

    SetCurrentDirectory($localWorkspace)

    $tf = GetTfPath
    $result = ExecuteProcess -fileName "$tf" -arguments "vc scorch /exclude:binaries /noprompt"
    return $result
}

function PreviewScorchWorkspace([string]$localWorkspace)
{
    <#
    .SYNOPSIS
    Previews a scorch of the local workspace.
    #>


    if(IsNullOrEmpty($localWorkspace))
    {
        $localWorkspace = $global:LocalWorkspace
    }

    SetCurrentDirectory($localWorkspace)

    $currentDir = GetCurrentDirectory
    Write-Host "CurrentDir: $currentDir"


    # Unrooted folders named "binaries" e.g. "D:\WS\Step7_Safety_T\packages\SomePack\Binaries" will not be deleted.
    # Even "/exclude:\binaries" does not help here.
    $tf = GetTfPath
    $result = ExecuteProcess -fileName "$tf" -arguments "vc scorch /exclude:binaries /noprompt /preview"
    return $result
}

function CleanWorkspace([string]$localWorkspace)
{
    <#
    .SYNOPSIS
    Cleans the local workspace via reconcile command.
    #>


    if(IsNullOrEmpty($localWorkspace))
    {
        $localWorkspace = $global:LocalWorkspace
    }

    SetCurrentDirectory($localWorkspace)

    $tf = GetTfPath
    $result = ExecuteProcess -fileName "$tf" -arguments "reconcile /clean /recursive /ignore /noprompt"
    return $result
}

function ExecuteProcess ([string]$fileName, [string]$arguments, [int]$maxTime)
{
    <#
    .SYNOPSIS
        Executes the given process. The process is forced to exit if the maximum time has elapsed.
    .PARAMETER fileName
        Path or name of the process to execute.
    .PARAMETER arguments
        Arguments passed to the process.
    .PARAMETER maxTime
        Maximum time in milliseconds to wait for the process to finish. The parameter must be
        greater than 0, otherwise the process has no time limit.
    #>


    $info = New-Object System.Diagnostics.ProcessStartInfo
    $info.FileName = "$fileName"
    $info.Arguments = "$arguments"
    $info.RedirectStandardError = $true
    $info.RedirectStandardOutput = $true
    $info.UseShellExecute = $false
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $info
    [void]$p.Start()
    $standardOutput = $p.StandardOutput.ReadToEnd()
    $standardError = $p.StandardError.ReadToEnd()
    $exitedInTime = $true
    if($maxTime -gt 0)
    {
        $exitedInTime = $p.WaitForExit($maxTime)
    }
    else
    {
        $p.WaitForExit()
    }
    $exitCode = $p.ExitCode
    return [PsCustomObject]@{
        StandardOutput = $standardOutput
        StandardError = $standardError
        ExitCode = $exitCode
        ExitedInTime = $exitedInTime
    }
}

function GetExitCodeString([int]$exitCode)
{
    <#
    Gets the description of an exit code produced by one of the
    TFVC methods.
    #>

    switch($exitCode)
    {
        0 { "Command succeeded" }
        1 { "Command succeeded partially" }
        2 { "Command is unrecognized" }
        100 { "Command failed" }
        default { "Exitcdoe is undefined" }
    }
}