Public/Find-AssemblyRevision.ps1

function Find-AssemblyRevision {
    <#
    .SYNOPSIS
        Finds the most recent SVN history interval with a given assembly revision value.
 
    .DESCRIPTION
        Reads committed snapshots of version.xml, newest first. Reports when the requested
        value was set and when it changed again. Uncommitted working-copy changes are ignored.
        Requires the Subversion command-line client and repository access.
 
    .PARAMETER Path
        Local working-copy path to version.xml, or its HTTP/HTTPS SVN repository URL.
 
    .PARAMETER AssemblyName
        The exact, case-sensitive assembly name to search for.
 
    .PARAMETER RevisionValue
        The revision value to find.
 
    .PARAMETER SvnPath
        Path or command name for the Subversion command-line client. Defaults to 'svn'.
 
    .PARAMETER ChunkSize
        Number of history entries fetched per SVN log request, newest first. The search
        stops as soon as the matching interval is found, so recent values are located
        without reading the entire history.
 
    .EXAMPLE
        Find-AssemblyRevision -Path '.\version.xml' -AssemblyName 'Example.Assembly' -RevisionValue 1
 
    .EXAMPLE
        Find-AssemblyRevision -Path 'https://svn.example.com/repos/project/Source/version.xml' -AssemblyName 'Example.Assembly' -RevisionValue 1
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $Path,
        [Parameter(Mandatory = $true, Position = 1)]
        [string] $AssemblyName,
        [Parameter(Mandatory = $true, Position = 2)]
        [ValidateRange(0, 2147483647)]
        [int] $RevisionValue,
        [string] $SvnPath = 'svn',
        [ValidateRange(1, 2147483647)]
        [int] $ChunkSize = 25
    )

    $ErrorActionPreference = 'Stop'

    if (-not (Get-Command -Name $SvnPath -ErrorAction SilentlyContinue)) {
        throw "SVN client '$SvnPath' was not found. Install the command-line client or supply -SvnPath."
    }

    function Invoke-SvnXml {
        param([string[]] $Arguments)

        $output = & $SvnPath @Arguments
        if ($LASTEXITCODE -ne 0) {
            throw "SVN failed (exit code $LASTEXITCODE): $($Arguments -join ' ')"
        }
        return [xml] ($output -join "`n")
    }

    [uri] $repositoryUri = $null
    if ([uri]::TryCreate($Path, [UriKind]::Absolute, [ref] $repositoryUri) -and
        $repositoryUri.Scheme -in @('http', 'https')) {
        $url = $Path
    }
    else {
        Write-Verbose "Resolving SVN repository URL for '$Path'."
        $info = Invoke-SvnXml -Arguments @('info', '--xml', '--non-interactive', '--', $Path)
        $url = [string] $info.info.entry.url
    }
    if ([string]::IsNullOrWhiteSpace($url)) {
        throw "No SVN URL was returned for '$Path'."
    }

    $target = "${url}@HEAD"
    Write-Verbose "Searching SVN history for '$url' in chunks of $ChunkSize, newest first."
    $firstMatch = $null
    $lastMatch = $null
    $newerEntry = $null
    $changedAway = $null
    $patch = $null
    $checkedCount = 0
    $done = $false
    $upperBound = 'HEAD'

    try {
        while (-not $done -and $null -ne $upperBound) {
            Write-Verbose "Fetching up to $ChunkSize history entries from SVN revision $upperBound downward."
            $log = Invoke-SvnXml -Arguments @('log', '--xml', '--non-interactive', '-r', "${upperBound}:0", '--limit', [string] $ChunkSize, '--', $target)
            $entries = @($log.log.logentry | Where-Object { $null -ne $_ })
            if ($entries.Count -eq 0) {
                break
            }

            foreach ($entry in $entries) {
                $checkedCount++
                Write-Progress -Id 1 -Activity 'Searching SVN assembly revision history' `
                    -Status "Checked $checkedCount entries; current SVN revision $($entry.revision)"
                Write-Verbose "Checking entry $checkedCount : SVN revision $($entry.revision) ($($entry.date))."
                $snapshot = Invoke-SvnXml -Arguments @('cat', '--non-interactive', '-r', [string] $entry.revision, '--', $target)
                $assembly = $snapshot.versions.assemblies.assembly |
                    Where-Object { $_.GetAttribute('name') -ceq $AssemblyName } |
                    Select-Object -First 1
                $revision = if ($null -ne $assembly) { $assembly.SelectSingleNode('revision') } else { $null }
                $matches = $null -ne $revision -and $revision.InnerText.Trim() -ceq [string] $RevisionValue

                if ($null -eq $revision) {
                    Write-Verbose "No revision element found for '$AssemblyName' in this snapshot."
                }
                else {
                    Write-Verbose "Assembly revision value: $($revision.InnerText.Trim()); patch: $($revision.GetAttribute('patch'))."
                }

                if ($matches) {
                    Write-Verbose "Match found in SVN revision $($entry.revision)."
                    if ($null -eq $lastMatch) {
                        $lastMatch = $entry
                        $changedAway = $newerEntry
                    }
                    $firstMatch = $entry
                    $patch = $revision.GetAttribute('patch')
                }
                elseif ($null -ne $firstMatch) {
                    Write-Verbose "Reached the preceding value in SVN revision $($entry.revision); the matching interval starts at SVN revision $($firstMatch.revision)."
                    $done = $true
                    break
                }
                $newerEntry = $entry
            }

            if ($done -or $entries.Count -lt $ChunkSize) {
                break
            }

            $oldestRevision = [long] $entries[-1].revision
            $upperBound = if ($oldestRevision -gt 0) { [string] ($oldestRevision - 1) } else { $null }
        }
    }
    finally {
        Write-Progress -Id 1 -Activity 'Searching SVN assembly revision history' -Completed
    }

    Write-Verbose "Search complete. Checked $checkedCount history entries."
    if ($null -eq $firstMatch) {
        Write-Warning "No committed snapshot has revision value $RevisionValue for '$AssemblyName'."
        return
    }

    [pscustomobject] @{
        AssemblyName = $AssemblyName
        RevisionValue = $RevisionValue
        PatchWhenSet = $patch
        SetInSvnRevision = [long] $firstMatch.revision
        SetDateUtc = [datetimeoffset]::Parse([string] $firstMatch.date).UtcDateTime
        SetBy = [string] $firstMatch.author
        SetMessage = [string] $firstMatch.msg
        LastMatchingSvnRevision = [long] $lastMatch.revision
        ChangedInSvnRevision = if ($null -ne $changedAway) { [long] $changedAway.revision } else { $null }
        ChangedDateUtc = if ($null -ne $changedAway) { [datetimeoffset]::Parse([string] $changedAway.date).UtcDateTime } else { $null }
        RepositoryUrl = $url
    }
}