Functions/Split-HgXml.ps1


function Split-HgXml
{
    <#
    .SYNOPSIS
    Converts Mercurial's XML style output to PowerShell objects.
     
    .DESCRIPTION
    Mercurial's log-like commands all output to standard out. This doesn't work well with PowerShell's pipeline.
     
    This command takes the output from Mercurial's xml style and converts that output into PowerShell objects and sends those objects down the pipeline. For best results, use the xml style with the --debug parameter. The gets all information about a changeset instead of Mercurial's limited, default set.
     
    The objects returned represent individual changsets and have the following properties:
     
        Revision: The numeric identifier.
        Node: The node identifier.
        Parent1: The first parent, with properties Revision and Node. Null if the changeset had no parents.
        Parent2: The second parent, with properties Revision and Node. Null if the changeset wasn't a merge.
        Date: The date.
        Description: The commit message.
        Author: The user who made the change, which is an object with Name and Email properties.
        Branch: The branch.
        Tags: An array of the changeset's tags.
        Files: An array of all the files that were part of the changeset.
        FileAdds: An array of all files added by the changeset.
        FileDeletes: An array of all the files deleted by the changeset.
        FileModifications: An array of all files modified by the changeset.
        Copies: An array of objects representing renames in the changeset. Each object has a Source and Destination property.
     
    ALIASES
      slhgx
       
    .EXAMPLE
    hg log --style xml --debug | Split-HgXml
     
    Returns generic objects representing all the changesets in the current repository, e.g.
     
    Author : PsHg Test User <pshg@example.com>
    Revision : 2
    Node : 91069564c902f97622a94305d9b5ba969590148a
    Parent1 : @{Revision=1; Node=b15b8cc69a8bc9c165d48509554f4d616b746a75}
    Parent2 :
    Date : 4/26/2012 8:23:32 AM
    Copies : {a.txt -> a2.txt}, {b.txt -> b2.txt}}
    Files : {a2.txt, b2.txt, a.txt, b.txt}
    FileAdds : {a2.txt, b2.txt}
    FileDeletes : {a.txt, b.txt}
    FileModifications : {}
    Description : Renaming files.
    Branch : default
    Tags : {tip}
     
    .EXAMPLE
    > $xml = ((hg log --style xml --debug) -join "")
    > Split-HgXml -InputObject $xml
     
    Converts a string of Mercurial's XML style output to objects.
    #>

    [CmdletBinding()]
    [OutputType([PsHg.ChangesetInfo])]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [AllowEmptyString()]
        [string]
        # The XML to parse. Can come from the pipeline or an explicit string.
        $InputObject
    )
    
    begin
    {
        Set-StrictMode -Version 'Latest'

        $entry = $null
        $xml = New-Object Text.StringBuilder
        $xmlOutputStarted = $false
        $xmlReady = $false
    }
    
    process
    {
        if( $pscmdlet.MyInvocation.ExpectingInput )
        {
            if( $_ -eq '<log>' )
            {
                $xmlOutputStarted = $true
                return
            }
            elseif( $_ -eq '</log>' )
            {
                $xmlOutputStarted = $false
                return
            }
            
            if( -not $xmlOutputStarted )
            {
                return
            }
            
            if( $_ -like '<logentry *' )
            {
                $xml = New-Object Text.StringBuilder "<?xml version=""1.0""?>`n<log>`n"
            }
            
            [void] $xml.Append($_)
            
            if( $_ -like '</logentry>' )
            {
                [void] $xml.Append('</log>')
                $xmlReady = $true
            }
        }
        else
        {
            $xml = New-Object Text.StringBuilder $InputObject
            $xmlReady = $true
        }
        
        if( -not $xmlReady )
        {
            return
        }
        
        $xmlDoc = [xml] ($Xml.ToString())
        
        filter Get-Parent
        {
            param(
                $ParentXml
            )

            if( -not $parentXml -or $parentXml.revision -eq -1 )
            {
                return $null
            }

            New-Object 'PsHg.ChangesetID' $parentXml.revision,$parentXml.node
        }
            
        $xmlDoc.log.logentry | ForEach-Object {

            $id = New-Object 'PsHg.ChangesetID' $_.revision,$_.node

            $parent1 = $null
            $parent2 = $null
            if( $_ | Get-Member 'parent' )
            {
                if( $_.parent -is 'Array' )
                {
                    $parent1 = Get-Parent $_.parent[0]
                    $parent2 = Get-Parent $_.parent[1]
                }
                else
                {
                    $parent1 = Get-Parent $_.parent
                }
            }

            $date = $_.date
            $description = $_.msg.InnerText
            $author = New-Object 'PsHg.AuthorInfo'  $_.author.InnerText,$_.author.email

            $branch = 'default'
            if( $_ | Get-Member 'branch' )
            {
                $branch = $_.branch
            }

            $phase = $null
            if( $_ | Get-Member 'phase' )
            {
                $phase = New-Object 'PsHg.PhaseInfo' $_.phase.InnerText,$_.phase.index
            }

            $diffstats = $null
            if( ($_ | Get-Member 'diffstat') -and ($_.diffstat -match '^(\d+): \+(\d+)/-(\d+)$') )
            {
                $diffstats = New-Object 'PsHg.DiffInfo' $Matches[1],$Matches[2],$Matches[3]
            }

            $entry = New-Object 'PsHg.ChangesetInfo' $id,$branch,$parent1,$parent2,$author,$date,$description,$phase,$diffstats

            if( $_ | Get-Member 'copies' )
            {
                $_.copies.copy | 
                    Where-Object { $_ } |
                    ForEach-Object { 
                        $copy = New-Object 'PsHg.CopyInfo'  $_.source,$_.InnerText
                        [void]$entry.Copies.Add( $copy )
                    }
            }

            $files = @{ }
            $usingPshgStyle = $false
            if( ($_ | Get-Member 'files') -and ($_.files | Get-Member 'file') )
            {
                $usingPshgStyle = $true
                foreach( $file in $_.files.file )
                {
                    $files[$file] = $file
                    [void]$entry.Files.Add( $file )
                }
            }

            if( ($_ | Get-Member 'paths') -and ($_.paths | Get-Member 'path') )
            {
                $_.paths.path | 
                    Where-Object { -not $usingPshgStyle -or $files.ContainsKey( $_.InnerText ) } |
                    ForEach-Object { 
                        $path = $_.InnerText
                        if( -not $usingPshgStyle )
                        {
                            [void]$entry.Files.Add( $path )
                        }
                        switch( $_.action )
                        {
                            'M' { [void]$entry.FileModifications.Add( $path ) }
                            'A' { [void]$entry.FileAdds.Add( $path ) }
                            'R' { [void]$entry.FileDeletes.Add( $path ) }
                        }
                    }
            }
                        
            if( $_ | Get-Member 'tag' )
            {    
                $_.tag | 
                    ForEach-Object { [void]$entry.Tags.Add( $_ ) }
            }

            if( $_ | Get-Member 'extra' )
            {
                $_.extra |
                    ForEach-Object { [void]$entry.Extras.Add( $_.key, $_.InnerText ) }
            }

            if( $_ | Get-Member 'bookmark' )
            {
                $_.bookmark | 
                    ForEach-Object { [void]$entry.Bookmarks.Add( $_ ) }
            }

            $entry
        }        
        $entry = $null
        $xml = $null
        $xmlReady = $false
    }
    
    end
    {
        if( $entry )
        {
            $entry
        }
    }

}

Set-Alias -Name 'slhgx' -Value 'Split-HgXml'