A Git-Aware Prompt for PowerShell


PowerShell Core’s default prompt exposes the current path but no repository or privilege metadata. The following prompt function adds the current logon server, elevation state, Git branch, working-tree status, index status, and stash count without requiring a separate prompt framework.

Prompt Layout

The prompt consists of three colour-coded segments.

Logon server β€” Reads $env:LOGONSERVER, normalises its casing, and renders the result on a grey background followed by the 𝚸 glyph. This identifies the authentication server associated with the current session.

$LogonServer = ConvertTo-TitleCase -InputString $env:LOGONSERVER
Write-Host -NoNewline "${LogonServer}[`u{1D6B8}] " -BackgroundColor Gray -ForegroundColor Black

Working directory and elevation state β€” Resolves the current Windows principal and checks membership in the built-in Administrators role. The working directory uses a blue background for a standard session and a dark-red background for an elevated session.

$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
  [Security.Principal.WindowsBuiltInRole]::Administrator)

$location = (Get-Location).Path
if ($isAdmin) {
  Write-Host -NoNewline "$($location) " -ForegroundColor Gray -BackgroundColor DarkRed
} else {
  Write-Host -NoNewline "$($location) " -BackgroundColor Blue -ForegroundColor Black
}

Git status β€” When git is available and git branch --show-current returns a branch name, the third segment contains:

The segment is omitted outside a repository. It is also omitted in detached HEAD state because git branch --show-current returns an empty string in that case.

Status values are mapped to colours and indicators as follows:

$isDirty = $hasUnstagedChanges -or $hasStagedChanges -or $hasUntrackedFiles
$bgColor = if ($isDirty) { "DarkYellow" } else { "DarkGreen" }

$stashCount = (git stash list 2>$null).Count
$stashInfo = if ($stashCount -gt 0) { " `u{1D6B8}:$stashCount " } else { "" }

$unstagedIndicator   = if ($hasUnstagedChanges) { "`u{1D6B3} " } else { "" }
$stagedIndicator     = if ($hasStagedChanges)   { "`u{1D6B1} " } else { "" }
$untrackedIndicator  = if ($hasUntrackedFiles)  { "`u{1D6B0}"  } else { "" }

Write-Host -NoNewline "`u{1D6A9}:" -ForegroundColor Black -BackgroundColor $bgColor
Write-Host -NoNewline "$gitBranch" -ForegroundColor Black -BackgroundColor $bgColor
# ... stash count and status indicators follow
Write-Host -NoNewline "$untrackedIndicator$unstagedIndicator$stagedIndicator" `
  -ForegroundColor Black -BackgroundColor $bgColor

Implementation Scope

Oh My Posh and Starship provide richer prompt engines, configuration formats, and theme ecosystems. This implementation targets a narrower requirement: one PowerShell function stored in $PROFILE, with no prompt binary, theme file, or Nerd Font dependency.

The function invokes git branch --show-current, git status --porcelain, and git stash list directly. Porcelain status output is intended for script consumption: unlike the default git status presentation, its format does not depend on localisation or user-facing layout changes.

Parsing Porcelain Status

Each git status --porcelain record starts with a two-character XY status field:

The parser performs one pass over the records and maintains three Boolean flags. It does not retain paths or count records because rendering depends only on whether each category is present. The parsing step therefore uses constant additional state; repository scanning and status generation remain the dominant costs.

$gitStatusOutput = git status --porcelain 2>$null

$hasUnstagedChanges = $false
$hasStagedChanges   = $false
$hasUntrackedFiles  = $false

if ($gitStatusOutput) {
  foreach ($line in $gitStatusOutput) {
    if ($line.StartsWith('??')) {
      $hasUntrackedFiles = $true
      continue
    }
    # Second character: work tree (unstaged) status
    if ($line.Length -gt 1 -and $line[1] -ne ' ') {
      $hasUnstagedChanges = $true
    }
    # First character: index (staged) status
    if ($line.Length -gt 0 -and $line[0] -ne ' ' -and $line[0] -ne '?') {
      $hasStagedChanges = $true
    }
  }
}

Normalising the Server Name

ConvertTo-TitleCase formats the value of LOGONSERVER, which commonly contains leading backslashes and an uppercase host name. The function preserves any leading non-letter characters, uppercases the first letter, and lowercases the remaining substring. Empty input and strings without letters are returned unchanged.

function ConvertTo-TitleCase {
  param(
    [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
    [string]$InputString
  )

  if ([string]::IsNullOrEmpty($InputString)) { return $InputString }

  for ($i = 0; $i -lt $InputString.Length; $i++) {
    if ([char]::IsLetter($InputString[$i])) {
      return $InputString.Substring(0, $i) +
             $InputString[$i].ToString().ToUpper() +
             $InputString.Substring($i + 1).ToLower()
    }
  }

  return $InputString
}

Runtime Characteristics

The visual encoding provides four independent state signals:

The function starts up to three Git processes whenever PowerShell renders the prompt inside a repository. That cost is usually small, but it can become noticeable in large repositories, on slow file systems, or when Git status requires expensive work-tree traversal.

To install the prompt, add both functions to $PROFILE (code $PROFILE) and use a Unicode-capable terminal. Windows Terminal supports the Mathematical Bold Capital glyphs used here without requiring a Nerd Font.

Complete Source

function ConvertTo-TitleCase {
  param(
    [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
    [string]$InputString
  )

  if ([string]::IsNullOrEmpty($InputString)) {
    return $InputString
  }

  # Find the first letter and capitalize it
  for ($i = 0; $i -lt $InputString.Length; $i++) {
    if ([char]::IsLetter($InputString[$i])) {
      return $InputString.Substring(0, $i) + $InputString[$i].ToString().ToUpper() + $InputString.Substring($i + 1).ToLower()
    }
  }

  # If no letter found, return original string
  return $InputString
}

# Enhanced git prompt for PowerShell Core
function prompt {

  # Show logon server with a PowerShell icon and convert to title case
  $LogonServer = ConvertTo-TitleCase -InputString $env:LOGONSERVER
  Write-Host -NoNewline "${LogonServer}[`u{1D6B8}] " -BackgroundColor Gray -ForegroundColor Black

  # Check if running as admin
  $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

  # Start with the location part of the prompt
  $location = (Get-Location).Path
  if ($isAdmin) {
    Write-Host -NoNewline "$($location) " -ForegroundColor Gray -BackgroundColor DarkRed
  }
  else {
    Write-Host -NoNewline "$($location) " -BackgroundColor Blue -ForegroundColor Black
  }

  # Get Git branch and status if in a git repository
  if (Get-Command git -ErrorAction SilentlyContinue) {
    try {
      $gitBranch = git branch --show-current 2>$null
      if ($gitBranch) {
        # Get all status output
        $gitStatusOutput = git status --porcelain 2>$null

        # Initialize status flags
        $hasUnstagedChanges = $false
        $hasStagedChanges = $false
        $hasUntrackedFiles = $false

        if ($gitStatusOutput) {
          foreach ($line in $gitStatusOutput) {
            # Check for untracked files (indicated by '??')
            if ($line.StartsWith('??')) {
              $hasUntrackedFiles = $true
              continue
            }

            # Check if the second character is not a space (indicating unstaged change)
            if ($line.Length -gt 1 -and $line[1] -ne ' ') {
              $hasUnstagedChanges = $true
            }

            # Check if the first character is not a space (indicating staged change)
            if ($line.Length -gt 0 -and $line[0] -ne ' ' -and $line[0] -ne '?') {
              $hasStagedChanges = $true
            }
          }
        }

        # Set background color based on whether there are any changes
        $isDirty = $hasUnstagedChanges -or $hasStagedChanges -or $hasUntrackedFiles
        $bgColor = if ($isDirty) {
          "DarkYellow"
        }
        else {
          "DarkGreen"
        }

        # Check for stashes
        $stashCount = (git stash list 2>$null).Count
        $stashInfo = if ($stashCount -gt 0) {
          " `u{1D6B8}:$stashCount "
        }
        else {
          ""
        }

        $unstagedIndicator = if ($hasUnstagedChanges) {
          "`u{1D6B3} "
        }
        else {
          ""
        }
        $stagedIndicator = if ($hasStagedChanges) {
          "`u{1D6B1} "
        }
        else {
          ""
        }
        $untrackedIndicator = if ($hasUntrackedFiles) {
          "`u{1D6B0}"
        }
        else {
          ""
        }

        # Write opening angle bracket with background color
        Write-Host -NoNewline "`u{1D6A9}:" -ForegroundColor Black -BackgroundColor $bgColor

        # Write branch name
        Write-Host -NoNewline "$gitBranch" -ForegroundColor Black -BackgroundColor $bgColor

        # Add stash count if there are stashes
        if ($stashCount -gt 0) {
          Write-Host -NoNewline $stashInfo -ForegroundColor Black -BackgroundColor $bgColor
        }
        elseif ($isDirty) {
          # Write a space if there are no stashes
          Write-Host -NoNewline " " -ForegroundColor Black -BackgroundColor $bgColor
        }

        # Add status indicators
        Write-Host -NoNewline "$untrackedIndicator$unstagedIndicator$stagedIndicator" -ForegroundColor Black -BackgroundColor $bgColor

        # Determine if we need a separator
        $hasAnyIndicators = $stashCount -gt 0 -or $hasUnstagedChanges -or $hasStagedChanges -or $hasUntrackedFiles
        if ($hasAnyIndicators -eq $false) {
          Write-Host -NoNewline " " -ForegroundColor Black -BackgroundColor $bgColor
        }
      }
    }
    catch {
      # Not in a git repository or git command failed
    }
  }

  # Close the bracket
  if ($isAdmin) {
    Write-Host -NoNewline ">"
  }
  else {
    Write-Host -NoNewline ">"
  }

  # Return a space for the actual prompt
  return " "
}