Using Git with Windows Powershell

Whenever I’m working with a Git repository I tend to do so from a Windows Powershell command line running inside Console. I prefer this to the UNIX-like “git bash” that comes with msysgit and it’s fairly straightforward to extend Powershell to provide git integration.

Custom Prompt

The first thing that I have is a custom prompt when I’m within a Git repository:

gitprompt

This prompt shows various pieces of information about the current git repository:

  • “1.3” – The name of the current branch that I’m working on
  • “+0” – The number of files that have been added since the last commit
  • “~1” – The number of files that have been modified since the last commit
  • “-0” – The number of files that have been deleted since the last commit
  • “!” – Whether or not there are untracked files in the working directory

Essentially this is implemented by writing a custom “prompt” function inside your Powershell profile. My colleague Mark Embling wrote about this in detail.

Tab Expansion

I also use a custom TabExpansion function that provides tab-completion for common git commands.

For example, if I type “git ch<tab>” then powershell will insert “git checkout”. This is taken further by providing auto-completion for branch names and remote names, so typing “git push <tab>” would automatically fill in “git push origin” and “git push origin <tab>” would then cycle through my local branches.

To implement a custom TabExpansion function, you’ll need to open your Powershell profile (stored in DocumentsWindowsPowershellProfile.ps1). If this file doesn’t exist, then you’ll need to create it.

Next, create a function called “TabExpansion”. This function takes two arguments – the last word that was entered before the user hit the tab key as well as the entire line that has been entered into the prompt. Inside this function, we want to parse the entire line in order to extract the last “block” (ie the last piece of input not separated by a semicolon or a pipe):

function TabExpansion($line, $lastWord) {
  $LineBlocks = [regex]::Split($line, '[|;]')
  $lastBlock = $LineBlocks[-1] 
}

Next, we can use a regular expression to see whether the block begins with “git “. If so, then we’ll hand the input off to a function called “gitTabExpansion”:

function TabExpansion($line, $lastWord) {
  $LineBlocks = [regex]::Split($line, '[|;]')
  $lastBlock = $LineBlocks[-1] 
 
  switch -regex ($lastBlock) {
    'git (.*)' { gitTabExpansion($lastBlock) }
  }
}

The gitTabExpansion again uses regular expressions to identify what the user has entered and acts appropriately:

function gitTabExpansion($lastBlock) {
     switch -regex ($lastBlock) {
 
        #Handles git branch -x -y -z <branch name>
        'git branch -(d|D) (S*)$' {
          gitLocalBranches($matches[2])
        }
 
        #handles git checkout <branch name>
        #handles git merge <brancj name>
        'git (checkout|merge) (S*)$' {
          gitLocalBranches($matches[2])
        }
 
        #handles git <cmd>
        #handles git help <cmd>
        'git (help )?(S*)$' {      
          gitCommands($matches[2])
        }
 
        #handles git push remote <branch>
        #handles git pull remote <branch>
        'git (push|pull) (S+) (S*)$' {
          gitLocalBranches($matches[3])
        }
 
        #handles git pull <remote>
        #handles git push <remote>
        'git (push|pull) (S*)$' {
          gitRemotes($matches[2])
        }
    }	
}

Hopefully the comments are pretty self explanatory. The gitLocalBranches, gitCommands and gitRemotes functions invoke the appropriate git command and parse the output:

function gitCommands($filter) {
  $cmdList = @()
  $output = git help
  foreach($line in $output) {
    if($line -match '^   (S+) (.*)') {
      $cmd = $matches[1]
      if($filter -and $cmd.StartsWith($filter)) {
        $cmdList += $cmd.Trim()
      }
      elseif(-not $filter) {
        $cmdList += $cmd.Trim()
      }
    }
  }
 
  $cmdList | sort
 }
 
function gitRemotes($filter) {
  if($filter) {
    git remote | where { $_.StartsWith($filter) }
  }
  else {
    git remote
  }
}
 
function gitLocalBranches($filter) {
   git branch | foreach { 
      if($_ -match "^*?s*(.*)") { 
        if($filter -and $matches[1].StartsWith($filter)) {
          $matches[1]
        }
        elseif(-not $filter) {
          $matches[1]
        }
      } 
   }
}

At the moment the functionality is quite limited but already I find this extremely useful when working with git repositories – it has already saved me countless keystrokes throughout the day.

I have also created similar functionality for Mercurial repositories which I’ve posted about here.

Written on March 7, 2010