PSGit.psm1

# Internal Functions
#region Interal Functions
function WriteMessage {
    [CmdletBinding()]
    param (
        # The Type of message you'd like, its just a prefix to the message.
        [Parameter(Mandatory=$true,Position=0)]
        [string]
        $Type,

        # The message to display
        [Parameter(Mandatory=$true,Position=1)]
        [string]
        $Message,

        # Color of the message, it will default to the hosts' verbose color.
        [Parameter(Position=2)]
        [ConsoleColor]
        $ForegroundColor = ($host.PrivateData.VerboseForegroundColor|ConvertColor),
        # Background color of the message, it will default to the hosts' verbose color.
        [Parameter(Position=3)]
        [ConsoleColor]
        $BackgroundColor = ($host.PrivateData.VerboseBackgroundColor|ConvertColor -default "black")
    )

    #todo check to see if the preference is on or off

    Write-Host "$($Type.ToUpper()): $Message" -ForegroundColor $ForegroundColor -BackgroundColor $BackgroundColor
}

function ConvertColor {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline=$true)]
        $color,
        $default="yellow"
    )
    if(!$color) {
        [consolecolor]$default
    }
    elseif(($color -as [ConsoleColor]) -ne $null) {
        [consolecolor]$color
    }
    else {
        if("system.Windows.Media.Color" -as [type]) {
            #if its a transparent color, just use the background color of the host
            if($color -eq "#00FFFFFF") {$color = $host.PrivateData.ConsolePaneBackgroundColor}
            if($color -is [string]){ $color = [System.Windows.Media.Color]$color }
            [int]$bright = if($color.R -gt 128 -bor $color.G -gt 128 -bor $color.B -gt 128){8}else{0}
            [int]$r = if($color.R -gt 64){4}else{0}
            [int]$g = if($color.G -gt 64){2}else{0}
            [int]$b = if($color.B -gt 64){1}else{0}
            [consolecolor]($bright -bor $r -bor $g -bor $b)
        }
        else {
            throw "Unable to process hosts default colors $color"
        }
    }
}
#endregion

function Get-RootFolder {
    #.Synopsis
    # Search up the directory tree recursively for a git root (and corresponding .git folder)
    [CmdletBinding(DefaultParameterSetName="IndexAndWorkDir")]
    param (
        # Where to start searching
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String]$Root = $Pwd
    )
    end {
        # Git Repositories are File System Based, and don't care aabout PSDrives
        [LibGit2Sharp.Repository]::Discover((Convert-Path $Root))
    }
}

# TODO: DOCUMENT ME
function Get-Change {
    [CmdletBinding(DefaultParameterSetName="IndexAndWorkDir")]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String]$Root = $Pwd,

        [Parameter(Position = 0)]
        [String[]]$PathSpec,

        [Parameter(ParameterSetName="WorkDirOnly", Mandatory=$true)]
        [Switch]$UnStagedOnly,

        [Parameter(ParameterSetName="IndexOnly", Mandatory=$true)]
        [Switch]$StagedOnly,

        [Parameter()]
        [switch]
        $HideUntracked,

        [Parameter()]
        [switch]
        $HideSubmodules,

        [Parameter()]
        [switch]
        $ShowIgnored
    )
    end {
        $Path = [LibGit2Sharp.Repository]::Discover((Convert-Path $Root))
        if(!$Path) {
            Write-Warning "The path is not in a git repository!"
            return
        }

        $PathSpec = $PathSpec | Where { "$_".Trim().Length -gt 0 }

        try {
            $repo = New-Object LibGit2Sharp.Repository $Path
            $Path = $repo.Info.WorkingDirectory

            $Options = New-Object LibGit2Sharp.StatusOptions
            $Options.Show = $PSCmdlet.ParameterSetName
            # Don't touch PathSpec unless you're serious, it breaks the output
            if($PathSpec) { $Options.PathSpec = $PathSpec }
            $Options.DetectRenamesInWorkDir = $true
            if($HideSubmodules)
            {
                $Options.ExcludeSubmodules = $true
            }

            $status = $repo.RetrieveStatus($Options)
        } finally {
            $repo.Dispose()
        }

        # Unaltered, Added, Staged, Removed, RenamedInIndex, StagedTypeChange,
        # Untracked, Modified, Missing, TypeChanged, RenamedInWorkDir,
        # Unreadable, Ignored, Nonexistent

        # Output staged changes, if any
        foreach($file in $status.Added) {
            New-Object PSCustomObject -Property @{
                PSTypeName = "PSGit.FileStatus"
                Staged = $true
                Change = "Added"
                Path = $file.FilePath + $(if(Test-Path (Join-Path $Path $File.FilePath) -Type Container){ "\" })
            }
        }
        foreach($file in $status.RenamedInIndex) {
            New-Object PSCustomObject -Property @{
                PSTypeName = "PSGit.FileStatus"
                Staged = $true
                Change = "Renamed"
                Path = $file.FilePath + $(if(Test-Path (Join-Path $Path $File.FilePath) -Type Container){ "\" })
                OldPath = $File.HeadToIndexRenameDetails.OldFilePath + $(if(Test-Path (Join-Path $Path $File.HeadToIndexRenameDetails.OldFilePath) -Type Container){ "\" })
            }
        }
        foreach($file in $status.Removed) {
            New-Object PSCustomObject -Property @{
                PSTypeName = "PSGit.FileStatus"
                Staged = $true
                Change = "Removed"
                Path = $file.FilePath + $(if(Test-Path (Join-Path $Path $File.FilePath) -Type Container){ "\" })
            }
        }
        foreach($file in $status.Staged) {
            #BUGBUG: hides rename + edit, but avoids double-outputs (and behaves like git)
            if(($file.State -band [LibGit2Sharp.FileStatus]::RenamedInIndex) -eq 0) {
                New-Object PSCustomObject -Property @{
                    PSTypeName = "PSGit.FileStatus"
                    Staged = $true
                    Change = "Modified"
                    Path = $file.FilePath + $(if(Test-Path (Join-Path $Path $File.FilePath) -Type Container){ "\" })
                }
            }
        }
        # Output unstaged changes, if any
        foreach($file in $status.RenamedInWorkDir) {
            New-Object PSCustomObject -Property @{
                PSTypeName = "PSGit.FileStatus"
                Staged = $false
                Change = "Renamed"
                Path = $file.FilePath + $(if(Test-Path (Join-Path $Path $File.FilePath) -Type Container){ "\" })
                OldPath = $File.IndexToWorkDirRenameDetails.OldFilePath + $(if(Test-Path (Join-Path $Path $File.IndexToWorkDirRenameDetails.OldFilePath) -Type Container){ "\" })
            }
        }
        foreach($file in $status.Modified) {
            #BUGBUG: hides rename + edit, but avoids double-outputs (and behaves like git)
            if(($file.State -band [LibGit2Sharp.FileStatus]::RenamedInWorkDir) -eq 0) {
                New-Object PSCustomObject -Property @{
                    PSTypeName = "PSGit.FileStatus"
                    Staged = $false
                    Change = "Modified"
                    Path = $file.FilePath + $(if(Test-Path (Join-Path $Path $File.FilePath) -Type Container){ "\" })
                }
            }
        }
        foreach($file in $status.Missing) {
            New-Object PSCustomObject -Property @{
                PSTypeName = "PSGit.FileStatus"
                Staged = $false
                Change = "Removed"
                Path = $file.FilePath + $(if(Test-Path (Join-Path $Path $File.FilePath) -Type Container){ "\" })
            }
        }
        # Optional output
        if(!$HideUntracked) {
            foreach($file in $status.Untracked) {
                New-Object PSCustomObject -Property @{
                    PSTypeName = "PSGit.FileStatus"
                    Staged = $false
                    Change = "Added"
                    Path = $file.FilePath + $(if(Test-Path (Join-Path $Path $File.FilePath) -Type Container){ "\" })
                }
            }
        }
        if($ShowIgnored) {
            foreach($file in $status.Ignored) {
                New-Object PSCustomObject -Property @{
                    PSTypeName = "PSGit.FileStatus"
                    Staged = $false
                    Change = "Ignored"
                    Path = $file.FilePath + $(if(Test-Path (Join-Path $Path $File.FilePath) -Type Container){ "\" })
                }
            }
        }
    }
}

$BranchProperties =
    @{ Name="Branch"; Expr={$_.Name}},
    @{ Name="IsHead"; Expr={ $_.IsCurrentRepositoryHead}}, "IsRemote", "IsTracking",
    @{ Name="Tip"; Expr={$_.Tip.Sha}},
    @{ Name="Remote"; expr = { $_.Remote.Url } },
    @{ Name="Ahead"; Expr= { $_.TrackingDetails.AheadBy }},
    @{ Name="Behind"; Expr = { $_.TrackingDetails.BehindBy }},
    @{ Name="CommonAncestor"; Expr={ $_.TrackingDetails.CommonAncestor.Sha }},
    @{ Name="GitDir"; Expr= {$_.Repository.Info.WorkingDirectory}}

# TODO: DOCUMENT ME
function Get-Info {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String]$Root = $Pwd
    )
    end {
        $Path = [LibGit2Sharp.Repository]::Discover((Convert-Path $Root))
        if(!$Path) {
            Write-Warning "The path is not in a git repository!"
            return
        }

        try {
            $repo = New-Object LibGit2Sharp.Repository $Path

            # We have to transform the object to keep the data around after .Dispose()
            $repo.Head |
                Select-Object $BranchProperties |
                ForEach-Object { $_.PSTypeNames.Insert(0,"PSGit.Branch"); $_ }
        } finally {
            if($null -ne $repo) {
                $repo.Dispose()
            }
        }
    }
}

# TODO: DOCUMENT ME
function Get-Branch {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String]$Root = $Pwd,

        [Switch]$Force
    )
    end {
        $Path = [LibGit2Sharp.Repository]::Discover((Convert-Path $Root))
        if(!$Path) {
            Write-Warning "The path is not in a git repository!"
            return
        }

         try {
            $repo = New-Object LibGit2Sharp.Repository $Path
            $(
                # In the initialized state, there are no "Branches"
                if([Linq.Enumerable]::Count($repo.Branches) -eq 0) {
                    # But really, there is the master!
                    $repo.Head
                } elseif($Force) {
                    $repo.Branches
                } else {
                    $repo.Branches | Where-Object { !$_.IsRemote }
                }
            # We have to transform the object to keep the data around after .Dispose()
            ) | Select-Object $BranchProperties |
                ForEach-Object { $_.PSTypeNames.Insert(0,"PSGit.Branch"); $_ }

        } finally {
            if($null -ne $repo) {
                $repo.Dispose()
            }
        }
    }
}
Set-Alias "Branch" "Get-Branch"

function Get-Status {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String]$Root = $Pwd
    )
    Get-Info -Root $Root |
        Add-Member -Type NoteProperty -Name Changes -Value (
            Get-Change -Root $Root
        ) -Passthru

}

# TODO: DOCUMENT ME
function Show-Status {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String]$Root = $Pwd
    )
    $status = Get-Status -Root $Root
    $changes = $status.Changes

    $status | Out-Default

    if($info.BehindBy) {
        WriteMessage "Action" " (use `git pull` to merge the remote branch into yours)"
    }
    $staged = $changes | where { $_.Staged }
    $unstaged = $changes | where { !$_.Staged}
    $added = $unstaged | where { $_.Change -eq "Added" }
    $unstaged = $unstaged | where { $_.Change -ne "Added" }

    if($staged) {
        WriteMessage "Changes to be committed" "`n (use ``git reset HEAD `${file}`` to unstage)" -ForegroundColor "Green"
        # $fg, $Host.UI.RawUI.ForegroundColor = $Host.UI.RawUI.ForegroundColor, "Green"
        $staged | Out-Default
        # $Host.UI.RawUI.ForegroundColor = $fg
    }

    if($unstaged) {
        WriteMessage "Changes not staged for commit" "`n (use ``git add `${file}`` to update what will be committed)`n (use ``git checkout -- `${file}`` to discard changes in the working directory)" -ForegroundColor "DarkYellow"
        $unstaged | Out-Default
    }

    if($added) {
        WriteMessage "Untracked Files" "`n (use ``git add `${file}`` to include them in what will be committed)" -ForegroundColor "Red"
        $added | Out-Default
    }
}
Set-Alias "Status" "Show-Status"

# Export-ModuleMember -Function *-* -Alias *

# For PSTypes??
# Update-TypeData -TypeName LibGit2Sharp.StatusEntry -MemberType ScriptMethod -MemberName ToString -Value { $this.FilePath }
# OR -Value {
# switch($this.State){
# "Ignored" { }
# "Untracked" { "?? " + $this.FilePath }
# "Added" { "A " + $this.FilePath }
# "Modified" { " M " + $this.FilePath}
# "Added, Modified" { "AM " + $this.FilePath}
# default { $this.State + " " + $this.FilePath}
# }
# } -Force


# TODO: DOCUMENT ME
function New-Repository {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String]$Root = $Pwd
    )
    end {
        $Path = Convert-Path $Root
        # Not sure why this is needed, but if you do a folder on the root it fails
        $Path = Join-Path $path "."

        $null = mkdir $Root -Force -ErrorAction SilentlyContinue
        try {
            $rtn = [LibGit2Sharp.Repository]::Init($Path)
        } finally {}
    }
}

. $PSScriptRoot\PSGitPrompt.ps1
. $PSScriptRoot\PSGitPowerline.ps1