While I'm a rather experienced .NET developer, I just had a first crack at a PowerShell script. I'm not entirely new to it, but never written anything so involved.
Script description and goals
At our company, the root of each SVN branch or trunk has 3 folders that have the property svn:externals
set with a list of externals. These folders are called Build
, Binaries
and Utilities
- hence the default value for the -Directories
parameter.
These are "pegged" at a certain revision. From time to time, we want to increase those revisions, and as a best practice we usually increase them all at once. Using Tortoise SVN as a GUI tool, it is rather cumbersome, so I've written a script.
About SVN
The script assumes svn.exe
is available on the PATH
environment variable.
Getting externals:
svn.exe propget svn:externals Build
Returns
Foo -r2015 http://svn.acme.com/common/Foo Bar -r2015 http://svn.acme.com/common/Bar Baz -r2015 http://svn.acme.com/common/Baz
Setting exernals:
svn.exe propset svn:externals -F externals.txt Build
Sets the externals on the Build folder using the file externals.txt
that contains a list equivalent to the output above.
Entire Code Dump
Stuff for Get-Help
<#
.SYNOPSIS
Script to change versions of SVN externals.
.DESCRIPTION
This script changes the explicit versions of SVN externals on specified directories. The default set consists of the "Binaries", "Build" and "Utilities" folders.
The script scans the current externals and determining the version that occurs the most times.
It then only changes externals whose version matches the determined version, to the new version specified.
Optionally, when using the Force parameter, other externals will be upgraded as well, effectively making all versions equal.
.NOTES
It is not possible to just provide additional directories to add to the default set.
If the Directories parameter is specified, only those directories are modified.
.PARAMETER Directories
The directories to change svn:externals on. Default: Binaries, Build, Utilities.
.PARAMETER NewRevision
The version to set for externals. If not specified, it will be prompted for.
.PARAMETER Force
If not all externals have the same version when the script is executed, only the "most occurring" version gets updated.
Use the -Force parameter to change all externals.
#>
Script code
Param(
[string[]]$Directories = ("Binaries", "Build", "Utilities"),
[int]$NewRevision = -1,
[switch]$Force = $false
)
$ErrorActionPreference = "Stop"
$REVISION_REGEX_FORMAT = "(?mi)(?<beforeNumber>.+\-r)(?<number>{0})(?<afterNumber>.*)"
$REVISION_REGEX_ALL = $REVISION_REGEX_FORMAT -f "[0-9]+"
function Invoke-Svn([string]$arguments)
{
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "svn.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $arguments
$pinfo.WorkingDirectory = $PWD
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
if($p.ExitCode -ne 0)
{
throw $stderr
}
return $stdout
}
function Get-SvnRevisions([string]$dir)
{
return $(Invoke-Svn "propget svn:externals $dir") -split '\n'
}
function Get-YesNoAnswer([string]$question)
{
return ("y", "yes", "true") -icontains (Read-Host $question)
}
# Keep prompting for the required NewRevision until it is >= 1
while($NewRevision -le 0)
{
$NewRevision = Read-Host "Please specify a value for NewRevision >= 1"
}
# Assemble revisions hashtable per directory
$revisions = @{}
$Directories | % { $revisions[$_] = @(Get-SvnRevisions $_)}
# Print a list of currently defined externals on each folder
Write-Host "The following externals are currently defined" -ForegroundColor Yellow
Write-Host "==============================================" -ForegroundColor Yellow
$Directories | % { Write-Host $_; $revisions[$_] | % { Write-Host "`t$_" } }
# Extract an int array that contains all the revision numbers
$allRevisions = $revisions.Values | % { $_ -join "`n" }
$revisionNumbers = [int[]] ([regex]::Matches($allRevisions, $REVISION_REGEX_ALL, "IgnoreCase") | % { $_.Groups["number"].Value })
# If all revisions are the same, all of them can be changed without problem.
# If they are not, and the user has not explicitly provided the -Force switch already, prompt to change all revisions.
$changeAllRevisions = $Force -or (($revisionNumbers | Select-Object -Unique).Count -eq 1)
if($changeAllRevisions -eq $false)
{
if (Get-YesNoAnswer("Not all externals are set at the same revision. Replace ALL external revisions?") -eq $true)
{
$changeAllRevisions = $true
}
}
$newRevisions = @{}
$tmpFile = "tmpExternals.txt"
$regexToUse = $REVISION_REGEX_ALL
if($changeAllRevisions -eq $false)
{
# If user does not wish to change all revisions: take the revision with the highest occurrence
$regexToUse = $REVISION_REGEX_FORMAT -f ($revisionNumbers | group | sort Count -Descending | select -First 1).Name
}
ForEach($dir in $Directories)
{
$newRevisions[$dir] = [regex]::Replace(($revisions[$dir] -join "`n"), $regexToUse, '${beforeNumber}'+$NewRevision+'${afterNumber}').Trim()
# Print a list of currently defined externals on each folder
Write-Host "New externals for ${dir}:" -ForegroundColor Green
$newRevisions[$dir] -split "`n" | % { Write-Host "`t$_" -ForegroundColor Green }
if(Get-YesNoAnswer("Write changes?") -eq $true)
{
#Easiest way to set multiple externals is to use a file...
Set-Content -Path $tmpFile -Value $NewRevisions[$dir] -NoNewline #NewLine is already present
Invoke-Svn "propset svn:externals -F $tmpFile $dir"
Remove-Item $tmpFile
}
else
{
Write-Host "Not modifiying directory ${dir}." -ForegroundColor Red
}
}
Exit 0;
Specific things that might be improve-able
Can I merge the declaration and assignment of $revisions
somehow, in the code below?
# Assemble revisions hashtable per directory
$revisions = @{}
$Directories | % { $revisions[$_] = @(Get-SvnRevisions $_)}
The following line is the only way I can figure out how to loop through the hash table's keys and then loop through the array of values associated with that key. I've tried toying around with the -Separator
parameter of Write-Host
, passing it the array directly, but couldn't quite get there.
$Directories | % { Write-Host $_; $revisions[$_] | % { Write-Host "`t$_" } }
Desired output (using horizontal tabs):
Build: Ext1 -r123 http://svn.whatever.com/Ext1 Ext2 -r123 http://svn.whatever.com/Ext1 Binaries: Ext3 -r123 http://svn.whatever.com/Ext3 Ext4 -r123 http://svn.whatever.com/Ext4 Utilities: Ext5 -r123 http://svn.whatever.com/Ext5 Ext6 -r123 http://svn.whatever.com/Ext6
By now you should all know what the list of externals looks like. This is my way of extracting an int[]
of just the revision numbers:
# Extract an int array that contains all the revision numbers
$allRevisions = $revisions.Values | % { $_ -join "`n" }
$revisionNumbers = [int[]] ([regex]::Matches($allRevisions, $REVISION_REGEX_ALL, "IgnoreCase") | % { $_.Groups["number"].Value })
Seems a bit long-winded, but idk if it can be shortened or made more readable.
The last block seems a little convoluted, maybe the area to focus on and look for improvement first.
All comments thoroughly appreciated!