PSFzf.Git.ps1


$script:GitKeyHandlers = @()

$script:foundGit = $false
$script:bashPath = $null
$script:grepPath = $null

if ($PSVersionTable.PSEdition -eq 'Core') {
    $script:pwshExec = "pwsh"
} else {
    $script:pwshExec = "powershell"
}

function Get-GitFzfArguments() {
    # take from https://github.com/junegunn/fzf-git.sh/blob/f72ebd823152fa1e9b000b96b71dd28717bc0293/fzf-git.sh#L89
    return @{
        Ansi = $true
        Layout = "reverse"
        Multi = $true
        Height = '50%'
        MinHeight = 20
        Border = $true
        Color = 'header:italic:underline'
        PreviewWindow = 'right,50%,border-left'
        Bind = @('ctrl-/:change-preview-window(down,50%,border-top|hidden|)')
    }
}

function SetupGitPaths() {
    if (-not $script:foundGit) {
        if ($IsLinux -or $IsMacOS) {
            # TODO: not tested on Mac
            $script:foundGit = $null -ne $(Get-Command git -ErrorAction SilentlyContinue)
            $script:bashPath = 'bash'
            $script:grepPath = 'grep'
        } else {
            $gitInfo = Get-Command git.exe -ErrorAction SilentlyContinue
            $script:foundGit = $null -ne $gitInfo
            if ($script:foundGit) {
                $gitPathLong = Split-Path (Split-Path $gitInfo.Source -Parent) -Parent
                # hack to get short path:
                $a = New-Object -ComObject Scripting.FileSystemObject
                $f = $a.GetFolder($gitPathLong)
                $script:bashPath = Join-Path $f.ShortPath "bin\bash.exe"
                $script:bashPath = Resolve-Path $script:bashPath
                $script:grepPath = Join-Path ${gitPathLong} "usr\bin\grep.exe"
            }
        }
    }
    return $script:foundGit
}
function SetGitKeyBindings($enable) {
    if ($enable) {
        if (-not $(SetupGitPaths)) {
            Write-Error "Failed to register git key bindings - git executable not found"
            return
        }

        if (Get-Command Set-PSReadLineKeyHandler -ErrorAction SilentlyContinue) {
            @('ctrl+g,ctrl+f', 'Select Git files via fzf', { Invoke-PsFzfGitFiles }), `
            @('ctrl+g,ctrl+h', 'Select Git hashes via fzf', { Invoke-PsFzfGitHashes }), `
            @('ctrl+g,ctrl+b', 'Select Git branches via fzf', { Invoke-PsFzfGitBranches }), `
            @('ctrl+g,ctrl+t', 'Select Git tags via fzf', { Invoke-PsFzfGitTags }), `
            @('ctrl+g,ctrl+s', 'Select Git tags via fzf', { Invoke-PsFzfGitStashes }) `
            | ForEach-Object {
                $script:GitKeyHandlers += $_[0]
                Set-PSReadLineKeyHandler -Chord $_[0] -Description $_[1] -ScriptBlock $_[2]
            }
        }
        else {
            Write-Error "Failed to register git key bindings - PSReadLine module not loaded"
            return
        }
    }
}

function RemoveGitKeyBindings() {
    $script:GitKeyHandlers | ForEach-Object {
        Remove-PSReadLineKeyHandler -Chord $_
    }
}

function IsInGitRepo() {
    git rev-parse HEAD 2>&1 | Out-Null
    return $?
}

function Get-ColorAlways($setting=' --color=always') {
    if ($RunningInWindowsTerminal -or -not $IsWindowsCheck) {
        return $setting
    }
    else {
        return ''
    }
}

function Get-HeaderStrings() {
    $header = "CTRL-A (Select all) / CTRL-D (Deselect all) / CTRL-T (Toggle all)"
    $keyBinds = 'ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all'
    return $Header, $keyBinds
}
function Invoke-PsFzfGitFiles() {
    if (-not (IsInGitRepo)) {
        return
    }

    if (-not $(SetupGitPaths)) {
        Write-Error "git executable could not be found"
        return
    }

    $previewCmd = "${script:bashPath} \""" + $(Join-Path $PsScriptRoot 'helpers/PsFzfGitFiles-Preview.sh') + "\"" {-1}" + $(Get-ColorAlways) + " \""$($pwd.ProviderPath)\"""
    $result = @()

    $headerStrings = Get-HeaderStrings
    $gitCmdsHeader = "`nALT-S (Git add) / ALT-R (Git reset)"
    $headerStr = $headerStrings[0] + $gitCmdsHeader + "`n`n"
    $statusCmd = "git $(Get-ColorAlways '-c color.status=always') status --short"

    $reloadBindCmd = "reload($statusCmd)"
    $stageScriptPath = Join-Path $PsScriptRoot 'helpers/PsFzfGitFiles-GitAdd.sh'
    $gitStageBind = "alt-s:execute-silent(" + "${script:bashPath} ${stageScriptPath} {+2..})+down+${reloadBindCmd}"
    $resetScriptPath = Join-Path $PsScriptRoot 'helpers/PsFzfGitFiles-GitReset.sh'
    $gitResetBind = "alt-r:execute-silent(" + "${script:bashPath} ${resetScriptPath} {+2..})+down+${reloadBindCmd}"

    $fzfArguments = Get-GitFzfArguments
    $fzfArguments['Bind'] += $headerStrings[1],$gitStageBind,$gitResetBind
    Invoke-Expression "& $statusCmd" | `
        Invoke-Fzf @fzfArguments `
        -Prompt '📁 Files> ' `
        -Preview "$previewCmd" -Header $headerStr | `
        foreach-object {
            $result += $_.Substring('?? '.Length)
        }
    InvokePromptHack
    if ($result.Length -gt 0) {
        $result = $result -join " "
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($result)
    }
}
function Invoke-PsFzfGitHashes() {
    if (-not (IsInGitRepo)) {
        return
    }

    if (-not $(SetupGitPaths)) {
        Write-Error "git executable could not be found"
        return
    }

    $previewCmd = "${script:bashPath} \""" + $(Join-Path $PsScriptRoot 'helpers/PsFzfGitHashes-Preview.sh') + "\"" {}" + $(Get-ColorAlways) + " \""$pwd\"""
    $result = @()

    $fzfArguments = Get-GitFzfArguments
    & git log --date=short --format="%C(green)%C(bold)%cd %C(auto)%h%d %s (%an)" $(Get-ColorAlways).Trim() --graph | `
        Invoke-Fzf @fzfArguments -NoSort  `
        -Prompt '🍡 Hashes> ' `
        -Preview "$previewCmd" | ForEach-Object {
        if ($_ -match '\d\d-\d\d-\d\d\s+([a-f0-9]+)\s+') {
            $result += $Matches.1
        }
    }

    InvokePromptHack
    if ($result.Length -gt 0) {
        $result = $result -join " "
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($result)
    }
}

function Invoke-PsFzfGitBranches() {
    if (-not (IsInGitRepo)) {
        return
    }

    if (-not $(SetupGitPaths)) {
        Write-Error "git executable could not be found"
        return
    }

    $fzfArguments = Get-GitFzfArguments
    $fzfArguments['PreviewWindow'] = 'down,border-top,40%'
    $fzfArguments['Bind'] += 'ctrl-/:change-preview-window(down,70%|hidden|)'
    $previewCmd = "${script:bashPath} \""" + $(Join-Path $PsScriptRoot 'helpers/PsFzfGitBranches-Preview.sh') + "\"" {}"
    $result = @()
    # use pwsh to prevent bash from trying to write to host output:
    $branches = & $script:pwshExec -NoProfile -NonInteractive -Command "& ${script:bashPath} $(Join-Path $PsScriptRoot 'helpers/PsFzfGitBranches.sh') branches"
    $branches |
        Invoke-Fzf @fzfArguments -Preview "$previewCmd" -Prompt '🌲 Branches> ' -HeaderLines 2 -Tiebreak begin -ReverseInput | `
        ForEach-Object {
            $result += $($_.Substring('* '.Length) -split ' ')[0]
        }

    InvokePromptHack
    if ($result.Length -gt 0) {
        $result = $result -join " "
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($result)
    }
}

function Invoke-PsFzfGitTags() {
    if (-not (IsInGitRepo)) {
        return
    }

    if (-not $(SetupGitPaths)) {
        Write-Error "git executable could not be found"
        return
    }

    $fzfArguments = Get-GitFzfArguments
    $fzfArguments['PreviewWindow'] = 'right,70%'
    $previewCmd = "git show --color=always {}"
    $result = @()
    git tag --sort -version:refname |
        Invoke-Fzf @fzfArguments -Preview "$previewCmd" -Prompt '📛 Tags> ' | `
        ForEach-Object {
            $result += $_
        }

    InvokePromptHack
    if ($result.Length -gt 0) {
        $result = $result -join " "
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($result)
    }
}

function Invoke-PsFzfGitStashes() {
    if (-not (IsInGitRepo)) {
        return
    }

    if (-not $(SetupGitPaths)) {
        Write-Error "git executable could not be found"
        return
    }

    $fzfArguments = Get-GitFzfArguments
    $fzfArguments['Bind'] += 'ctrl-x:execute-silent(git stash drop {1})+reload(git stash list)'
    $header = "CTRL-X (drop stash)`n`n"
    $previewCmd = 'git show --color=always {1}'

    $result = @()
    git stash list --color=always |
        Invoke-Fzf @fzfArguments -Header $header -Delimiter ':' -Preview "$previewCmd" -Prompt '🥡 Stashes> ' | `
        ForEach-Object {
            $result += $_.Split(':')[0]
        }

    InvokePromptHack
    if ($result.Length -gt 0) {
        $result = $result -join " "
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert("""$result""")
    }
}