Extensions/Git.Diff.UGit.Extension.ps1

<#
.Synopsis
    Diff Extension
.Description
    Outputs git diff entries as objects
#>

[Management.Automation.Cmdlet("Out","Git")]         # It extends Out-Git
[ValidatePattern("^git (?>diff|stash show(?:\s\d+)? -(?>p|-patch))",Options='IgnoreCase')] # when the pattern is "git diff"
[OutputType('Git.Diff','Git.Diff.ChangeSet')]
param()

begin {
    # Diff messages are spread across many lines, so we need to keep track of them.
    $lines = [Collections.Queue]::new()
    $allDiffLines = [Collections.Queue]::new()    

    function OutDiff {
        param([string[]]$OutputLines)


        if (-not $OutputLines) { return }
        $outputLineCount = 0
        $diffRange  = $null

        $diffObject = [Ordered]@{
            PSTypeName='git.diff'
            ChangeSet=@()
            GitOutputLines = $OutputLines
            Binary=$false
            GitRoot = $gitRoot
        }

        foreach ($outputLine in $OutputLines) {
            $outputLineCount++
            if ($outputLineCount -eq 1) {
                $diffObject.From, $diffObject.To = $outputLine -replace '^diff --git' -split '\s[ab]/' -ne ''
                $fromPath = if ($diffObject.From) { Join-Path $gitRoot $diffObject.From }
                $toPath   = if ($diffObject.To) { Join-Path $gitRoot $diffObject.To } 
                if ($toPath -and (Test-Path $toPath)) {
                    $diffObject.File = Get-Item $toPath
                } elseif ($fromPath -and (Test-Path $fromPath)) {
                    $diffObject.File = Get-Item $fromPath
                }
            }
            if (-not $diffRange -and $outputline -match 'index\s(?<fromhash>[0-9a-f]+)..(?<tohash>[0-9a-f]+)') {
                $diffObject.FromHash, $diffObject.ToHash = $Matches.fromhash, $Matches.tohash
            }
            if ($outputLine -like 'Binary files *differ') {
                $diffObject.Binary = $true
            }
            if ($outputLine -like "@@*@@*") {
                if ($diffRange) {
                    $diffObject.ChangeSet += [PSCustomObject]$diffRange
                }

                $extendedHeader = $outputLine -replace '^@@[^@]+@@' -replace '^\s+'
                $diffRange = [Ordered]@{
                    PSTypeName='git.diff.range';
                    Changes=@(if ($extendedHeader) {$extendedHeader});
                    Added=@();
                    Removed=@()
                }
                $diffRange.LineStart,
                $diffRange.LineCount,
                $diffRange.NewLineStart,
                $diffRange.NewLineCount =
                    @($outputLine -replace '\s' -split '[-@+]' -ne '' -split ',')[0..3] -as [int[]]
                continue
            }

            if ($diffRange) {
                $diffRange.Changes += $outputLine
                if ($outputLine.StartsWith('+')) {
                    $diffRange.Added += $outputLine -replace '^+'
                }
                elseif ($outputLine.StartsWith('-')) {
                    $diffRange.Removed += $outputLine -replace '^+'
                }
            }

            if ($outputLineCount -eq $OutputLines.Length) {
                $diffObject.ChangeSet += [PSCustomObject]$diffRange
            }
        }
        [PSCustomObject]$diffObject
    }
}


process {
    if ($gitCommand -match '--name-only') { return  }
    if ("$gitOut" -like 'diff*' -and $lines) {
        OutDiff $lines.ToArray()
        $lines.Clear()
    }
    $lines.Enqueue($gitOut)
    $allDiffLines.Enqueue($gitOut)
}

end {
    if ($gitCommand -match '--name-only') { return  }
    OutDiff $lines.ToArray()
}