Extensions/Git.Status.UGit.Extension.ps1

<#
.SYNOPSIS
    git status extension
.DESCRIPTION
    Returns git status as an object.

    git status provides a lot of useful information.
.EXAMPLE
    # Get the status of the current repository
    git status
.EXAMPLE
    # Get the untracked files
    git status |
        Select-Object -ExpandProperty Untracked
.EXAMPLE
    # Get the status of the repo in the current directory
    git status .
.EXAMPLE
    # Get untracked files in the current directory
    git status . |
        Select-Object -ExpandProperty Untracked
.EXAMPLE
    # See all git status can do
    git status |
        Get-Member
.EXAMPLE
    # We can also git status --short
    git status --short
.EXAMPLE
    # We can also ask for short status with a branch summary
    git status --short --branch
.EXAMPLE
    # We can use the shortform `-sb`
    git status -sb
#>

[Management.Automation.Cmdlet("Out","Git")]
[ValidatePattern('^git status')]
param()

begin {
    <#
    If any of these parameters are used, we will skip processing.
    #>

    $SkipIf = 'porcelain' -join '|'
    if ($gitCommand -match "\s-(?>$SkipIf)") { break }

    $shortStatus = $gitCommand -match '(?>-s|--short)'

    $statusLines = @()

    filter gitOctal {
        [Regex]::Replace(
            # First we trim leading and trailing quotes. Easy.
            $_ -replace '^"|"$',
            # Then we look for any sequence of slash + 3 digits [0-7]
            "(?:\\[0-7]{3}){1,}",
            # And replace them with this short script
            {                        
                param($match)
                # We want them back as UTF8
                [Text.Encoding]::UTF8.GetString(
                    # But these bytes are encoded with
                    # [ISO-8859-1](https://en.wikipedia.org/wiki/ISO/IEC_8859-1)
                    [Text.Encoding]::GetEncoding('ISO-8859-1').GetBytes(
                        # The match contains the sequence
                        @(
                            foreach (
                                # so we split it up
                                # (which removes the slash)
                                $octalByte in $match -split '\\' -ne ''
                            ) {
                                # then we convert each byte in base 8
                                [Convert]::ToByte($octalByte,8) -as
                                    [char] # and make it a character
                            }
                        ) -join '' # then we join our characters,
                    ) # get their bytes,
                ) # and output the replaced string.
            }
        )
    }
}

process {
    # collect all the status lines
    $statusLines += "$gitOut"
}

end {    
    # --short status does not have too much information
    if ($shortStatus) {
        # It's just a series of lines we will turn into objects
        # and put into a summary object
        $ShortStatusObject = [Ordered]@{
            PSTypeName = 'Git.Status.Short'
            Status = ''
            StatusLines = $statusLines
            Short = @()            
            WorkingDirectory = if ("$PWD".StartsWith($GitRoot)) {
                $pwd
            } else {
                $gitRoot
            }
        }        

        foreach ($shortLine in $statusLines) {
            # The first character is any `staged` status
            $stagedStatus = $shortLine.Substring(0,1) 
            # The second character is any `unstaged` status
            $unstagedStatus = $shortline.Substring(1,1)

            $filePath = $shortLine.Substring(3)

            if ($stagedStatus -eq '#' -and $unstagedStatus -eq '#') {                
                $ShortStatusObject.Status = $filePath
                continue
            }

            $filePath = $filePath | gitOctal                        
            
            $fileStatus, $filePath = $shortLine -replace '^\s{0,}' -split '[\s\t]', 2            
            $ShortStatusObject.Short += [PSCustomObject](                 
                [Ordered]@{
                    Staged = $stagedStatus
                    Unstaged = $unstagedStatus
                    Path = $filePath | gitOctal
                }
            )
        }
        return ([PSCustomObject]$ShortStatusObject)
    }

    $gitStatusOut = [Ordered]@{
        PSTypeName = 'Git.Status'
        BranchName = ''
        Status     = ''
        Staged     = @()
        Unstaged   = @()
        Untracked  = @()
        Unmerged   = @()
        GitRoot    = $GitRoot
        WorkingDirectory = if ("$PWD".StartsWith($GitRoot)) {
            $pwd
        } else {
            $gitRoot
        }
    }    

    $inPhase     = ''

    for ($sln = 0; $sln -lt $statusLines.Length; $sln++) {
        if ($sln -eq 0) {
            $gitStatusOut.BranchName = @($statusLines[$sln] -split ' ' -ne '')[-1]
            continue
        }
        if ($statusLines[$sln] -like '*not staged for commit:*') {
            # When on a new branch with no upstream, this comes first.
            $inPhase = 'Unstaged'
            continue
        }
        if ($statusLines[$sln] -like "Changes to be committed:*") {
            $inPhase = 'Staged'
        }
        if ($statusLines[$sln] -like "Unmerged*:*") {
            $inPhase = 'Unmerged'
            continue
        }
        if ($statusLines[$sln] -like "Untracked files:*") {
            $inPhase = 'Untracked'
            continue
        }

        if ($sln -eq 1 -and -not $inPhase) {
            $gitStatusOut.Status = $statusLines[$sln]
            continue
        }

        if ($statusLines[$sln] -match '^\s+\(') { continue }
        if ($statusLines[$sln] -match '^\s+' -and $inPhase) {
            $trimmedLine = $statusLines[$sln].Trim()
            $changeType =
                if ( $trimmedLine -match "^([\w\s]+):") {
                    $matches.1
                } else {
                    ''
                }
            $changePath = $trimmedLine -replace "^[\w\s]+:\s+"

            # If git quotes a status line,
            # it means there are octal encoded characters in the path
            if ($changePath -match '^"' -and $changePath -match '"$') {
                # So we want to change our change path.
                $changePath = $changePath | gitOctal
            }
            $resolvedChangePath =
                try {
                    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($changePath)
                    $resolvedFile = [IO.FileInfo]"$resolvedPath"
                    if ($resolvedFile.Length) {
                        $resolvedFile
                    } elseif (
                        $resolvedDirectory = [IO.DirectoryInfo]"$resolvedPath"
                    ) {
                        $resolvedDirectory
                    }                   
                } catch {
                    Write-Verbose "Could not resolve path '$changePath' : $_"
                }
            if ($inPhase -eq 'untracked') {                
                $gitStatusOut.$inPhase += $resolvedChangePath
            } else {
                $gitStatusOut.$inPhase += [PSCustomObject]@{
                    ChangeType = $changeType -replace '\s'
                    Path       = $changePath
                    File       = $resolvedChangePath
                }
            }

        }
    }

    $gitStatusOut.StatusLines = $statusLines
    if ($gitStatusOut.BranchName) {
        [PSCustomObject]$gitStatusOut
    } else {
        $statusLines
    }
}