Public/Get-GitIdentities.ps1

function Get-GitIdentities {
<#
.SYNOPSIS
Enumerates Git identities by reading per-alias gitconfig files and global .gitconfig includeIf blocks.
.DESCRIPTION
Returns a list of identity objects with: alias, platform, name, email, username, folders, ssh.
Information is gathered from:
 - Per-alias gitconfig files (~/.gitconfig-{alias}): user info, credentials, SSH configuration
 - Global .gitconfig includeIf blocks: folder associations
#>

  [CmdletBinding()] param(
    [string]$User,
    [ValidateSet('Silent','Error','Warn','Info','Debug')][string]$Verbosity='Warn'
  )
  $script:GitIdentitiesVerbosity = $Verbosity
  $userHome = Get-GIUserHome -User $User

  $results = @{}
  $addOrMerge = {
    param($alias,$data)
    if (-not $results.ContainsKey($alias)) {
      # Normalizar folders a array limpio
      if (-not ($data.folders -is [System.Collections.IEnumerable])) { $data.folders = @($data.folders) }
      $data.folders = @($data.folders | Where-Object { $_ }) | Select-Object -Unique
      $results[$alias] = $data
    } else {
      $existing = $results[$alias]
      # Merge folders
      if ($data.folders) {
        $existing.folders = @(@($existing.folders)+@($data.folders)) | Where-Object { $_ } | Select-Object -Unique
      }
      # Solo actualizar si no existe (prioridad a gitconfig-alias)
      foreach ($prop in 'platform','name','email','username','ssh') {
        if (-not $existing.$prop -and $data.$prop) { $existing.$prop = $data.$prop }
      }
    }
  }

  # 1. Per-alias gitconfig-* files
  Get-ChildItem -Path $userHome -Filter '.gitconfig-*' -File -ErrorAction SilentlyContinue | ForEach-Object {
    $file = $_.FullName
    $alias = ($_.Name -replace '^\.gitconfig-','')
    $lines = Get-Content -LiteralPath $file -Encoding UTF8
    $name = ($lines | Where-Object { $_ -match '^\s*name\s*=\s*' } | Select-Object -First 1) -replace '^\s*name\s*=\s*',''
    $email = ($lines | Where-Object { $_ -match '^\s*email\s*=\s*' } | Select-Object -First 1) -replace '^\s*email\s*=\s*',''
    $username = ($lines | Where-Object { $_ -match '^\s*username\s*=\s*' } | Select-Object -First 1) -replace '^\s*username\s*=\s*',''
    
    # SSH info: buscar core.sshCommand
    $sshInfo = [pscustomobject]@{ hasKey=$false; keyPath=$null; sshUsers=$null }
    $sshLines = @($lines | Where-Object { $_ -match '^\s*sshCommand\s*=\s*' })
    if ($sshLines.Count -gt 0) {
      # Extract key file from -i parameter (first occurrence)
      $firstLine = $sshLines[0]
      if ($firstLine -match '-i\s+"?([^"\s]+)"?') {
        $keyPath = $Matches[1]
        # Expandir ~ al home del usuario
        if ($keyPath -like '~/*' -or $keyPath -like '~\*') {
          $keyPath = $keyPath -replace '^~[/\\]?', ($userHome + [IO.Path]::DirectorySeparatorChar)
        }
        $sshInfo.keyPath = $keyPath
        $sshInfo.hasKey = Test-Path -LiteralPath $keyPath -ErrorAction SilentlyContinue
      }
      # Extract SSH users from all sshCommand lines
      $sshUsers = @()
      foreach ($line in $sshLines) {
        # Check for {{USERNAME}} token (multi-user)
        if ($line -match '\{\{USERNAME\}\}') {
          if ($line -match 'ssh://([^@]+)@') {
            $sshUsers += $Matches[1]
          }
        }
        # Check for -o User=xxx format
        elseif ($line -match '-o\s+User=(\S+)') {
          $sshUsers += $Matches[1]
        }
        # Check for ssh://user@host format
        elseif ($line -match 'ssh://([^@]+)@') {
          $sshUsers += $Matches[1]
        }
      }
      if ($sshUsers.Count -gt 0) {
        $uniqueUsers = @($sshUsers | Select-Object -Unique)
        $sshInfo.sshUsers = $uniqueUsers -join ', '
      }
    }
    
    # Credenciales: buscar secciones [credential "url"]
    $creds = @()
    for ($i=0; $i -lt $lines.Count; $i++) {
      $l = $lines[$i]
      if ($l -match '^\s*\[credential\s+"([^"]+)"\s*\]') {
        $cUrl = $Matches[1]
        $j=$i+1; $cUser=$null
        while ($j -lt $lines.Count -and $lines[$j] -notmatch '^\s*\[') {
          if ($lines[$j] -match '^\s*username\s*=\s*(.+)$') { $cUser = $Matches[1].Trim() }
          $j++
        }
        $creds += [pscustomobject]@{ url=$cUrl; username=$cUser }
        $i=$j-1
      }
    }
    & $addOrMerge $alias ([pscustomobject]@{ alias=$alias; platform=$null; name=$name; email=$email; username=$username; folders=@(); credentials=$creds; ssh=$sshInfo })
  }

  # 2. Global .gitconfig includeIf blocks
  $globalGit = Join-Path $userHome '.gitconfig'
  if (Test-Path -LiteralPath $globalGit) {
    $glines = Get-Content -LiteralPath $globalGit -Encoding UTF8
    for ($i=0; $i -lt $glines.Count; $i++) {
      $line = $glines[$i]
      if ($line -like '*includeIf*gitdir:*') {
        # Expect next line path = .../.gitconfig-alias
        $block = @($line)
        $j=$i+1
        while ($j -lt $glines.Count -and $glines[$j] -match '^\s' ) { $block += $glines[$j]; $j++ }
        $pathLine = $block | Where-Object { $_ -match '\bpath\s*=\s*' } | Select-Object -First 1
        if ($pathLine) {
          $p = ($pathLine -split '=')[1].Trim()
          if ($p -match '\.gitconfig-(.+)$') {
            $alias = $Matches[1]
            # Extract folder from condition
            $folder = $null
            if ($line -match 'gitdir:([^"\]]+)') { $folder = $Matches[1] }
            if ($folder) {
              # Normalizar: asegurar barra final y limpiar caracteres
              $folder = $folder.Trim().TrimEnd('"',']')
              if ($folder -notmatch '/$') { $folder += '/' }
            }
            & $addOrMerge $alias ([pscustomobject]@{ alias=$alias; platform=$null; name=$null; email=$null; username=$null; folders=@($folder) })
          }
        }
        $i = $j-1
      }
    }
  }

  # Convertir a salida ordenada
  $objects = @()
  foreach ($k in $results.Keys | Sort-Object) {
    $obj = $results[$k]
    # Platform: derivar del primer dominio de credentials si existe
    $obj.platform = $null
    if ($obj.PSObject.Properties.Name -contains 'credentials' -and $obj.credentials -and $obj.credentials.Count -gt 0) {
      $firstCred = ($obj.credentials | Where-Object { $_.url })[0]
      if ($firstCred) {
        $u = $firstCred.url
        if ($u.EndsWith('/')) { $u = $u.TrimEnd('/') }
        $domain = $null
        if ($u -match '^[a-zA-Z][a-zA-Z0-9+.-]*://([^/]+)') { $domain = $Matches[1] }
        elseif ($u -match '^([^/]+)(/|$)') { $domain = $Matches[1] }
        if ($domain) {
          $hl = $domain.ToLowerInvariant()
          switch ($hl) {
            'github.com' { $obj.platform='github' }
            'dev.azure.com' { $obj.platform='azure' }
            'gitlab.com' { $obj.platform='gitlab' }
            'bitbucket.org' { $obj.platform='bitbucket' }
            default { $obj.platform = $hl }
          }
        }
      }
    }
    # Limpiar propiedades internas
    $null = $obj.PSObject.Properties.Remove('credentials')
    $objects += $obj
  }
  return $objects
}