Automate Visual Studio Team Services with PowerShell

One of the main skills programmers have is the ability to automate the mundane. In my day to day job of both working on and with Visual Studio Team Services (VSTS) I find myself performing many of the same tasks over and over. And whenever these tasks require me to leave the comfort of my PowerShell prompt I get grumpy. Since I rather not be grump I created a project called PsVsts to help me automate those tasks.

PsVsts is a PowerShell module which provides several handy commandlets for working with VSTS. The point of this project is not to simply create a wrapper around the public REST apis but to lift those apis up a level to make them more convinient to use. Because of that many of commandlets consume multiple APIs.

The project currently contains the following commandlets:

  • Get-MyWorkItems Gets the work items that are assigned to or created by you. Provides easy way to filter by open vs finished items.
  • Get-WorkItems Gets the work items given a query.
  • Open-WorkItems Opens work items in your web browser.
  • Push-ToVsts
    Takes a local git repo, creates a corresponding repo in your VSTS project, adds that repo as a remote origin and pushes your local repo to it.
  • Submit-PullRequest Submits a pull request
  • Get-Builds Gets a list of builds
  • Set-VstsConfig Sets a config value for use in other PsVsts functions
  • Get-VstsConfig Gets the config values

 

The commands I use most often are the Get-MyWorkItems and Submit-PullRequest. The Submit-PullRequest commandlet wraps both the pull request and identity apis to allow me to easily create a PR and add reviewers using their name or email. I use this commandlet as part of function in my PowerShell profile to automate my common worflow to make changes and submit them for review:

function fastFix($branchName, $title) {
  
  if((-not $branchName) -or (-not $title)) {
    throw "You must specify branchName and title"
    return
  }
  
  Write-Host "Checking out $branchName"
  git checkout -b $branchName
  
  Write-Host "Adding all files and committing"
  git commit -am $title
  
  Write-Host "Pushing to server"
  git push -u origin $branchName
  
  Write-Host "Submit Pull Request"
  Submit-PullRequest -Title $title -Description $title -SourceBranch $branchName -TargetBranch master -Project MyProject -Account MyAccount -Repository MyRepo
  
}

I use the Get-MyWorkItems commandlet to quickly see what is assigned to me and then I can pipe that to Open-WorkItems to view them in the browser. By default this commandlet will try to show only open items.

Get-MyWorkitems -AssignedToMe | Open-WorkItems.

 

You can easily install these commandlets on your machine using the PsVsts Chocolatey package.

choco install psvsts

I hope these may be of help to any other PowerShell users who also loathe being grumpy :)

 

Quickly moving up a directory tree

I am lazy and I have been working recently on optimizing my PowerShell profile to become faster from the command line. My most recent target was a command I run often to move up a directory:

> cd ../

Now that may not look like a lot of characters but it gets worse when I want to jump 3 or 4 levels up:

> cd ../../../../

Annoying! To fix this I added this little snippet to my PowerShell profile:

for($i = 1; $i -le 5; $i++){
  $u =  "".PadLeft($i,"u")
  $unum =  "u$i"
  $d =  $u.Replace("u","../")
  Invoke-Expression "function $u { push-location $d }"
  Invoke-Expression "function $unum { push-location $d }"
}

This beautiful bit of code generates two sets of functions. One lets me move up a directory a given number of times by running a command like:

> u4

This will move up four directories. The second set of functions does the same thing but by typing u the number of times you want to traverse up:

> uuuu

Now I can hear you saying “Hey Matt, You claim to be all about being fast from the command line so why would you ever run the command uuuu when you can just run u4?”

That is an astute observation nameless reader! But sometimes I am in a mood where it is satisfying to press one key repeatedly. Ok?

 

Moving and renaming resource keys in a .resx file

I work on websites where we have several resource files that are localized in many languages. This makes operations like renaming a resource key and moving a resource key to a different file annoying since you must do it across several languages. To help with this I created a couple of PowerShell scripts to automate this process.

These PowerShell scripts assume your .resx files live in a folder named “Resources” but this can be easily changed.

RenameResource.ps1

param([String]$keyOld, [String]$keyNew)
if(-not $keyOld -or -not $keyOld) {
  echo "RenameResource.ps1 keyOld keyNew"
  return
}

$files=get-childitem Resources *.resx

foreach($file in $files) {
  $xml = [xml](Get-Content $file.FullName)
  $dataNode = $xml.root.data | Where-Object { $_.GetAttribute("name") -eq $keyOld }
  if ($dataNode -ne $null) {
    $dataNode.SetAttribute("name", $keyNew)
    $xml.Save($file.FullName)
  }
}

 

MoveResource.ps1

param([String]$resourceNameSource, [String]$resourceNameDestination, [string] $keyName)
if(-not $resourceNameSource -or -not $resourceNameDestination -or -not $keyName) {
  echo "MoveResource.ps1 resourceNameSource resourceNameDestination keyName"
  return
}

$sourceFiles = Get-ChildItem "Resources\$resourceNameSource*.resx"
$destFiles = Get-ChildItem "Resources\$resourceNameDestination*.resx"
$mapping = @{}

$sourceFiles | foreach {
  $parts = $_ -split "\."
  $list = New-Object "system.collections.arraylist"
  $list.Add($_) > $null
  if($parts.Length -eq 2) {
    $mapping["en-us"] = $list
  }
  else {
    $mapping[$parts[1]] = $list
  }
}

$destFiles | foreach {
  $parts = $_ -split "\."
  if($parts.Length -eq 2) {
    $mapping["en-us"].Add($_ ) > $null
  }
  else {
    $mapping[$parts[1]].Add($_ ) > $null
  }
}

foreach($pair in $mapping.Values) {
  $sourcePath = $pair[0]
  $destPath = $pair[1]
  $source = [xml](Get-Content $sourcePath)
  $dest = [xml](Get-Content $destPath)

  $keyNodeSource = $source.root.data | Where-Object { $_.GetAttribute("name") -eq $keyName }
  $keyNodeDest = $dest.root.data | Where-Object { $_.GetAttribute("name") -eq $keyName }
  if ($keyNodeSource -eq $null) {
      echo "Key does not exist in source"
      return;
  }
  if($keyNodeDest -ne $null) {
    echo "Key already exists in destination"
    return;
  }
  try
  {
    $newNode = $dest.ImportNode($keyNodeSource.Clone(), $true)
    $source.root.RemoveChild($keyNodeSource) > $null
    $dest.root.AppendChild($newNode) > $null
    $dest.Save($destPath)
    $source.Save($sourcePath)
  }
  catch {
    Write-Host "Error text: " $_
    return
  }
}

 

Usage

Renaming a resource key:

.\RenameResource.ps1 oldKey newKey

 

Moving a resource with key “keyName”  from a file named “ResourceFile1.resx” to “ResourceFile2.resx”:

.\MoveResource.ps1 ResourceFile1 ResourceFile2 keyName

 

 

A combined Mercurial and Git PowerShell Prompt

A while ago I posted the code I use to create a custom PowerShell prompt to show the status of my Mercurial repositories. Since then I have also started using Git so I updated my prompt to work for both. I find this prompt very convenient since it quickly shows what branch you are in as well as what pending changes you have in your working directory.

Here is the updated code:

if (test-path function:\prompt) {
   $oldPrompt = ls function: | ? {$_.Name -eq "prompt"}
   remove-item -force function:\prompt
 }
 function prompt {
  function getGitStatus {
  $branch = git branch 2>&1
  if($branch.Exception -eq $null) {
    $status = "git"
    $branch | foreach {
      if ($_ -match "^\*(.*)"){
        $status += $matches[1]
      }
    }

    $untracked = 0
    $updates = 0
    $deletes = 0
    $addsIndex = 0
    $deletesIndex =0
    $updatesIndex = 0

    git status --porcelain | foreach {
      if($_ -match "^([\w?]|\s)([\w?])?\s*") {
        switch ($matches[1]) {
          "A" {$addsIndex++ }
          "M" {$updatesIndex++ }
          "D" {$deletesIndex++ }
          "?" {$untracked++ }
        }
        switch ($matches[2]) {
          "A" {$adds++ }
          "M" {$updates++ }
          "D" {$deletes++ }
         }
      }
    }
    $status += " a:" +$addsIndex
    if($adds -gt 0) {
      $status += "($adds)"
    }

    $status += " m:" +$updatesIndex
    if($updates -gt 0) {
      $status += "($updates)"
    }

    $status += " d:" +$deletesIndex
    if($deletes -gt 0) {
      $status += "($deletes)"
    }     

    $status += " ?:$untracked"
    return $status
  }
  else {
    return $false
  }
}

  function getHgStatus {
    $summary = hg summary 2>&1
    if($summary.Exception -eq $null) {
      $regex = "(?si)(parent:(?<parent>.*?)(\n|\r)+.*?)(branch:(?<branch>.*)\s)(commit:(?<commit>.*)\s)(update:(?<update>.*))";
      $summary = [System.String]::Join([System.Environment]::NewLine,$summary)
      $res = $summary -match $regex
      $status = "hg {0} c:{1}" -f $matches["branch"].Trim(), $matches["commit"].Trim()
      return $status
   }
   else {
    return $false
   }
  }

  $status = getGitStatus
  if(-not $status) {
      $status = getHgStatus
  }
  $host.ui.rawui.WindowTitle = (get-location).Path
  if($status) {
    write-host ($status) -NoNewLine -ForegroundColor Green
    write-host (">") -NoNewLine -ForegroundColor Green
  }
  else {
    & $oldPrompt
  }
  return " "
}

After adding this prompt to your profile when you are in a git repository you will see this:

This is more complex than the Mercurial prompt since git has the concept of a staging area. The number not in parentheses is the “staged” value and the number in parentheses is the unstaged value. So in the prompt above the repository has 1 modification staged and one unstaged.

When you are in a Mercurial repository you will see this:

hgPrompt

 

Launching the TortoiseHg log more conveniently from the command line

When working with Mercurial I usually perform most tasks from the command line but often I want to be able to visually explore the history of the whole repository or of a single file.  For this I find TortoiseHg’s repository explorer very useful.

image

It is easy to launch it from the command line by executing the “hgtk log” command. The annoying thing about this command is that you must specify the full path of a file to see it’s history.  I am lazy and don’t want to type the following in to see the history of a file

htgk log .\website\content\css\myfile.css

To make this quicker I wrote a PowerShell function called hlog. Just place the following function in your PowerShell profile:

function hlog ($file) {
  if (-not $file) {
    hgtk log
  }
  elseif(Test-Path -LiteralPath $file -ErrorAction SilentlyContinue) {
    hgtk log $file
  }
  else {
    $path = Split-Path $file
    $leaf = Split-Path $file -Leaf
    Get-ChildItem $path -include $leaf -recurse | Where-Object { hgtk log $_.FullName }
  }
}

With this you have much greater flexibility in how you launch the visual log.  You can…

Launch for the whole repository

hlog

Launch it using a full file path

hlog .\website\content\css\myfile.css

Launch it with just the file name
(This will launch the log for every file with the name “myfile.css” in your source tree)

hlog myfile.css

Launch for files matching wild cards
(This will match all css files two directories deep that start with the word my)

hlog *\*\my*.css