3
\$\begingroup\$

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!

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.