diff --git a/.vscode/launch.json b/.vscode/launch.json index 42a031619..1b00b2b95 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,12 @@ { "version": "0.2.0", "configurations": [ + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Interactive Session", + "cwd": "${workspaceRoot}" + }, { "type": "PowerShell", "request": "launch", diff --git a/src/AnsiUtils.ps1 b/src/AnsiUtils.ps1 index 2c87f036e..5a0672edf 100644 --- a/src/AnsiUtils.ps1 +++ b/src/AnsiUtils.ps1 @@ -24,29 +24,60 @@ $AnsiEscape = [char]27 + "[" $ColorTranslatorType = 'System.Drawing.ColorTranslator' -as [Type] $ColorType = 'System.Drawing.Color' -as [Type] +function EscapseAnsiString([string]$AnsiString) { + if ($PSVersionTable.PSVersion.Major -ge 6) { + $res = $AnsiString -replace "$([char]27)", '`e' + } + else { + $res = $AnsiString -replace "$([char]27)", '$([char]27)' + } + + $res +} + +function Test-VirtualTerminalSequece([psobject]$Object) { + if ($global:GitPromptSettings.AnsiConsole -and ($Object -is [string])) { + return $Object.Contains($AnsiEscape) + } + else { + return $false + } +} + function Get-VirtualTerminalSequence ($color, [int]$offset = 0) { if ($color -is [byte]) { return "${AnsiEscape}$(38 + $offset);5;${color}m" } + + if ($color -is [int]) { + $r = ($color -shr 16) -band 0xff + $g = ($color -shr 8) -band 0xff + $b = $color -band 0xff + return "${AnsiEscape}$(38 + $offset);2;${r};${g};${b}m" + } + if ($color -is [String]) { try { - if ($ColorTranslatorType) { - $color = $ColorTranslatorType::FromHtml($color) + if ($null -ne ($color -as [System.ConsoleColor])) { + $color = [System.ConsoleColor]$color } - else { - $color = [ConsoleColor]$color + elseif ($ColorTranslatorType) { + $color = $ColorTranslatorType::FromHtml($color) } } catch { Write-Debug $_ } } + if ($ColorType -and ($color -is $ColorType)) { return "${AnsiEscape}$(38 + $offset);2;$($color.R);$($color.G);$($color.B)m" } - if (($color -is [ConsoleColor]) -and ($color -ge 0) -and ($color -le 15)) { + + if (($color -is [System.ConsoleColor]) -and ($color -ge 0) -and ($color -le 15)) { return "${AnsiEscape}$($ConsoleColorToAnsi[$color] + $offset)m" } + return "${AnsiEscape}$($AnsiDefaultColor + $offset)m" } diff --git a/src/GitPrompt.ps1 b/src/GitPrompt.ps1 index 762d60387..b522587c6 100644 --- a/src/GitPrompt.ps1 +++ b/src/GitPrompt.ps1 @@ -1,361 +1,894 @@ # Inspired by Mark Embling # http://www.markembling.info/view/my-ideal-powershell-prompt-with-git-integration -$global:GitPromptSettings = [pscustomobject]@{ - DefaultForegroundColor = $null +$global:GitPromptSettings = [GitPromptSettings]::new() - BeforeText = ' [' - BeforeForegroundColor = [ConsoleColor]::Yellow - BeforeBackgroundColor = $null +# Override some of the normal colors if the background color is set to the default DarkMagenta. +$s = $global:GitPromptSettings +if ($Host.UI.RawUI.BackgroundColor -eq [ConsoleColor]::DarkMagenta) { + $s.LocalDefaultStatusSymbol.ForegroundColor = 'Green' + $s.LocalWorkingStatusSymbol.ForegroundColor = 'Red' + $s.BeforeIndexText.ForegroundColor = 'Green' + $s.IndexColor.ForegroundColor = 'Green' + $s.WorkingColor.ForegroundColor = 'Red' +} - DelimText = ' |' - DelimForegroundColor = [ConsoleColor]::Yellow - DelimBackgroundColor = $null +$isAdminProcess = Test-Administrator +$adminHeader = if ($isAdminProcess) { 'Administrator: ' } else { '' } - AfterText = ']' - AfterForegroundColor = [ConsoleColor]::Yellow - AfterBackgroundColor = $null +$WindowTitleSupported = $true +# TODO: Hmm, this is a curious way to detemine window title supported +# Could do $host.Name -eq "Package Manager Host" but that is kinda specific +# Could attempt to change it and catch any exception and then set this to $false +if (Get-Module NuGet) { + $WindowTitleSupported = $false +} - FileAddedText = '+' - FileModifiedText = '~' - FileRemovedText = '-' - FileConflictedText = '!' +<# +.SYNOPSIS + Writes the object to the display or renders it as a string using ANSI/VT sequences. +.DESCRIPTION + Writes the specified object to the display unless $GitPromptSettings.AnsiConsole + is enabled. In this case, the Object is rendered, along with the specified + colors, as a string with the appropriate ANSI/VT sequences for colors embedded + in the string. If a StringBuilder is provided, the string is appended to the + StringBuilder. +.EXAMPLE + PS C:\> Write-Prompt "PS > " -ForegroundColor Cyan -BackgroundColor Black + On a system where $GitPromptSettings.AnsiConsole is set to $false, this + will write the above to the display using the Write-Host command. + If AnsiConsole is set to $true, this will return a string of the form: + "`e[96m`e[40mPS > `e[0m". +.EXAMPLE + PS C:\> $sb = [System.Text.StringBuilder]::new() + PS C:\> $sb | Write-Prompt "PS > " -ForegroundColor Cyan -BackgroundColor Black + On a system where $GitPromptSettings.AnsiConsole is set to $false, this + will write the above to the display using the Write-Host command. + If AnsiConsole is set to $true, this will append the following string to the + StringBuilder object piped into the command: + "`e[96m`e[40mPS > `e[0m". +#> +function Write-Prompt { + [CmdletBinding(DefaultParameterSetName="Default")] + param( + # Specifies objects to display in the console or render as a string if + # $GitPromptSettings.AnsiConsole is enabled. If the Object is of type + # [PoshGitTextSpan] the other color parameters are ignored since a + # [PoshGitTextSpan] provides the colors. + [Parameter(Mandatory, Position=0)] + $Object, - LocalDefaultStatusSymbol = $null - LocalDefaultStatusForegroundColor = [ConsoleColor]::DarkGreen - LocalDefaultStatusForegroundBrightColor = [ConsoleColor]::Green - LocalDefaultStatusBackgroundColor = $null + # Specifies the foreground color. + [Parameter(ParameterSetName="Default")] + $ForegroundColor = $null, - LocalWorkingStatusSymbol = '!' - LocalWorkingStatusForegroundColor = [ConsoleColor]::DarkRed - LocalWorkingStatusForegroundBrightColor = [ConsoleColor]::Red - LocalWorkingStatusBackgroundColor = $null + # Specifies the background color. + [Parameter(ParameterSetName="Default")] + $BackgroundColor = $null, - LocalStagedStatusSymbol = '~' - LocalStagedStatusForegroundColor = [ConsoleColor]::Cyan - LocalStagedStatusBackgroundColor = $null + # Specifies both the background and foreground colors via [PoshGitCellColor] object. + [Parameter(ParameterSetName="CellColor")] + [ValidateNotNull()] + [PoshGitCellColor] + $Color, + + # When specified and $GitPromptSettings.AnsiConsole is enabled, the Object parameter + # is written to the StringBuilder along with the appropriate ANSI/VT sequences for + # the specified foreground and background colors. + [Parameter(ValueFromPipeline = $true)] + [System.Text.StringBuilder] + $StringBuilder + ) + + if ($PSCmdlet.ParameterSetName -eq "CellColor") { + $bgColor = $Color.BackgroundColor + $fgColor = $Color.ForegroundColor + } + else { + $bgColor = $BackgroundColor + $fgColor = $ForegroundColor + } - BranchUntrackedSymbol = $null - BranchForegroundColor = [ConsoleColor]::Cyan - BranchBackgroundColor = $null + $s = $global:GitPromptSettings + if ($s) { + if ($null -eq $fgColor) { + $fgColor = $s.DefaultColor.ForegroundColor + } - BranchGoneStatusSymbol = [char]0x00D7 # × Multiplication sign - BranchGoneStatusForegroundColor = [ConsoleColor]::DarkCyan - BranchGoneStatusBackgroundColor = $null + if ($null -eq $bgColor) { + $bgColor = $s.DefaultColor.BackgroundColor + } - BranchIdenticalStatusToSymbol = [char]0x2261 # ≡ Three horizontal lines - BranchIdenticalStatusToForegroundColor = [ConsoleColor]::Cyan - BranchIdenticalStatusToBackgroundColor = $null + if ($s.AnsiConsole) { + if ($Object -is [PoshGitTextSpan]) { + $str = $Object.RenderAnsi() + } + else { + $e = [char]27 + "[" + $fg = Get-ForegroundVirtualTerminalSequence $fgColor + $bg = Get-BackgroundVirtualTerminalSequence $bgColor + $str = "${fg}${bg}${Object}${e}0m" + } - BranchAheadStatusSymbol = [char]0x2191 # ↑ Up arrow - BranchAheadStatusForegroundColor = [ConsoleColor]::Green - BranchAheadStatusBackgroundColor = $null + if ($StringBuilder) { + return $StringBuilder.Append($str) + } - BranchBehindStatusSymbol = [char]0x2193 # ↓ Down arrow - BranchBehindStatusForegroundColor = [ConsoleColor]::Red - BranchBehindStatusBackgroundColor = $null + return $str + } + } + + if ($Object -is [PoshGitTextSpan]) { + $bgColor = $Object.BackgroundColor + $fgColor = $Object.ForegroundColor + $Object = $Object.Text + } + + $writeHostParams = @{ + Object = $Object; + NoNewLine = $true; + } - BranchBehindAndAheadStatusSymbol = [char]0x2195 # ↕ Up & Down arrow - BranchBehindAndAheadStatusForegroundColor = [ConsoleColor]::Yellow - BranchBehindAndAheadStatusBackgroundColor = $null + if ($bgColor -and ($bgColor -ge 0) -and ($bgColor -le 15)) { + $writeHostParams.BackgroundColor = $bgColor + } - BeforeIndexText = "" - BeforeIndexForegroundColor = [ConsoleColor]::DarkGreen - BeforeIndexForegroundBrightColor = [ConsoleColor]::Green - BeforeIndexBackgroundColor = $null + if ($fgColor -and ($fgColor -ge 0) -and ($fgColor -le 15)) { + $writeHostParams.ForegroundColor = $fgColor + } - IndexForegroundColor = [ConsoleColor]::DarkGreen - IndexForegroundBrightColor = [ConsoleColor]::Green - IndexBackgroundColor = $null + Write-Host @writeHostParams + if ($StringBuilder) { + return $StringBuilder + } + + return "" +} + +<# +.SYNOPSIS + Writes the Git status for repo. Typically, you use Write-VcsStatus + function instead of this one. +.DESCRIPTION + Writes the Git status for repo. This includes the branch name, branch + status with respect to its remote (if exists), index status, working + dir status, working dir local status and stash count (optional). + Various settings from GitPromptSettngs are used to format and color + the Git status. + + On systems that support ANSI terminal sequences, this method will + return a string containing ANSI sequences to color various parts of + the Git status string. This string can be written to the host and + the ANSI sequences will be interpreted and converted to the specified + behaviors which is typically setting the foreground and/or background + color of text. +.EXAMPLE + PS C:\> Write-GitStatus (Get-GitStatus) + + Writes the Git status for the current repo. +.INPUTS + System.Management.Automation.PSCustomObject + This is PSCustomObject returned by Get-GitStatus +.OUTPUTS + System.String + This command returns a System.String object. +#> +function Write-GitStatus { + param( + # The Git status object that provides the status information to be written. + # This object is retrieved via the Get-GitStatus command. + [Parameter(Position = 0)] + $Status + ) + + $s = $global:GitPromptSettings + if (!$Status -or !$s) { + if ($global:PreviousWindowTitle) { + $Host.UI.RawUI.WindowTitle = $global:PreviousWindowTitle + } + + return "" + } - WorkingForegroundColor = [ConsoleColor]::DarkRed - WorkingForegroundBrightColor = [ConsoleColor]::Red - WorkingBackgroundColor = $null + $sb = [System.Text.StringBuilder]::new(150) - EnableStashStatus = $false - BeforeStashText = ' (' - BeforeStashBackgroundColor = $null - BeforeStashForegroundColor = [ConsoleColor]::Red - AfterStashText = ')' - AfterStashBackgroundColor = $null - AfterStashForegroundColor = [ConsoleColor]::Red - StashBackgroundColor = $null - StashForegroundColor = [ConsoleColor]::Red + $sb | Write-Prompt $s.BeforeText > $null + $sb | Write-GitBranchName $Status -NoLeadingSpace > $null + $sb | Write-GitBranchStatus $Status > $null - ErrorForegroundColor = [ConsoleColor]::Red - ErrorBackgroundColor = $null + if ($s.EnableFileStatus -and $Status.HasIndex) { + $sb | Write-Prompt $s.BeforeIndexText > $null - ShowStatusWhenZero = $true + $sb | Write-GitIndexStatus $Status > $null - AutoRefreshIndex = $true + if ($Status.HasWorking) { + $sb | Write-Prompt $s.DelimText > $null + } + } - # Valid values are "Full", "Compact", and "Minimal" - BranchBehindAndAheadDisplay = "Full" + if ($s.EnableFileStatus -and $Status.HasWorking) { + $sb | Write-GitWorkingDirStatus $Status > $null + } - EnablePromptStatus = !$Global:GitMissing - EnableFileStatus = $true - EnableFileStatusFromCache = $null - RepositoriesInWhichToDisableFileStatus = @( ) # Array of repository paths - DescribeStyle = '' + $sb | Write-GitWorkingDirStatusSummary $Status > $null - EnableWindowTitle = 'posh~git ~ ' + if ($s.EnableStashStatus -and ($Status.StashCount -gt 0)) { + $sb | Write-GitStashCount $Status > $null + } - AnsiConsole = $Host.UI.SupportsVirtualTerminal -or ($Env:ConEmuANSI -eq "ON") + $sb | Write-Prompt $s.AfterText > $null - DefaultPromptPrefix = '' - DefaultPromptSuffix = '$(''>'' * ($nestedPromptLevel + 1)) ' - DefaultPromptDebugSuffix = ' [DBG]$(''>'' * ($nestedPromptLevel + 1)) ' - DefaultPromptEnableTiming = $false - DefaultPromptAbbreviateHomeDirectory = $false + if ($WindowTitleSupported -and $s.EnableWindowTitle) { + if (!$global:PreviousWindowTitle) { + $global:PreviousWindowTitle = $Host.UI.RawUI.WindowTitle + } - Debug = $false + $repoName = Split-Path -Leaf (Split-Path $status.GitDir) + $prefix = if ($s.EnableWindowTitle -is [string]) { $s.EnableWindowTitle } else { '' } + $Host.UI.RawUI.WindowTitle = "${script:adminHeader}${prefix}${repoName} [$($Status.Branch)]" + } - BranchNameLimit = 0 - TruncatedBranchSuffix = '...' + return $sb.ToString() } -$isAdminProcess = Test-Administrator +<# +.SYNOPSIS + Formats the branch name text according to $GitPromptSettings. +.DESCRIPTION + Formats the branch name text according the $GitPromptSettings: + BranchNameLimit and TruncatedBranchSuffix. +.EXAMPLE + PS C:\> $branchName = Format-GitBranchName (Get-GitStatus).Branch + + Gets the branch name formatted as specified by the user's $GitPromptSettings. +.INPUTS + System.String + This is the branch name as a string. +.OUTPUTS + System.String + This command returns a System.String object. +#> +function Format-GitBranchName { + param( + # The branch name to format according to the GitPromptSettings: + # BranchNameLimit and TruncatedBranchSuffix. + [Parameter(Position=0)] + [string] + $BranchName + ) -$adminHeader = if ($isAdminProcess) { 'Administrator: ' } else { '' } + $s = $global:GitPromptSettings + if (!$s -or !$BranchName) { + return "$BranchName" + } -$WindowTitleSupported = $true -if (Get-Module NuGet) { - $WindowTitleSupported = $false + $res = $BranchName + if (($s.BranchNameLimit -gt 0) -and ($BranchName.Length -gt $s.BranchNameLimit)) + { + $res = "{0}{1}" -f $BranchName.Substring(0, $s.BranchNameLimit), $s.TruncatedBranchSuffix + } + + $res } -function Write-Prompt { +<# +.SYNOPSIS + Gets the colors to use for the branch status. +.DESCRIPTION + Gets the colors to use for the branch status. This color is typically + used for the branch name as well. The default color is specified by + $GitPromptSettins.BranchColor. But depending on the Git status object + passed in, the colors could be changed to match that of one these + other $GitPromptSettings: BranchBehindAndAheadStatusSymbol, + BranchBehindStatusSymbol or BranchAheadStatusSymbol. +.EXAMPLE + PS C:\> $branchStatusColor = Get-GitBranchStatusColor (Get-GitStatus) + + Returns a PoshGitTextSpan with the foreground and background colors + for the branch status. +.INPUTS + System.Management.Automation.PSCustomObject + This is PSCustomObject returned by Get-GitStatus +.OUTPUTS + PoshGitTextSpan + A PoshGitTextSpan with colors reflecting those to be used by + branch status symbols. +#> +function Get-GitBranchStatusColor { param( - [parameter(Mandatory = $true)] - $Object, - [parameter()] - $ForegroundColor = $null, - [parameter()] - $BackgroundColor = $null, - [parameter(ValueFromPipeline = $true)] - [Text.StringBuilder] - $Builder) + # The Git status object that provides branch status information. + # This object is retrieved via the Get-GitStatus command. + [Parameter(Position = 0)] + $Status + ) + $s = $global:GitPromptSettings - if ($s -and ($null -eq $ForegroundColor)) { - $ForegroundColor = $s.DefaultForegroundColor + if (!$s) { + return [PoshGitTextSpan]::new() } - if ($GitPromptSettings.AnsiConsole) { - $e = [char]27 + "[" - $f = Get-ForegroundVirtualTerminalSequence $ForegroundColor - $b = Get-BackgroundVirtualTerminalSequence $BackgroundColor - if ($Builder) { - return $Builder.Append($f).Append($b).Append($Object).Append($e).Append("0m") - } - return "${f}${b}${Object}${e}0m" + $branchStatusTextSpan = [PoshGitTextSpan]::new($s.BranchColor) + + if (($Status.BehindBy -ge 1) -and ($Status.AheadBy -ge 1)) { + # We are both behind and ahead of remote + $branchStatusTextSpan = [PoshGitTextSpan]::new($s.BranchBehindAndAheadStatusSymbol) } - $writeHostParams = @{ - Object = $Object; - NoNewLine = $true; + elseif ($Status.BehindBy -ge 1) { + # We are behind remote + $branchStatusTextSpan = [PoshGitTextSpan]::new($s.BranchBehindStatusSymbol) } - if (($BackgroundColor -ge 0) -and ($BackgroundColor -le 15)) { - $writeHostParams.BackgroundColor = $BackgroundColor + elseif ($Status.AheadBy -ge 1) { + # We are ahead of remote + $branchStatusTextSpan = [PoshGitTextSpan]::new($s.BranchAheadStatusSymbol) } - if (($ForegroundColor -ge 0) -and ($ForegroundColor -le 15)) { - $writeHostParams.ForegroundColor = $ForegroundColor + + $branchStatusTextSpan.Text = '' + $branchStatusTextSpan +} + +<# +.SYNOPSIS + Writes the branch name given the current Git status. +.DESCRIPTION + Writes the branch name given the current Git status which can retrieved + via the Get-GitStatus command. Branch name can be affected by the + $GitPromptSettings: BranchColor, BranchNameLimit, TruncatedBranchSuffix + and Branch*StatusSymbol colors. +.EXAMPLE + PS C:\> Write-GitBranchName (Get-GitStatus) + + Writes the name of the current branch. +.INPUTS + System.Management.Automation.PSCustomObject + This is PSCustomObject returned by Get-GitStatus +.OUTPUTS + System.String, System.Text.StringBuilder + This command returns a System.String object unless the -StringBuilder parameter + is supplied. In this case, it returns a System.Text.StringBuilder. +#> +function Write-GitBranchName { + param( + # The Git status object that provides the status information to be written. + # This object is retrieved via the Get-GitStatus command. + [Parameter(Position = 0)] + $Status, + + # If specified the branch name is written into the provided StringBuilder object. + [Parameter(ValueFromPipeline = $true)] + [System.Text.StringBuilder] + $StringBuilder, + + # If specified, suppresses the output of the leading space character. + [Parameter()] + [switch] + $NoLeadingSpace + ) + + $s = $global:GitPromptSettings + if (!$Status -or !$s) { + return $(if ($StringBuilder) { $StringBuilder } else { "" }) } - Write-Host @writeHostParams - if ($Builder) { - return $Builder + + $str = "" + + # Use the branch status colors (or CustomAnsi) to display the branch name + $branchNameTextSpan = Get-GitBranchStatusColor $Status + $branchNameTextSpan.Text = Format-GitBranchName $Status.Branch + if (!$NoLeadingSpace) { + $branchNameTextSpan.Text = " " + $branchNameTextSpan.Text } - return "" + + if ($StringBuilder) { + $StringBuilder | Write-Prompt $branchNameTextSpan > $null + } + else { + $str = Write-Prompt $branchNameTextSpan + } + + return $(if ($StringBuilder) { $StringBuilder } else { $str }) } -function Format-BranchName($branchName){ +<# +.SYNOPSIS + Writes the branch status text given the current Git status. +.DESCRIPTION + Writes the branch status text given the current Git status which can retrieved + via the Get-GitStatus command. Branch status includes information about the + upstream branch, how far behind and/or ahead the local branch is from the remote. +.EXAMPLE + PS C:\> Write-GitBranchStatus (Get-GitStatus) + + Writes the status of the current branch to the host. +.INPUTS + System.Management.Automation.PSCustomObject + This is PSCustomObject returned by Get-GitStatus +.OUTPUTS + System.String, System.Text.StringBuilder + This command returns a System.String object unless the -StringBuilder parameter + is supplied. In this case, it returns a System.Text.StringBuilder. +#> +function Write-GitBranchStatus { + param( + # The Git status object that provides the status information to be written. + # This object is retrieved via the Get-GitStatus command. + [Parameter(Position = 0)] + $Status, + + # If specified the branch status is written into the provided StringBuilder object. + [Parameter(ValueFromPipeline = $true)] + [System.Text.StringBuilder] + $StringBuilder, + + # If specified, suppresses the output of the leading space character. + [Parameter()] + [switch] + $NoLeadingSpace + ) + $s = $global:GitPromptSettings + if (!$Status -or !$s) { + return $(if ($StringBuilder) { $StringBuilder } else { "" }) + } - if($s.BranchNameLimit -gt 0 -and $branchName.Length -gt $s.BranchNameLimit) - { - $branchName = "{0}{1}" -f $branchName.Substring(0,$s.BranchNameLimit), $s.TruncatedBranchSuffix + $branchStatusTextSpan = Get-GitBranchStatusColor $Status + + if (!$Status.Upstream) { + $branchStatusTextSpan.Text = $s.BranchUntrackedText + } + elseif ($Status.UpstreamGone -eq $true) { + # Upstream branch is gone + $branchStatusTextSpan.Text = $s.BranchGoneStatusSymbol.Text + } + elseif (($Status.BehindBy -eq 0) -and ($Status.AheadBy -eq 0)) { + # We are aligned with remote + $branchStatusTextSpan.Text = $s.BranchIdenticalStatusSymbol.Text + } + elseif (($Status.BehindBy -ge 1) -and ($Status.AheadBy -ge 1)) { + # We are both behind and ahead of remote + if ($s.BranchBehindAndAheadDisplay -eq "Full") { + $branchStatusTextSpan.Text = ("{0}{1} {2}{3}" -f $s.BranchBehindStatusSymbol.Text, $Status.BehindBy, $s.BranchAheadStatusSymbol.Text, $status.AheadBy) + } + elseif ($s.BranchBehindAndAheadDisplay -eq "Compact") { + $branchStatusTextSpan.Text = ("{0}{1}{2}" -f $Status.BehindBy, $s.BranchBehindAndAheadStatusSymbol.Text, $Status.AheadBy) + } + } + elseif ($Status.BehindBy -ge 1) { + # We are behind remote + if (($s.BranchBehindAndAheadDisplay -eq "Full") -Or ($s.BranchBehindAndAheadDisplay -eq "Compact")) { + $branchStatusTextSpan.Text = ("{0}{1}" -f $s.BranchBehindStatusSymbol.Text, $Status.BehindBy) + } + } + elseif ($Status.AheadBy -ge 1) { + # We are ahead of remote + if (($s.BranchBehindAndAheadDisplay -eq "Full") -or ($s.BranchBehindAndAheadDisplay -eq "Compact")) { + $branchStatusTextSpan.Text = ("{0}{1}" -f $s.BranchAheadStatusSymbol.Text, $Status.AheadBy) + } + } + else { + # This condition should not be possible but defaulting the variables to be safe + $branchStatusTextSpan.Text = "?" + } + + $str = "" + if ($branchStatusTextSpan.Text) { + $textSpan = [PoshGitTextSpan]::new($branchStatusTextSpan) + if (!$NoLeadingSpace) { + $textSpan.Text = " " + $branchStatusTextSpan.Text + } + + if ($StringBuilder) { + $StringBuilder | Write-Prompt $textSpan > $null + } + else { + $str = Write-Prompt $textSpan + } } - return $branchName + return $(if ($StringBuilder) { $StringBuilder } else { $str }) } -function Write-GitStatus($status) { +<# +.SYNOPSIS + Writes the index status text given the current Git status. +.DESCRIPTION + Writes the index status text given the current Git status. +.EXAMPLE + PS C:\> Write-GitIndexStatus (Get-GitStatus) + + Writes the Git index status to the host. +.INPUTS + System.Management.Automation.PSCustomObject + This is PSCustomObject returned by Get-GitStatus +.OUTPUTS + System.String, System.Text.StringBuilder + This command returns a System.String object unless the -StringBuilder parameter + is supplied. In this case, it returns a System.Text.StringBuilder. +#> +function Write-GitIndexStatus { + param( + # The Git status object that provides the status information to be written. + # This object is retrieved via the Get-GitStatus command. + [Parameter(Position = 0)] + $Status, + + # If specified the index status is written into the provided StringBuilder object. + [Parameter(ValueFromPipeline = $true)] + [System.Text.StringBuilder] + $StringBuilder, + + # If specified, suppresses the output of the leading space character. + [Parameter()] + [switch] + $NoLeadingSpace + ) + $s = $global:GitPromptSettings - $sb = New-Object System.Text.StringBuilder - if ($status -and $s) { - $sb | Write-Prompt $s.BeforeText -BackgroundColor $s.BeforeBackgroundColor -ForegroundColor $s.BeforeForegroundColor | Out-Null - - $branchStatusText = $null - $branchStatusBackgroundColor = $s.BranchBackgroundColor - $branchStatusForegroundColor = $s.BranchForegroundColor - - if (!$status.Upstream) { - $branchStatusText = $s.BranchUntrackedSymbol - } elseif ($status.UpstreamGone -eq $true) { - # Upstream branch is gone - $branchStatusText = $s.BranchGoneStatusSymbol - $branchStatusBackgroundColor = $s.BranchGoneStatusBackgroundColor - $branchStatusForegroundColor = $s.BranchGoneStatusForegroundColor - } elseif ($status.BehindBy -eq 0 -and $status.AheadBy -eq 0) { - # We are aligned with remote - $branchStatusText = $s.BranchIdenticalStatusToSymbol - $branchStatusBackgroundColor = $s.BranchIdenticalStatusToBackgroundColor - $branchStatusForegroundColor = $s.BranchIdenticalStatusToForegroundColor - } elseif ($status.BehindBy -ge 1 -and $status.AheadBy -ge 1) { - # We are both behind and ahead of remote - if ($s.BranchBehindAndAheadDisplay -eq "Full") { - $branchStatusText = ("{0}{1} {2}{3}" -f $s.BranchBehindStatusSymbol, $status.BehindBy, $s.BranchAheadStatusSymbol, $status.AheadBy) - } elseif ($s.BranchBehindAndAheadDisplay -eq "Compact") { - $branchStatusText = ("{0}{1}{2}" -f $status.BehindBy, $s.BranchBehindAndAheadStatusSymbol, $status.AheadBy) - } else { - $branchStatusText = $s.BranchBehindAndAheadStatusSymbol + if (!$Status -or !$s) { + return $(if ($StringBuilder) { $StringBuilder } else { "" }) + } + + $str = "" + + if ($Status.HasIndex) { + if ($s.ShowStatusWhenZero -or $Status.Index.Added) { + $indexStatusText = " " + if ($NoLeadingSpace) { + $indexStatusText = "" + $NoLeadingSpace = $false } - $branchStatusBackgroundColor = $s.BranchBehindAndAheadStatusBackgroundColor - $branchStatusForegroundColor = $s.BranchBehindAndAheadStatusForegroundColor - } elseif ($status.BehindBy -ge 1) { - # We are behind remote - if ($s.BranchBehindAndAheadDisplay -eq "Full" -Or $s.BranchBehindAndAheadDisplay -eq "Compact") { - $branchStatusText = ("{0}{1}" -f $s.BranchBehindStatusSymbol, $status.BehindBy) - } else { - $branchStatusText = $s.BranchBehindStatusSymbol + + $indexStatusText += "$($s.FileAddedText)$($Status.Index.Added.Count)" + + if ($StringBuilder) { + $StringBuilder | Write-Prompt $indexStatusText -Color $s.IndexColor > $null } - $branchStatusBackgroundColor = $s.BranchBehindStatusBackgroundColor - $branchStatusForegroundColor = $s.BranchBehindStatusForegroundColor - } elseif ($status.AheadBy -ge 1) { - # We are ahead of remote - if ($s.BranchBehindAndAheadDisplay -eq "Full" -Or $s.BranchBehindAndAheadDisplay -eq "Compact") { - $branchStatusText = ("{0}{1}" -f $s.BranchAheadStatusSymbol, $status.AheadBy) - } else { - $branchStatusText = $s.BranchAheadStatusSymbol + else { + $str += Write-Prompt $indexStatusText -Color $s.IndexColor } - $branchStatusBackgroundColor = $s.BranchAheadStatusBackgroundColor - $branchStatusForegroundColor = $s.BranchAheadStatusForegroundColor - } else { - # This condition should not be possible but defaulting the variables to be safe - $branchStatusText = "?" } - $sb | Write-Prompt (Format-BranchName($status.Branch)) -BackgroundColor $branchStatusBackgroundColor -ForegroundColor $branchStatusForegroundColor | Out-Null + if ($s.ShowStatusWhenZero -or $status.Index.Modified) { + $indexStatusText = " " + if ($NoLeadingSpace) { + $indexStatusText = "" + $NoLeadingSpace = $false + } - if ($branchStatusText) { - $sb | Write-Prompt (" {0}" -f $branchStatusText) -BackgroundColor $branchStatusBackgroundColor -ForegroundColor $branchStatusForegroundColor | Out-Null - } + $indexStatusText += "$($s.FileModifiedText)$($status.Index.Modified.Count)" - if($s.EnableFileStatus -and $status.HasIndex) { - $sb | Write-Prompt $s.BeforeIndexText -BackgroundColor $s.BeforeIndexBackgroundColor -ForegroundColor $s.BeforeIndexForegroundColor | Out-Null + if ($StringBuilder) { + $StringBuilder | Write-Prompt $indexStatusText -Color $s.IndexColor > $null + } + else { + $str += Write-Prompt $indexStatusText -Color $s.IndexColor + } + } - if($s.ShowStatusWhenZero -or $status.Index.Added) { - $sb | Write-Prompt (" $($s.FileAddedText)$($status.Index.Added.Count)") -BackgroundColor $s.IndexBackgroundColor -ForegroundColor $s.IndexForegroundColor | Out-Null + if ($s.ShowStatusWhenZero -or $Status.Index.Deleted) { + $indexStatusText = " " + if ($NoLeadingSpace) { + $indexStatusText = "" + $NoLeadingSpace = $false } - if($s.ShowStatusWhenZero -or $status.Index.Modified) { - $sb | Write-Prompt (" $($s.FileModifiedText)$($status.Index.Modified.Count)") -BackgroundColor $s.IndexBackgroundColor -ForegroundColor $s.IndexForegroundColor | Out-Null + + $indexStatusText += "$($s.FileRemovedText)$($Status.Index.Deleted.Count)" + + if ($StringBuilder) { + $StringBuilder | Write-Prompt $indexStatusText -Color $s.IndexColor > $null } - if($s.ShowStatusWhenZero -or $status.Index.Deleted) { - $sb | Write-Prompt (" $($s.FileRemovedText)$($status.Index.Deleted.Count)") -BackgroundColor $s.IndexBackgroundColor -ForegroundColor $s.IndexForegroundColor | Out-Null + else { + $str += Write-Prompt $indexStatusText -Color $s.IndexColor } + } - if ($status.Index.Unmerged) { - $sb | Write-Prompt (" $($s.FileConflictedText)$($status.Index.Unmerged.Count)") -BackgroundColor $s.IndexBackgroundColor -ForegroundColor $s.IndexForegroundColor | Out-Null + if ($Status.Index.Unmerged) { + $indexStatusText = " " + if ($NoLeadingSpace) { + $indexStatusText = "" + $NoLeadingSpace = $false } - if($status.HasWorking) { - $sb | Write-Prompt $s.DelimText -BackgroundColor $s.DelimBackgroundColor -ForegroundColor $s.DelimForegroundColor | Out-Null + $indexStatusText += "$($s.FileConflictedText)$($Status.Index.Unmerged.Count)" + + if ($StringBuilder) { + $StringBuilder | Write-Prompt $indexStatusText -Color $s.IndexColor > $null + } + else { + $str += Write-Prompt $indexStatusText -Color $s.IndexColor } } + } - if($s.EnableFileStatus -and $status.HasWorking) { - if($s.ShowStatusWhenZero -or $status.Working.Added) { - $sb | Write-Prompt (" $($s.FileAddedText)$($status.Working.Added.Count)") -BackgroundColor $s.WorkingBackgroundColor -ForegroundColor $s.WorkingForegroundColor | Out-Null + return $(if ($StringBuilder) { $StringBuilder } else { $str }) +} + +<# +.SYNOPSIS + Writes the working directory status text given the current Git status. +.DESCRIPTION + Writes the working directory status text given the current Git status. +.EXAMPLE + PS C:\> Write-GitWorkingDirStatus (Get-GitStatus) + + Writes the Git working directory status to the host. +.INPUTS + System.Management.Automation.PSCustomObject + This is PSCustomObject returned by Get-GitStatus +.OUTPUTS + System.String, System.Text.StringBuilder + This command returns a System.String object unless the -StringBuilder parameter + is supplied. In this case, it returns a System.Text.StringBuilder. +#> +function Write-GitWorkingDirStatus { + param( + # The Git status object that provides the status information to be written. + # This object is retrieved via the Get-GitStatus command. + [Parameter(Position = 0)] + $Status, + + # If specified the working dir status is written into the provided StringBuilder object. + [Parameter(ValueFromPipeline = $true)] + [System.Text.StringBuilder] + $StringBuilder, + + # If specified, suppresses the output of the leading space character. + [Parameter()] + [switch] + $NoLeadingSpace + ) + + $s = $global:GitPromptSettings + if (!$Status -or !$s) { + return $(if ($StringBuilder) { $StringBuilder } else { "" }) + } + + $str = "" + + if ($Status.HasWorking) { + if ($s.ShowStatusWhenZero -or $Status.Working.Added) { + $workingStatusText = " " + if ($NoLeadingSpace) { + $workingStatusText = "" + $NoLeadingSpace = $false } - if($s.ShowStatusWhenZero -or $status.Working.Modified) { - $sb | Write-Prompt (" $($s.FileModifiedText)$($status.Working.Modified.Count)") -BackgroundColor $s.WorkingBackgroundColor -ForegroundColor $s.WorkingForegroundColor | Out-Null + + $workingStatusText += "$($s.FileAddedText)$($Status.Working.Added.Count)" + + if ($StringBuilder) { + $StringBuilder | Write-Prompt $workingStatusText -Color $s.WorkingColor > $null } - if($s.ShowStatusWhenZero -or $status.Working.Deleted) { - $sb | Write-Prompt (" $($s.FileRemovedText)$($status.Working.Deleted.Count)") -BackgroundColor $s.WorkingBackgroundColor -ForegroundColor $s.WorkingForegroundColor | Out-Null + else { + $str += Write-Prompt $workingStatusText -Color $s.WorkingColor } + } - if ($status.Working.Unmerged) { - $sb | Write-Prompt (" $($s.FileConflictedText)$($status.Working.Unmerged.Count)") -BackgroundColor $s.WorkingBackgroundColor -ForegroundColor $s.WorkingForegroundColor | Out-Null + if ($s.ShowStatusWhenZero -or $Status.Working.Modified) { + $workingStatusText = " " + if ($NoLeadingSpace) { + $workingStatusText = "" + $NoLeadingSpace = $false } - } - if ($status.HasWorking) { - # We have un-staged files in the working tree - $localStatusSymbol = $s.LocalWorkingStatusSymbol - $localStatusBackgroundColor = $s.LocalWorkingStatusBackgroundColor - $localStatusForegroundColor = $s.LocalWorkingStatusForegroundColor - } elseif ($status.HasIndex) { - # We have staged but uncommited files - $localStatusSymbol = $s.LocalStagedStatusSymbol - $localStatusBackgroundColor = $s.LocalStagedStatusBackgroundColor - $localStatusForegroundColor = $s.LocalStagedStatusForegroundColor - } else { - # No uncommited changes - $localStatusSymbol = $s.LocalDefaultStatusSymbol - $localStatusBackgroundColor = $s.LocalDefaultStatusBackgroundColor - $localStatusForegroundColor = $s.LocalDefaultStatusForegroundColor - } + $workingStatusText += "$($s.FileModifiedText)$($Status.Working.Modified.Count)" - if ($localStatusSymbol) { - $sb | Write-Prompt (" {0}" -f $localStatusSymbol) -BackgroundColor $localStatusBackgroundColor -ForegroundColor $localStatusForegroundColor | Out-Null + if ($StringBuilder) { + $StringBuilder | Write-Prompt $workingStatusText -Color $s.WorkingColor > $null + } + else { + $str += Write-Prompt $workingStatusText -Color $s.WorkingColor + } } - if ($s.EnableStashStatus -and ($status.StashCount -gt 0)) { - $sb | Write-Prompt $s.BeforeStashText -BackgroundColor $s.BeforeStashBackgroundColor -ForegroundColor $s.BeforeStashForegroundColor | Out-Null - $sb | Write-Prompt $status.StashCount -BackgroundColor $s.StashBackgroundColor -ForegroundColor $s.StashForegroundColor | Out-Null - $sb | Write-Prompt $s.AfterStashText -BackgroundColor $s.AfterStashBackgroundColor -ForegroundColor $s.AfterStashForegroundColor | Out-Null - } + if ($s.ShowStatusWhenZero -or $Status.Working.Deleted) { + $workingStatusText = " " + if ($NoLeadingSpace) { + $workingStatusText = "" + $NoLeadingSpace = $false + } - $sb | Write-Prompt $s.AfterText -BackgroundColor $s.AfterBackgroundColor -ForegroundColor $s.AfterForegroundColor | Out-Null + $workingStatusText += "$($s.FileRemovedText)$($Status.Working.Deleted.Count)" - if ($WindowTitleSupported -and $s.EnableWindowTitle) { - if( -not $Global:PreviousWindowTitle ) { - $Global:PreviousWindowTitle = $Host.UI.RawUI.WindowTitle + if ($StringBuilder) { + $StringBuilder | Write-Prompt $workingStatusText -Color $s.WorkingColor > $null + } + else { + $str += Write-Prompt $workingStatusText -Color $s.WorkingColor } - $repoName = Split-Path -Leaf (Split-Path $status.GitDir) - $prefix = if ($s.EnableWindowTitle -is [string]) { $s.EnableWindowTitle } else { '' } - $Host.UI.RawUI.WindowTitle = "$script:adminHeader$prefix$repoName [$($status.Branch)]" } - return $sb.ToString() - } elseif ( $Global:PreviousWindowTitle ) { - $Host.UI.RawUI.WindowTitle = $Global:PreviousWindowTitle - return "" + if ($Status.Working.Unmerged) { + $workingStatusText = " " + if ($NoLeadingSpace) { + $workingStatusText = "" + $NoLeadingSpace = $false + } + + $workingStatusText += "$($s.FileConflictedText)$($Status.Working.Unmerged.Count)" + + if ($StringBuilder) { + $StringBuilder | Write-Prompt $workingStatusText -Color $s.WorkingColor > $null + } + else { + $str += Write-Prompt $workingStatusText -Color $s.WorkingColor + } + } } + + return $(if ($StringBuilder) { $StringBuilder } else { $str }) } -if(!(Test-Path Variable:Global:VcsPromptStatuses)) { - $Global:VcsPromptStatuses = @() +<# +.SYNOPSIS + Writes the working directory status summary text given the current Git status. +.DESCRIPTION + Writes the working directory status summary text given the current Git status. + If there are any unstaged commits, the $GitPromptSettings.LocalWorkingStatusSymbol + will be output. If not, then if are any staged but uncommmited changes, the + $GitPromptSettings.LocalStagedStatusSymbol will be output. If not, then + $GitPromptSettings.LocalDefaultStatusSymbol will be output. +.EXAMPLE + PS C:\> Write-GitWorkingDirStatusSummary (Get-GitStatus) + + Outputs the Git working directory status summary text. +.INPUTS + System.Management.Automation.PSCustomObject + This is PSCustomObject returned by Get-GitStatus +.OUTPUTS + System.String, System.Text.StringBuilder + This command returns a System.String object unless the -StringBuilder parameter + is supplied. In this case, it returns a System.Text.StringBuilder. +#> +function Write-GitWorkingDirStatusSummary { + param( + # The Git status object that provides the status information to be written. + # This object is retrieved via the Get-GitStatus command. + [Parameter(Position = 0)] + $Status, + + # If specified the working dir local status is written into the provided StringBuilder object. + [Parameter(ValueFromPipeline = $true)] + [System.Text.StringBuilder] + $StringBuilder, + + # If specified, suppresses the output of the leading space character. + [Parameter()] + [switch] + $NoLeadingSpace + ) + + $s = $global:GitPromptSettings + if (!$Status -or !$s) { + return $(if ($StringBuilder) { $StringBuilder } else { "" }) + } + + $str = "" + + # No uncommited changes + $localStatusSymbol = $s.LocalDefaultStatusSymbol + + if ($Status.HasWorking) { + # We have un-staged files in the working tree + $localStatusSymbol = $s.LocalWorkingStatusSymbol + } + elseif ($Status.HasIndex) { + # We have staged but uncommited files + $localStatusSymbol = $s.LocalStagedStatusSymbol + } + + if ($localStatusSymbol.Text) { + $textSpan = [PoshGitTextSpan]::new($localStatusSymbol) + if (!$NoLeadingSpace) { + $textSpan.Text = " " + $localStatusSymbol.Text + } + + if ($StringBuilder) { + $StringBuilder | Write-Prompt $textSpan > $null + } + else { + $str += Write-Prompt $textSpan + } + } + + return $(if ($StringBuilder) { $StringBuilder } else { $str }) } -$s = $global:GitPromptSettings -# Override some of the normal colors if the background color is set to the default DarkMagenta. -if ($Host.UI.RawUI.BackgroundColor -eq [ConsoleColor]::DarkMagenta) { - $s.LocalDefaultStatusForegroundColor = $s.LocalDefaultStatusForegroundBrightColor - $s.LocalWorkingStatusForegroundColor = $s.LocalWorkingStatusForegroundBrightColor +<# +.SYNOPSIS + Writes the stash count given the current Git status. +.DESCRIPTION + Writes the stash count given the current Git status. +.EXAMPLE + PS C:\> Write-GitStashCount (Get-GitStatus) + + Writes the Git stash count to the host. +.INPUTS + System.Management.Automation.PSCustomObject + This is PSCustomObject returned by Get-GitStatus +.OUTPUTS + System.String, System.Text.StringBuilder + This command returns a System.String object unless the -StringBuilder parameter + is supplied. In this case, it returns a System.Text.StringBuilder. +#> +function Write-GitStashCount { + param( + # The Git status object that provides the status information to be written. + # This object is retrieved via the Get-GitStatus command. + [Parameter(Position = 0)] + $Status, - $s.BeforeIndexForegroundColor = $s.BeforeIndexForegroundBrightColor - $s.IndexForegroundColor = $s.IndexForegroundBrightColor + # If specified the working dir local status is written into the provided StringBuilder object. + [Parameter(ValueFromPipeline = $true)] + [System.Text.StringBuilder] + $StringBuilder + ) + + $s = $global:GitPromptSettings + if (!$Status -or !$s) { + return $(if ($StringBuilder) { $StringBuilder } else { "" }) + } - $s.WorkingForegroundColor = $s.WorkingForegroundBrightColor + $str = "" + + if ($Status.StashCount -gt 0) { + $stashText = "$($Status.StashCount)" + + if ($StringBuilder) { + $StringBuilder | Write-Prompt $s.BeforeStashText > $null + $StringBuilder | Write-Prompt $stashText -Color $s.StashColor > $null + $StringBuilder | Write-Prompt $s.AfterStashText > $null + } + else { + $str += Write-Prompt $s.BeforeStashText + $str += Write-Prompt $stashText -Color $s.StashColor + $str += Write-Prompt $s.AfterStashText + } + } + + return $(if ($StringBuilder) { $StringBuilder } else { $str }) } +if (!(Test-Path Variable:Global:VcsPromptStatuses)) { + $global:VcsPromptStatuses = @() +} + +<# +.SYNOPSIS + Writes all version control prompt statuses configured in $global:VscPromptStatuses. +.DESCRIPTION + Writes all version control prompt statuses configured in $global:VscPromptStatuses. + By default, this includes the PoshGit prompt status. +.EXAMPLE + PS C:\> Write-VcsStatus + + Writes all version control prompt statuses that have been configured + with the global variable $VscPromptStatuses +#> function Global:Write-VcsStatus { Set-ConsoleMode -ANSI - $Global:VcsPromptStatuses | ForEach-Object { & $_ } + + $OFS = "" + "$($global:VcsPromptStatuses | ForEach-Object { & $_ })" } # Add scriptblock that will execute for Write-VcsStatus $PoshGitVcsPrompt = { try { - $Global:GitStatus = Get-GitStatus + $global:GitStatus = Get-GitStatus Write-GitStatus $GitStatus } catch { - $s = $Global:GitPromptSettings + $s = $global:GitPromptSettings if ($s) { - Write-Prompt $s.BeforeText -BackgroundColor $s.BeforeBackgroundColor -ForegroundColor $s.BeforeForegroundColor - Write-Prompt "Error: $_" -BackgroundColor $s.ErrorBackgroundColor -ForegroundColor $s.ErrorForegroundColor - Write-Prompt $s.AfterText -BackgroundColor $s.AfterBackgroundColor -ForegroundColor $s.AfterForegroundColor + $errorText = "PoshGitVcsPrompt error: $_" + $sb = [System.Text.StringBuilder]::new() + + $sb | Write-Prompt $s.BeforeText > $null + $sb | Write-Prompt $errorText -Color $s.ErrorColor > $null + $sb | Write-Prompt $s.AfterText > $null + + $sb.ToString() } } } -$Global:VcsPromptStatuses += $PoshGitVcsPrompt +$global:VcsPromptStatuses += $PoshGitVcsPrompt diff --git a/src/GitUtils.ps1 b/src/GitUtils.ps1 index b606d0c72..1e6bef8f6 100644 --- a/src/GitUtils.ps1 +++ b/src/GitUtils.ps1 @@ -591,5 +591,6 @@ function Update-AllBranches($Upstream = 'master', [switch]$Quiet) { Write-Warning "Rebase failed for $branch" } } + git checkout -q $head } diff --git a/src/PoshGitTypes.ps1 b/src/PoshGitTypes.ps1 new file mode 100644 index 000000000..98b1aa488 --- /dev/null +++ b/src/PoshGitTypes.ps1 @@ -0,0 +1,214 @@ +enum BranchBehindAndAheadDisplayOptions { Full; Compact; Minimal } + +class PoshGitCellColor { + [psobject]$BackgroundColor + [psobject]$ForegroundColor + + PoshGitCellColor() { + $this.ForegroundColor = $null + $this.BackgroundColor = $null + } + + PoshGitCellColor([psobject]$ForegroundColor) { + $this.ForegroundColor = $ForegroundColor + $this.BackgroundColor = $null + } + + PoshGitCellColor([psobject]$ForegroundColor, [psobject]$BackgroundColor) { + $this.ForegroundColor = $ForegroundColor + $this.BackgroundColor = $BackgroundColor + } + + hidden [string] ToString($color) { + $ansiTerm = "$([char]0x1b)[0m" + $colorSwatch = " " + + if (!$color) { + $str = "" + } + elseif (Test-VirtualTerminalSequece $color) { + $txt = EscapseAnsiString $color + $str = "${color}${colorSwatch}${ansiTerm} $txt" + } + else { + $str = "" + + if ($global:GitPromptSettings.AnsiConsole) { + $bg = Get-BackgroundVirtualTerminalSequence $color + $str += "${bg}${colorSwatch}${ansiTerm} " + } + + $str += $color.ToString() + } + + return $str + } + + [string] ToString() { + $str = "ForegroundColor: " + $str += $this.ToString($this.ForegroundColor) + ", " + $str += "BackgroundColor: " + $str += $this.ToString($this.BackgroundColor) + return $str + } +} + +class PoshGitTextSpan { + [string]$Text + [psobject]$BackgroundColor + [psobject]$ForegroundColor + [string]$CustomAnsi + + PoshGitTextSpan() { + $this.Text = "" + $this.ForegroundColor = $null + $this.BackgroundColor = $null + $this.CustomAnsi = $null + } + + PoshGitTextSpan([string]$Text) { + $this.Text = $Text + $this.ForegroundColor = $null + $this.BackgroundColor = $null + $this.CustomAnsi = $null + } + + PoshGitTextSpan([string]$Text, [psobject]$ForegroundColor) { + $this.Text = $Text + $this.ForegroundColor = $ForegroundColor + $this.BackgroundColor = $null + $this.CustomAnsi = $null + } + + PoshGitTextSpan([string]$Text, [psobject]$ForegroundColor, [psobject]$BackgroundColor) { + $this.Text = $Text + $this.ForegroundColor = $ForegroundColor + $this.BackgroundColor = $BackgroundColor + $this.CustomAnsi = $null + } + + PoshGitTextSpan([PoshGitTextSpan]$PoshGitTextSpan) { + $this.Text = $PoshGitTextSpan.Text + $this.ForegroundColor = $PoshGitTextSpan.ForegroundColor + $this.BackgroundColor = $PoshGitTextSpan.BackgroundColor + $this.CustomAnsi = $PoshGitTextSpan.CustomAnsi + } + + PoshGitTextSpan([PoshGitCellColor]$PoshGitCellColor) { + $this.Text = '' + $this.ForegroundColor = $PoshGitCellColor.ForegroundColor + $this.BackgroundColor = $PoshGitCellColor.BackgroundColor + $this.CustomAnsi = $null + } + + [string] ToString() { + if ($global:GitPromptSettings.AnsiConsole) { + if ($this.CustomAnsi) { + $e = [char]27 + "[" + $ansi = $this.CustomAnsi + $escAnsi = EscapseAnsiString $this.CustomAnsi + $txt = $this.RenderAnsi() + $str = "Text: '$txt',`t CustomAnsi: '${ansi}${escAnsi}${e}0m'" + } + else { + $color = [PoshGitCellColor]::new($this.ForegroundColor, $this.BackgroundColor) + $txt = $this.RenderAnsi() + $str = "Text: '$txt',`t $($color.ToString())" + } + } + else { + $color = [PoshGitCellColor]::new($this.ForegroundColor, $this.BackgroundColor) + $txt = $this.Text + $str = "Text: '$txt',`t $($color.ToString())" + } + + return $str + } + + [string] RenderAnsi() { + $e = [char]27 + "[" + $txt = $this.Text + + if ($this.CustomAnsi) { + $ansi = $this.CustomAnsi + $str = "${ansi}${txt}${e}0m" + } + else { + $bg = $this.BackgroundColor + if ($bg -and !(Test-VirtualTerminalSequece $bg)) { + $bg = Get-BackgroundVirtualTerminalSequence $bg + } + + $fg = $this.ForegroundColor + if ($fg -and !(Test-VirtualTerminalSequece $fg)) { + $fg = Get-ForegroundVirtualTerminalSequence $fg + } + + $str = "${fg}${bg}${txt}${e}0m" + } + + return $str + } +} + +class GitPromptSettings { + [bool]$AnsiConsole = $Host.UI.SupportsVirtualTerminal -or ($Env:ConEmuANSI -eq "ON") + + [PoshGitCellColor]$DefaultColor = [PoshGitCellColor]::new() + [PoshGitCellColor]$BranchColor = [PoshGitCellColor]::new([ConsoleColor]::Cyan) + + [PoshGitCellColor]$IndexColor = [PoshGitCellColor]::new([ConsoleColor]::DarkGreen) + [PoshGitCellColor]$WorkingColor = [PoshGitCellColor]::new([ConsoleColor]::DarkRed) + [PoshGitCellColor]$StashColor = [PoshGitCellColor]::new([ConsoleColor]::Red) + [PoshGitCellColor]$ErrorColor = [PoshGitCellColor]::new([ConsoleColor]::Red) + + [PoshGitTextSpan]$BeforeText = [PoshGitTextSpan]::new(' [', [ConsoleColor]::Yellow) + [PoshGitTextSpan]$DelimText = [PoshGitTextSpan]::new(' |', [ConsoleColor]::Yellow) + [PoshGitTextSpan]$AfterText = [PoshGitTextSpan]::new(']', [ConsoleColor]::Yellow) + + [PoshGitTextSpan]$BeforeIndexText = [PoshGitTextSpan]::new('', [ConsoleColor]::DarkGreen) + [PoshGitTextSpan]$BeforeStashText = [PoshGitTextSpan]::new(' (', [ConsoleColor]::Red) + [PoshGitTextSpan]$AfterStashText = [PoshGitTextSpan]::new(')', [ConsoleColor]::Red) + + [PoshGitTextSpan]$LocalDefaultStatusSymbol = [PoshGitTextSpan]::new('', [ConsoleColor]::DarkGreen) + [PoshGitTextSpan]$LocalWorkingStatusSymbol = [PoshGitTextSpan]::new('!', [ConsoleColor]::DarkRed) + [PoshGitTextSpan]$LocalStagedStatusSymbol = [PoshGitTextSpan]::new('~', [ConsoleColor]::Cyan) + + [PoshGitTextSpan]$BranchGoneStatusSymbol = [PoshGitTextSpan]::new([char]0x00D7, [ConsoleColor]::DarkCyan) # × Multiplication sign + [PoshGitTextSpan]$BranchIdenticalStatusSymbol = [PoshGitTextSpan]::new([char]0x2261, [ConsoleColor]::Cyan) # ≡ Three horizontal lines + [PoshGitTextSpan]$BranchAheadStatusSymbol = [PoshGitTextSpan]::new([char]0x2191, [ConsoleColor]::Green) # ↑ Up arrow + [PoshGitTextSpan]$BranchBehindStatusSymbol = [PoshGitTextSpan]::new([char]0x2193, [ConsoleColor]::Red) # ↓ Down arrow + [PoshGitTextSpan]$BranchBehindAndAheadStatusSymbol = [PoshGitTextSpan]::new([char]0x2195, [ConsoleColor]::Yellow) # ↕ Up & Down arrow + + [BranchBehindAndAheadDisplayOptions]$BranchBehindAndAheadDisplay = [BranchBehindAndAheadDisplayOptions]::Full + + [string]$FileAddedText = '+' + [string]$FileModifiedText = '~' + [string]$FileRemovedText = '-' + [string]$FileConflictedText = '!' + [string]$BranchUntrackedText = '' + + [bool]$EnableStashStatus = $false + [bool]$ShowStatusWhenZero = $true + [bool]$AutoRefreshIndex = $true + + [bool]$EnablePromptStatus = !$global:GitMissing + [bool]$EnableFileStatus = $true + + [Nullable[bool]]$EnableFileStatusFromCache = $null + [string[]]$RepositoriesInWhichToDisableFileStatus = @() + + [string]$DescribeStyle = '' + [psobject]$EnableWindowTitle = 'posh~git ~ ' + + [string]$DefaultPromptPrefix = '' + [string]$DefaultPromptSuffix = '$(''>'' * ($nestedPromptLevel + 1)) ' + [string]$DefaultPromptDebugSuffix = ' [DBG]$(''>'' * ($nestedPromptLevel + 1)) ' + [bool]$DefaultPromptEnableTiming = $false + [bool]$DefaultPromptAbbreviateHomeDirectory = $false + + [int]$BranchNameLimit = 0 + [string]$TruncatedBranchSuffix = '...' + + [bool]$Debug = $false +} diff --git a/src/posh-git.psd1 b/src/posh-git.psd1 index 82be71d21..22898358f 100644 --- a/src/posh-git.psd1 +++ b/src/posh-git.psd1 @@ -4,7 +4,7 @@ RootModule = 'posh-git.psm1' # Version number of this module. -ModuleVersion = '1.0.0.0' +ModuleVersion = '1.0.0' # ID used to uniquely identify this module GUID = '74c9fd30-734b-4c89-a8ae-7727ad21d1d5' @@ -13,21 +13,29 @@ GUID = '74c9fd30-734b-4c89-a8ae-7727ad21d1d5' Author = 'Keith Dahlby and contributors' # Copyright statement for this module -Copyright = '(c) 2010-2017 Keith Dahlby and contributors' +Copyright = '(c) 2010-2018 Keith Dahlby and contributors' # Description of the functionality provided by this module Description = 'Provides prompt with Git status summary information and tab completion for Git commands, parameters, remotes and branch names.' # Minimum version of the Windows PowerShell engine required by this module -PowerShellVersion = '3.0' +PowerShellVersion = '5.0' # Functions to export from this module FunctionsToExport = @( 'Add-PoshGitToProfile', + 'Format-GitBranchName', + 'Get-GitBranchStatusColor', 'Get-GitStatus', 'Get-GitDirectory', 'Update-AllBranches', 'Write-GitStatus', + 'Write-GitBranchName', + 'Write-GitBranchStatus', + 'Write-GitIndexStatus', + 'Write-GitStashCount', + 'Write-GitWorkingDirStatus', + 'Write-GitWorkingDirStatusSummary', 'Write-Prompt', 'Write-VcsStatus', 'Get-SshAgent', @@ -43,7 +51,7 @@ FunctionsToExport = @( CmdletsToExport = @() # Variables to export from this module -VariablesToExport = @() +VariablesToExport = @('GitPromptScriptBlock') # Aliases to export from this module AliasesToExport = @() @@ -51,7 +59,6 @@ AliasesToExport = @() # Private data to pass to the module specified in RootModule/ModuleToProcess. # This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ - PSData = @{ # Tags applied to this module. These help with module discovery in online galleries. Tags = @('git', 'prompt', 'tab', 'tab-completion', 'tab-expansion', 'tabexpansion') @@ -65,10 +72,9 @@ PrivateData = @{ # ReleaseNotes of this module ReleaseNotes = 'https://github.com/dahlbyk/posh-git/blob/develop/CHANGELOG.md' - # TODO: REMOVE BEFOE RELEASE - PreReleaseVersion = 'pre00' + # OVERRIDE THIS FIELD FOR PUBLISHED RELEASES - LEAVE AT 'alpha' FOR CLONED/LOCAL REPO USAGE + Prerelease = 'alpha' } - } } diff --git a/src/posh-git.psm1 b/src/posh-git.psm1 index 5d0b91926..43db716e7 100644 --- a/src/posh-git.psm1 +++ b/src/posh-git.psm1 @@ -5,6 +5,7 @@ param([switch]$NoVersionWarn, [switch]$ForcePoshGitPrompt) . $PSScriptRoot\ConsoleMode.ps1 . $PSScriptRoot\Utils.ps1 . $PSScriptRoot\AnsiUtils.ps1 +. $PSScriptRoot\PoshGitTypes.ps1 . $PSScriptRoot\GitUtils.ps1 . $PSScriptRoot\GitPrompt.ps1 . $PSScriptRoot\GitParamTabExpansion.ps1 @@ -26,8 +27,78 @@ else { $defaultPromptDef = $initialSessionState.Commands['prompt'].Definition } -# If there is no prompt function or the prompt function is the default, replace the current prompt function definition -$poshGitPromptScriptBlock = $null +# The built-in posh-git prompt function in ScriptBlock form. +$GitPromptScriptBlock = { + $settings = $global:GitPromptSettings + if (!$settings) { + return "<`$GitPromptSettings not found> " + } + + if ($settings.DefaultPromptEnableTiming) { + $sw = [System.Diagnostics.Stopwatch]::StartNew() + } + + $origLastExitCode = $global:LASTEXITCODE + + # A UNC path has no drive so it's better to use the ProviderPath e.g. "\\server\share". + # However for any path with a drive defined, it's better to use the Path property. + # In this case, ProviderPath is "\LocalMachine\My"" whereas Path is "Cert:\LocalMachine\My". + # The latter is more desirable. + $pathInfo = $ExecutionContext.SessionState.Path.CurrentLocation + $currentPath = if ($pathInfo.Drive) { $pathInfo.Path } else { $pathInfo.ProviderPath } + + # File system paths are case-sensitive on Linux and case-insensitive on Windows and macOS + if (($PSVersionTable.PSVersion.Major -ge 6) -and $IsLinux) { + $stringComparison = [System.StringComparison]::Ordinal + } + else { + $stringComparison = [System.StringComparison]::OrdinalIgnoreCase + } + + # Abbreviate path by replacing beginning of path with ~ *iff* the path is in the user's home dir + $abbrevHomeDir = $settings.DefaultPromptAbbreviateHomeDirectory + if ($abbrevHomeDir -and $currentPath -and $currentPath.StartsWith($Home, $stringComparison)) { + $currentPath = "~" + $currentPath.SubString($Home.Length) + } + + $prompt = '' + + # Display default prompt prefix if not empty. + $defaultPromptPrefix = [string]$settings.DefaultPromptPrefix + if ($defaultPromptPrefix) { + $expandedDefaultPromptPrefix = $ExecutionContext.SessionState.InvokeCommand.ExpandString($defaultPromptPrefix) + $prompt += Write-Prompt $expandedDefaultPromptPrefix + } + + # Write the abbreviated current path + $prompt += Write-Prompt $currentPath + + # Write the Git status summary information + $prompt += Write-VcsStatus + + # If stopped in the debugger, the prompt needs to indicate that in some fashion + $hasInBreakpoint = [runspace]::DefaultRunspace.Debugger | Get-Member -Name InBreakpoint -MemberType property + $debugMode = (Test-Path Variable:/PSDebugContext) -or ($hasInBreakpoint -and [runspace]::DefaultRunspace.Debugger.InBreakpoint) + $promptSuffix = if ($debugMode) { $settings.DefaultPromptDebugSuffix } else { $settings.DefaultPromptSuffix } + + # If user specifies $null or empty string, set to ' ' to avoid "PS>" unexpectedly being displayed + if (!$promptSuffix) { + $promptSuffix = ' ' + } + + $expandedPromptSuffix = $ExecutionContext.SessionState.InvokeCommand.ExpandString($promptSuffix) + + # If prompt timing enabled, display elapsed milliseconds + if ($settings.DefaultPromptEnableTiming) { + $sw.Stop() + $elapsed = $sw.ElapsedMilliseconds + $prompt += Write-Prompt " ${elapsed}ms" + } + + $global:LASTEXITCODE = $origLastExitCode + $prompt += $expandedPromptSuffix + $prompt +} $currentPromptDef = if ($funcInfo = Get-Command prompt -ErrorAction SilentlyContinue) { $funcInfo.Definition } @@ -43,75 +114,10 @@ if (!$currentPromptDef) { function global:prompt { ' ' } } +# If there is no prompt function or the prompt function is the default, replace the current prompt function definition if ($ForcePoshGitPrompt -or !$currentPromptDef -or ($currentPromptDef -eq $defaultPromptDef)) { - $poshGitPromptScriptBlock = { - if ($GitPromptSettings.DefaultPromptEnableTiming) { - $sw = [System.Diagnostics.Stopwatch]::StartNew() - } - $origLastExitCode = $global:LASTEXITCODE - - # A UNC path has no drive so it's better to use the ProviderPath e.g. "\\server\share". - # However for any path with a drive defined, it's better to use the Path property. - # In this case, ProviderPath is "\LocalMachine\My"" whereas Path is "Cert:\LocalMachine\My". - # The latter is more desirable. - $pathInfo = $ExecutionContext.SessionState.Path.CurrentLocation - $currentPath = if ($pathInfo.Drive) { $pathInfo.Path } else { $pathInfo.ProviderPath } - - # File system paths are case-sensitive on Linux and case-insensitive on Windows and macOS - if (($PSVersionTable.PSVersion.Major -ge 6) -and $IsLinux) { - $stringComparison = [System.StringComparison]::Ordinal - } - else { - $stringComparison = [System.StringComparison]::OrdinalIgnoreCase - } - - # Abbreviate path by replacing beginning of path with ~ *iff* the path is in the user's home dir - $abbrevHomeDir = $GitPromptSettings.DefaultPromptAbbreviateHomeDirectory - if ($abbrevHomeDir -and $currentPath -and $currentPath.StartsWith($Home, $stringComparison)) - { - $currentPath = "~" + $currentPath.SubString($Home.Length) - } - - $res = '' - - # Display default prompt prefix if not empty. - $defaultPromptPrefix = [string]$GitPromptSettings.DefaultPromptPrefix - if ($defaultPromptPrefix) { - $expandedDefaultPromptPrefix = $ExecutionContext.SessionState.InvokeCommand.ExpandString($defaultPromptPrefix) - $res += Write-Prompt $expandedDefaultPromptPrefix - } - - # Write the abbreviated current path - $res += Write-Prompt $currentPath - - # Write the Git status summary information - $res += Write-VcsStatus - - # If stopped in the debugger, the prompt needs to indicate that in some fashion - $hasInBreakpoint = [runspace]::DefaultRunspace.Debugger | Get-Member -Name InBreakpoint -MemberType property - $debugMode = (Test-Path Variable:/PSDebugContext) -or ($hasInBreakpoint -and [runspace]::DefaultRunspace.Debugger.InBreakpoint) - $promptSuffix = if ($debugMode) { $GitPromptSettings.DefaultPromptDebugSuffix } else { $GitPromptSettings.DefaultPromptSuffix } - - # If user specifies $null or empty string, set to ' ' to avoid "PS>" unexpectedly being displayed - if (!$promptSuffix) { - $promptSuffix = ' ' - } - - $expandedPromptSuffix = $ExecutionContext.SessionState.InvokeCommand.ExpandString($promptSuffix) - - # If prompt timing enabled, display elapsed milliseconds - if ($GitPromptSettings.DefaultPromptEnableTiming) { - $sw.Stop() - $elapsed = $sw.ElapsedMilliseconds - $res += Write-Prompt " ${elapsed}ms" - } - - $global:LASTEXITCODE = $origLastExitCode - $res + $expandedPromptSuffix - } - # Set the posh-git prompt as the default prompt - Set-Item Function:\prompt -Value $poshGitPromptScriptBlock + Set-Item Function:\prompt -Value $GitPromptScriptBlock } # Install handler for removal/unload of the module @@ -120,7 +126,7 @@ $ExecutionContext.SessionState.Module.OnRemove = { # Check if the posh-git prompt function itself has been replaced. If so, do not restore the prompt function $promptDef = if ($funcInfo = Get-Command prompt -ErrorAction SilentlyContinue) { $funcInfo.Definition } - if ($promptDef -eq $poshGitPromptScriptBlock) { + if ($promptDef -eq $GitPromptScriptBlock) { Set-Item Function:\prompt -Value ([scriptblock]::Create($defaultPromptDef)) return } @@ -131,10 +137,18 @@ $ExecutionContext.SessionState.Module.OnRemove = { $exportModuleMemberParams = @{ Function = @( 'Add-PoshGitToProfile', + 'Format-GitBranchName', + 'Get-GitBranchStatusColor', 'Get-GitDirectory', 'Get-GitStatus', 'Update-AllBranches', 'Write-GitStatus', + 'Write-GitBranchName', + 'Write-GitBranchStatus', + 'Write-GitIndexStatus', + 'Write-GitStashCount', + 'Write-GitWorkingDirStatus', + 'Write-GitWorkingDirStatusSummary', 'Write-Prompt', 'Write-VcsStatus', 'Get-SshAgent', @@ -145,6 +159,9 @@ $exportModuleMemberParams = @{ 'TabExpansion', 'tgit' ) + Variable = @( + 'GitPromptScriptBlock' + ) } Export-ModuleMember @exportModuleMemberParams