Extensions/Git.Stash.UGit.Extension.ps1

<#
.SYNOPSIS
    git stash extension
.DESCRIPTION
    Manages git code stashes. Returns objects wherever possible.
.EXAMPLE
    git stash list
#>

[Management.Automation.Cmdlet("Out","Git")]           # It's an extension for Out-Git
[ValidatePattern("^git stash",Options='IgnoreCase')] # when the pattern is "git branch"
param()

begin {
    # Prepare to collect of the output lines
    $stashLines = @()
}

process {
    # Accumulate each output line when it comes it.
    $stashLines += $_
}

end {
    # If we don't know how to handle output, we want to output normally.
    # So keep track if we know.
    $stashOutputHandled = $false
    
    # If the command is stash show --patch,
    if ($GitCommand -match 'stash show(?:\s\d+)? -(?>p|-patch)') {
        # the diff extension will actually be handling things.
        return
    }
    
    # Create a base output object
    $gitStashOut     = [Ordered]@{
        GitRoot = $GitRoot
        GitCommand = $GitCommand
        GitOutputLines = $stashLines
    }

    if ($GitCommand -match '^git stash (?>apply|pop)') {
        $gitStashOut += [Ordered]@{
            Unstaged = @()
            Untracked = @()
            Staged = @()
        }
    }

    # When popping or applying a stash,
    # we will need to know which line we're on
    $stashLineNumber = 0
    # and which phase we are in.
    $inPhase     = ''

    # Walk over all stash lines.
    foreach ($stashLine in $stashLines) {
        # Increment the counter
        $stashLineNumber++
        # If the line is "No local changes to save"
        if ($stashLine -eq 'No local changes to save') {
            # return a 'git.stash.nothing' object.
            return [PSCustomObject]([Ordered]@{
                PSTypeName = 'git.stash.nothing'                
            } + $gitStashOut)
        }

        # If the command was git stash list,
        if ($GitCommand -match '^git stash list') {
            # match lines that describe a stash,
            if ($stashLine -match '^stash\@\{(?<Number>\d+)\}:\s{0,}(?<Message>.+$)') {
                # indicate output was handled,
                $stashOutputHandled = $true
                # and output a 'git.stash.entry' for that line.
                [PSCustomObject]([Ordered]@{
                    PSTypeName = 'git.stash.entry'
                    Number  = [int]$matches.Number
                    Message = $matches.Message
                } + $gitStashOut)
            }
        }

        # If the command was git stash (and it saved),
        if ($GitCommand -eq 'git stash' -and
            $stashLine -match 'Saved working directory and index state\s{0,}(?<Message>.+$)') {
            # mark output as handled
            $stashOutputHandled = $true
            # and return a stash entry object.
            [PSCustomObject]([Ordered]@{
                PSTypeName = 'git.stash.entry'
                Number  = [int]0
                Message = $matches.Message
            } + $gitStashOut)                                     
        }

        # If we are popping or applying a stash
        if ($GitCommand -match '^git stash (?>pop|apply)') {
            # The first line contains the branch name
            if ($stashLineNumber -eq 1) {
                # (in the last word).
                $gitStashOut.BranchName = @($stashLine -split ' ' -ne '')[-1] 
            }
            # Everything else returns like git status.
            # use certain lines to indicate we are changing phases
            if ($stashLine -like '*not staged for commit:*') {
                # When on a new branch with no upstream, this comes first.
                $inPhase = 'Unstaged'
                continue
            }
            if ($stashLine -like "Changes to be committed:*") {
                
                $inPhase = 'Staged'
                continue
            }
            if ($stashLine -like "Untracked files:*") {
                $inPhase = 'Untracked'
                continue
            }

            # The second line should contain the status
            if ($stashLineNumber -eq 2 -and -not $inPhase) {
                $gitStashOut.Status = $stashLine
                continue
            }

            # We can ignore lines that begin with whitespace and parenthesis
            if ($stashLine -match '^\s+\(') { continue }
            # Lines that start with whitespace tell us what files were applied
            if ($stashLine -match '^\s+' -and $inPhase) {
                $trimmedLine = $stashLine.Trim()
                $changeType = 
                    # The changetype will be in parenthesis
                    if ( $trimmedLine -match "^([\w\s]+):") {
                        $matches.1
                    } else {
                        ''
                    }
                # The path of the change will be the content after the colon
                $changePath = $trimmedLine -replace "^[\w\s]+:\s+"
                # store the file in the appropriate collection on the object.
                if ($inPhase -eq 'untracked') {
                    $gitStashOut.$inPhase += Get-Item -ErrorAction SilentlyContinue -Path $changePath
                } else {
                    $gitStashOut.$inPhase += [PSCustomObject]@{
                        ChangeType = $changeType -replace '\s'
                        Path       = $changePath
                        File       = Get-Item -ErrorAction SilentlyContinue -Path $changePath
                    }
                }                
            }
        }

        # If the stash line indicates a dropped ref
        if ($stashLine -match '^Dropped refs/stash\@\{(?<Number>\d+)\}\s\((?<CommitHash>[a-f0-9]+)\)') {
            # Create the drop info
            $dropInfo = [Ordered]@{
                Number = [int]$matches.Number
                CommitHash = $matches.CommitHash
            }
            
            # If the command was 'git stash drop'
            if ($GitCommand -match '^git stash drop') {
                # return the drop info
                return [PSCustomObject]([Ordered]@{
                    PSTypeName = 'git.stash.drop'
                } + $dropInfo + $gitStashOut)
            } else {
                # Otherwise, add this to the gitStashOutput.
                $gitStashOut.Dropped = [PSCustomObject]([Ordered]@{
                    PSTypeName = 'git.stash.drop'                    
                } + $dropInfo)
            }
        }
    }

    # If the command was git stash apply or pop
    if ($GitCommand -match '^git stash (?>apply|pop)') {
        # indicate output was handled
        $stashOutputHandled = $true
        # and return a 'git.stash.apply' object.
        [PSCustomObject]([Ordered]@{
            PSTypeName = "git.stash.apply"
        } + $gitStashOut)
    }

    # If we still had nothing handling output
    if (-not $stashOutputHandled) {
        $stashLines # output the stash lines as-is.
    }
}