functions/Set-GitBranch.ps1

function Set-GitBranch
{
    <#
    .SYNOPSIS
    Checks out the specified branches for the specified repository.
 
    .DESCRIPTION
    Checks out the specified branches for the specified repository.
    An optional script block may be passed in containing git commands to be executed against each specified branch after it has been checked out. The command in the script block will be split by semi-colon (default) or a user-defined separator, and each command run in turn.
    The output of the command has three components: header (in the form "<RepoName> | <BranchName>"), command, and results. Each section can be directed to the host, the pipeline, or suppressed. By default, the header, command, and results are only written when GitScript is used.
 
    .PARAMETER RepoName
    The name of the git repository that has the specified branches.
    This should match the directory name of one of the repositories defined in the $GitRepoPath module variable. If there is no match, a warning is generated.
    When the parameter is omitted, the current repository will be used if currently inside a repository; otherwise, nothing is returned.
 
    .PARAMETER BranchName
    The names of the branches to be checked out.
    These should match existing branches in the specified repository.
 
    .PARAMETER SetLocation
    Sets the working directory to the top-level directory of the specified repository.
    In the case where multiple RepoName values are passed in, the location will reflect the repository that was specified last.
 
    .PARAMETER GitScript
    Used to provide git commands that will be executed against the specified branches.
    The default command separator is the semi-colon (";").
    An alternative separator can be specified with the GitScriptSeparator parameter.
    A literal separator character can be specified with a backtick escape e.g. "`;".
 
    .PARAMETER HeaderOut
    Used to suppress, direct, or color the background of the header output.
    When the value is 'None', the header will be suppressed.
    When the value is 'Pipe', the header will be sent to the pipeline.
    When the value is a standard Powershell color, the header will be written to the host with a background in that color.
    When GitScript is provided, default is 'DarkGray'; otherwise it is 'None'.
 
    .PARAMETER CommandOut
    Used to suppress, direct, or color the command output.
    When the value is 'None', the command will be suppressed.
    When the value is 'Pipe', the command will be sent to the pipeline.
    When the value is a standard Powershell color, the command will be written to the host in that color.
    Default is 'Green'.
 
    .PARAMETER ResultsOut
    Used to suppress, direct, or color the results output.
    When the value is 'None', the results will be suppressed.
    When the value is 'Pipe', the results will be sent to the pipeline.
    When the value is 'Native', the results will be written to the host using both native git colors and git output streams.
    When the value is a standard Powershell color, the results will be written to the host in that color.
    Default is 'DarkGray'.
 
    .PARAMETER GitScriptSeparator
    An alternative separator for splitting commands passed in to the GitScript parameter.
    If an empty string ('') or $null is passed in, no splitting will occur i.e. the script will execute as a single statement.
    The default separator is a semi-colon (";").
 
    .EXAMPLE
    ## Check out a branch from outside a repository without naming a repository ##
 
    PS C:\> $GitRepoPath = 'C:\PowdrgitExamples\MyToolbox;C:\PowdrgitExamples\Project1' # to ensure the repository paths are defined
    PS C:\> Set-GitBranch -BranchName main
 
    # Nothing was returned because the current location is not inside a repository.
 
    .EXAMPLE
    ## Call from outside a repository for non-existent repository ##
 
    PS C:\> $GitRepoPath = 'C:\PowdrgitExamples\MyToolbox;C:\PowdrgitExamples\Project1' # to ensure the repository paths are defined
    PS C:\> Set-GitBranch -RepoName NonExistentRepo -BranchName main
    WARNING: [Set-GitBranch]Repository 'NonExistentRepo' not found. Check the repository directory has been added to the $GitRepoPath module variable.
 
    .EXAMPLE
    ## Check out a branch from outside a repository by naming a repository ##
 
    PS C:\> $GitRepoPath = 'C:\PowdrgitExamples\MyToolbox;C:\PowdrgitExamples\Project1' # to ensure the repository paths are defined
    PS C:\> Set-GitBranch -RepoName MyToolbox -BranchName feature1
 
    # Nothing was returned, but the specified branch is now checked out for the specified repository.
 
    # To confirm the checkout:
    PS C:\> Get-GitBranch -RepoName MyToolbox -Current | Format-Table -Property RepoName,BranchName,IsCheckedOut,IsRemote
 
    RepoName BranchName IsCheckedOut IsRemote
    -------- ---------- ------------ --------
    MyToolbox feature1 True False
 
    # Checkout main branch:
    PS C:\> Set-GitBranch -RepoName MyToolbox -BranchName main
 
    # To confirm the checkout:
    PS C:\> Get-GitBranch -RepoName MyToolbox -Current | Format-Table -Property RepoName,BranchName,IsCheckedOut,IsRemote
 
    RepoName BranchName IsCheckedOut IsRemote
    -------- ---------- ------------ --------
    MyToolbox main True False
 
    .EXAMPLE
    ## Check out a branch from outside a repository and use SetLocation parameter ##
 
    PS C:\> $GitRepoPath = 'C:\PowdrgitExamples\MyToolbox;C:\PowdrgitExamples\Project1' # to ensure the repository paths are defined
    PS C:\> Set-GitBranch -RepoName MyToolbox -BranchName main -SetLocation
    PS C:\PowdrgitExamples\MyToolbox>
 
    # Nothing was returned, but the specified branch is now checked out for the specified repository
    # Also, because the SetLocation switch was used, the current location (reflected in the prompt) changed to the repository's top-level directory.
 
    .EXAMPLE
    ## Check out a branch from outside a repository and use GitScript to run a git command against the branch ##
 
    PS C:\> $GitRepoPath = 'C:\PowdrgitExamples\MyToolbox;C:\PowdrgitExamples\Project1' # to ensure the repository paths are defined
    PS C:\> Set-GitBranch -RepoName MyToolbox -BranchName main -GitScript 'git pull'
    MyToolbox | main
    git pull
    Already up to date.
 
    .EXAMPLE
    ## Use GitScript to run a git command against a branch and capture the only the git output in a variable ##
 
    PS C:\> $GitRepoPath = 'C:\PowdrgitExamples\MyToolbox;C:\PowdrgitExamples\Project1' # to ensure the repository paths are defined
    PS C:\> $gitPullOutput = $null
    PS C:\> $gitPullOutput = Set-GitBranch -RepoName MyToolbox -BranchName main -GitScript 'git pull' -ResultsOut Pipe
    MyToolbox | main
    git pull
    PS C:\> $gitPullOutput
    Already up to date.
 
    # The header and command output were still seen in the host as they were not suppressed with the HeaderOut and CommandOut parameters.
 
    .EXAMPLE
    ## Use GitScript to run a git command against a branch and suppress all output ##
 
    PS C:\> $GitRepoPath = 'C:\PowdrgitExamples\MyToolbox;C:\PowdrgitExamples\Project1' # to ensure the repository paths are defined
    PS C:\> Set-GitBranch -RepoName MyToolbox -BranchName main -GitScript 'git pull' -HeaderOut None -CommandOut None -ResultsOut None
    PS C:\>
 
    # No output was seen in the host as it was suppressed with the HeaderOut, CommandOut, and ResultsOut parameters.
 
    .EXAMPLE
    ## Run a git command against multiple branches ##
 
    PS C:\> $GitRepoPath = 'C:\PowdrgitExamples\MyToolbox;C:\PowdrgitExamples\Project1' # to ensure the repository paths are defined
    PS C:\> $branchesToPull = Get-GitBranch -RepoName MyToolbox | Where-Object BranchName -in 'feature1','release' | Select-Object -ExpandProperty BranchName
    PS C:\> Set-GitBranch -RepoName MyToolbox -BranchName $branchesToPull -GitScript 'git pull'
    MyToolbox | feature1
    git pull
    Already up to date.
    MyToolbox | release
    git pull
    Already up to date.
    PS C:\>
 
    # The command passed to the script block parameter was executed against each branch stored in the $branchesToPull variable.
 
    .EXAMPLE
    ## Run a git command against multiple branches in multiple repositories ##
 
    PS C:\> $GitRepoPath = 'C:\PowdrgitExamples\MyToolbox;C:\PowdrgitExamples\Project1' # to ensure the repository paths are defined
    PS C:\> Get-GitRepo | Get-GitBranch | Set-GitBranch -GitScript 'git status'
    MyToolbox | feature1
    git status
    On branch feature1
    Your branch is ahead of 'origin/feature1' by 1 commit.
    (use "git push" to publish your local commits)
 
    nothing to commit, working tree clean
    MyToolbox | feature3
    git status
    On branch feature3
    nothing to commit, working tree clean
    MyToolbox | main
    git status
    On branch main
    Your branch is ahead of 'origin/main' by 3 commits.
    (use "git push" to publish your local commits)
 
    nothing to commit, working tree clean
    MyToolbox | release
    git status
    On branch release
    Your branch is up to date with 'origin/release'.
 
    nothing to commit, working tree clean
    Project1 | main
    git status
    On branch main
    nothing to commit, working tree clean
    Project1 | newfeature
    git status
    On branch newfeature
    nothing to commit, working tree clean
 
    # By piping the results of Get-GitRepo | Get-GitBranch into Set-GitBranch, we can see the status of all branches in all repositories in a single command.
 
    .INPUTS
    [System.String]
    Accepts string objects via the RepoName parameter. The output of Get-GitBranch can be piped into Set-GitTag.
 
    .OUTPUTS
    [System.String]
    When output is present, returns String objects.
 
    .NOTES
    Author : nmbell
 
    .LINK
    about_powdrgit
    .LINK
    Get-GitBranch
    .LINK
    Get-GitRepo
    .LINK
    Invoke-GitExpression
    #>


    # Use cmdlet binding
    [CmdletBinding(
      HelpURI = 'https://github.com/nmbell/powdrgit/blob/main/help/Set-GitBranch.md'
    )]

    # Declare parameters
    Param(

         [Parameter(
          Mandatory                       = $false
        , Position                        = 0
        , ValueFromPipeline               = $true
        , ValueFromPipelineByPropertyName = $true
        )]
        [ArgumentCompleter({
            Param ($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters)
            Get-GitRepo -Verbose:$false `
                | Select-Object -ExpandProperty RepoName `
                | Where-Object { $_ -like "$wordToComplete*" } `
                | Sort-Object
        })]
        [String]
        $RepoName

    ,    [Parameter(
          Mandatory                       = $true
        , Position                        = 1
        , ValueFromPipeline               = $false
        , ValueFromPipelineByPropertyName = $true
        , HelpMessage                     = 'Enter the name of a branch that exists in the repository'
        )]
        [ArgumentCompleter({
            Param ($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters)
            $argRepoName = $fakeBoundParameters.RepoName
            If (!($fakeBoundParameters.ContainsKey('RepoName')))
            {
                $argRepoName = Get-GitRepo -Verbose:$false -Current | Select-Object -ExpandProperty RepoName
            }
            Get-GitBranch -Verbose:$false -RepoName $argRepoName -IncludeRemote `
                | Select-Object -ExpandProperty BranchName `
                | Where-Object { $_ -like "$wordToComplete*" }
        })]
        [String[]]
        $BranchName

    ,    [Switch]
        $SetLocation

    ,    [ValidateNotNullOrEmpty()]
        [String]
        $GitScript

    ,    [ArgumentCompleter({
            Param ($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters)
            @('None','Pipe')+[Enum]::GetValues([System.ConsoleColor]) | Where-Object { $_ -like "$wordToComplete*" }
        })]
        [String]
        $HeaderOut

    ,    [ArgumentCompleter({
            Param ($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters)
            @('None','Pipe')+[Enum]::GetValues([System.ConsoleColor]) | Where-Object { $_ -like "$wordToComplete*" }
        })]
        [String]
        $CommandOut = 'Green'

    ,    [ArgumentCompleter({
            Param ($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters)
            @('None','Pipe','Native')+[Enum]::GetValues([System.ConsoleColor]) | Where-Object { $_ -like "$wordToComplete*" }
        })]
        [String]
        $ResultsOut = 'DarkGray'

    ,    [String]
        $GitScriptSeparator = ';'

    )

    BEGIN
    {
        $wvBlock          = 'B'

        # Common BEGIN:
        Set-StrictMode -Version 2.0
        $thisFunctionName = $MyInvocation.InvocationName
        $start            = Get-Date
        $wvIndent         = '| '*($PowdrgitCallDepth++)
        Write-Verbose "$(wvTimestamp)$wvIndent[$thisFunctionName][$wvBlock]Start: $($start.ToString('yyyy-MM-dd HH:mm:ss.fff'))"

        # Function BEGIN:
        Write-Verbose "$(wvTimestamp)$wvIndent[$thisFunctionName][$wvBlock]Storing current location"
        Push-Location -StackName SetGitBranch
        If (!$HeaderOut)
        {
            $HeaderOut = 'None'
            If ($GitScript) { $HeaderOut = 'DarkGray' }
        }
    }

    PROCESS
    {
        $wvBlock = 'P'

        # Find the repository name from current location
        If (!$RepoName) { $RepoName = Get-GitRepo -Current | Select-Object -ExpandProperty RepoName }

        # Go to the repository and get the repository info
        $repo = Set-GitRepo -RepoName $RepoName -PassThru -WarningAction SilentlyContinue

        # Set the branch
        If ($repo)
        {
            # Validate parameters
            $validBranches   = @()
            $invalidBranches = @()
            $allBranches     = Get-GitBranch -RepoName $RepoName -IncludeRemote -WarningAction SilentlyContinue | Select-Object -ExpandProperty BranchName
            $validBranches   = $BranchName | Where-Object { $_ -in    $allBranches }
            $invalidBranches = $BranchName | Where-Object { $_ -notin $allBranches }
            If ($invalidBranches)
            {
                Write-Warning ("[$thisFunctionName]The following are not valid branches in the '$RepoName' repository: $($invalidBranches -join ',')") -ErrorAction Stop
            }

            ForEach ($branch in $validBranches)
            {
                Write-Verbose "$(wvTimestamp)$wvIndent[$thisFunctionName][$wvBlock]Checking out branch $branch"
                $gitCommand = "git checkout $branch"
                $gitResults = Invoke-GitExpression -Command $gitCommand
                $currentBranch = Get-GitBranch -Current | Select-Object -ExpandProperty BranchName
                Write-Verbose "$(wvTimestamp)$wvIndent[$thisFunctionName][$wvBlock]Current branch is $currentBranch"
                If ($currentBranch -ne $branch)
                {
                    $gitResults | Out-String | Write-Host
                    Write-Warning "[$thisFunctionName]Failed to checkout branch $branch`:"
                }
                Else
                {
                    Write-Verbose "$(wvTimestamp)$wvIndent[$thisFunctionName][$wvBlock]Writing header"
                    Write-GitBranchOut -OutputType Header -OutputValue "$RepoName | $branch" -OutputStream $HeaderOut

                    If ($GitScript)
                    {
                        Write-Verbose "$(wvTimestamp)$wvIndent[$thisFunctionName][$wvBlock]Executing script block"
                        If (!$GitScriptSeparator) { $GitScriptSeparator = 'z%G1+$jNMuU%XUCASoPf312osOOjMHCOnh+kn3Ke' } # some unlikely to occur string
                        $GitScript = $GitScript.Replace('`'+$GitScriptSeparator,'<separator>') # to preserve escaped separators
                        $gitScriptLines = ($GitScript) -split $GitScriptSeparator

                        ForEach ($line in $gitScriptLines | Where-Object { $_.Trim() })
                        {
                            $line = $line.Replace('<separator>',$GitScriptSeparator).Trim()

                            Write-Verbose "$(wvTimestamp)$wvIndent[$thisFunctionName][$wvBlock]Writing command"
                            Write-GitBranchOut -OutputType Command -OutputValue $line -OutputStream $CommandOut

                            # results
                            Write-Verbose "$(wvTimestamp)$wvIndent[$thisFunctionName][$wvBlock]Running command"
                            If (!$ResultsOut -or $ResultsOut -eq 'Pipe')
                            {
                                Invoke-GitExpression -Command $line
                            }
                            ElseIf ($ResultsOut -eq 'None')
                            {
                                Invoke-GitExpression -Command $line | Out-Null
                            }
                            ElseIf ($ResultsOut -eq 'Native')
                            {
                                Invoke-Expression -Command $line
                            }
                            Else
                            {
                                Invoke-GitExpression -Command $line | Write-Host -ForegroundColor $ResultsOut
                            }
                        }
                    }
                }
            }
        }
        ElseIf ($RepoName)
        {
            Write-Warning "[$thisFunctionName]Repository '$RepoName' not found. Check the repository directory has been added to the `$GitRepoPath module variable."
        }
    }

    END
    {
        $wvBlock = 'E'

        # Function END:
        If (!$SetLocation)
        {
            Write-Verbose "$(wvTimestamp)$wvIndent[$thisFunctionName][$wvBlock]Setting location to original directory"
            Pop-Location -StackName SetGitBranch
        }

        # Common END:
        $end      = Get-Date
        $duration = New-TimeSpan -Start $start -End $end
        Write-Verbose "$(wvTimestamp)$wvIndent[$thisFunctionName][$wvBlock]Finish: $($end.ToString('yyyy-MM-dd HH:mm:ss.fff')) ($('{0}d {1:00}:{2:00}:{3:00}.{4:000}' -f $duration.Days,$duration.Hours,$duration.Minutes,$duration.Seconds,$duration.Milliseconds))"
        $PowdrgitCallDepth--
    }
}