src/Util/GitParse.ps1

# GitParse — Función standalone que ejecuta git status + parse y devuelve un
# hashtable con los datos del repo. NO depende de [Repo] ni de [GitService] —
# eso permite que viva adentro de un runspace paralelo (ForEach-Object -Parallel)
# donde las clases del script padre no son visibles (limitación PS 7).
#
# El caller (RepoDiscoveryService.DiscoverParallel) recibe los hashtables y
# los mapea a [Repo] en el parent runspace, donde la clase sí está cargada.

function Get-RepoStateData {
    param(
        [Parameter(Mandatory)] [string] $Path
    )

    $result = @{
        Path     = $Path
        Name     = Split-Path $Path -Leaf
        Status   = 'nogit'
        Branch   = '—'
        IsOnMainBranch = $false
        Ahead    = 0
        Behind   = 0
        Modified = 0
        Added    = 0
        Deleted  = 0
        Untracked = 0
        StashCount = 0
        LastCommit = $null     # @{ Hash, Message, Author, Date } o $null
    }
    $result.Id = $result.Name.ToLower() -replace '[^a-z0-9]', '-'

    # ¿Es repo git?
    $gitMarker = Join-Path $Path '.git'
    if (-not (Test-Path -LiteralPath $gitMarker)) {
        return $result   # nogit, returna lo que ya está
    }

    # Cambiamos al directorio para no pasar -C (más portable). El runspace
    # tiene su propia working dir, no contamina al parent.
    $prev = Get-Location
    try {
        Set-Location -LiteralPath $Path -ErrorAction Stop
    } catch {
        return $result
    }

    try {
        # 1) Status porcelain v2 con --branch
        $output = & git status --porcelain=v2 --branch 2>$null
        if ($LASTEXITCODE -ne 0 -or $null -eq $output) {
            return $result
        }

        $hasConflict = $false
        foreach ($line in $output) {
            if ($line.StartsWith('# branch.head ')) {
                $result.Branch = $line.Substring(14).Trim()
                $result.IsOnMainBranch = ($result.Branch -in @('main', 'master', 'develop'))
            }
            elseif ($line.StartsWith('# branch.ab ')) {
                $parts = $line.Substring(12).Trim() -split '\s+'
                if ($parts.Count -eq 2) {
                    $result.Ahead  = [int]($parts[0].TrimStart('+'))
                    $result.Behind = [int]($parts[1].TrimStart('-'))
                }
            }
            elseif ($line.StartsWith('1 ') -or $line.StartsWith('2 ')) {
                $xy = $line.Substring(2, 2)
                $x = $xy[0]; $y = $xy[1]
                if ($x -ne '.') {
                    switch ($x) {
                        'M' { $result.Modified++ }
                        'A' { $result.Added++ }
                        'D' { $result.Deleted++ }
                        'R' { $result.Modified++ }
                        'C' { $result.Added++ }
                        default { $result.Modified++ }
                    }
                }
                if ($y -ne '.') {
                    switch ($y) {
                        'M' { $result.Modified++ }
                        'A' { $result.Added++ }
                        'D' { $result.Deleted++ }
                        'R' { $result.Modified++ }
                        default { $result.Modified++ }
                    }
                }
            }
            elseif ($line.StartsWith('? ')) {
                $result.Untracked++
            }
            elseif ($line.StartsWith('u ')) {
                $hasConflict = $true
            }
        }

        # 2) Stashes
        $stashOutput = & git stash list 2>$null
        if ($LASTEXITCODE -eq 0 -and $stashOutput) {
            $result.StashCount = @($stashOutput).Count
        }

        # 3) Last commit
        $lastLine = & git log -1 --format='%h%x09%an%x09%cr%x09%s' 2>$null
        if ($LASTEXITCODE -eq 0 -and $lastLine) {
            # `$lastLine` puede ser string o array; normalizamos a string.
            $first = if ($lastLine -is [array]) { $lastLine[0] } else { $lastLine }
            $parts = ($first -split "`t", 4)
            if ($parts.Count -eq 4) {
                $result.LastCommit = @{
                    Hash    = $parts[0]
                    Author  = $parts[1]
                    Date    = $parts[2]
                    Message = $parts[3]
                }
            }
        }

        # 4) Status derivado (mismo orden que GitService.GetRepoState)
        $dirty = $result.Modified + $result.Added + $result.Deleted + $result.Untracked
        if ($hasConflict)              { $result.Status = 'conflict' }
        elseif ($dirty -gt 0)          { $result.Status = 'dirty' }
        elseif ($result.Ahead -gt 0)   { $result.Status = 'unpushed' }
        elseif ($result.Behind -gt 0)  { $result.Status = 'behind' }
        else                            { $result.Status = 'clean' }
    }
    finally {
        Set-Location -LiteralPath $prev -ErrorAction SilentlyContinue
    }

    return $result
}