src/UI/Screens/CherryPickScreen.ps1
|
# CherryPickScreen — Selector de source branch + lista de commits exclusivos + # cherry-pick uno por uno (réplica del CherryPickFlowController v2). # # Flow: # 1. select-source — lista de branches != current. ←→ filter, ↵ pick. # 2. pick-commits — lista de commits que están en source pero no en current, # ↵ aplica cherry-pick. Loop hasta Esc para volver al source. # # Si el cherry-pick falla por conflict, hacemos --abort y mostramos el error # (el user decide si reintentar manual con git en otra terminal). # # Usa 2 FilteredListPicker instances — uno para branches (label simple) y otro # para commits (RenderRow custom con hash/subject/author + MatchFn que matchea # subject + hash juntos). class CherryPickScreen { [object] $Theme [object] $Renderer [object] $Primitives [object] $Frame [object] $AppHeader [object] $StatusBar [object] $Git [object] $I18n = $null # Inyectable post-construct. [object] $Repo [string] $CurrentBranch = '' [string] $SourceBranch = '' [string] $Mode = 'select-source' # 'select-source' | 'pick-commits' [object] $BranchPicker = $null [object] $CommitPicker = $null [int] $PickedCount = 0 [bool] $QuitRequested = $false [bool] $UseAltBuffer = $true [string] $StatusMessage = '' [string] $StatusKind = '' CherryPickScreen($theme, $renderer, $primitives, $frame, $appHeader, $statusBar, $git) { $this.Theme = $theme $this.Renderer = $renderer $this.Primitives = $primitives $this.Frame = $frame $this.AppHeader = $appHeader $this.StatusBar = $statusBar $this.Git = $git $this.InitPickers() } hidden [void] InitPickers() { $this.BranchPicker = [FilteredListPicker]::new() $this.BranchPicker.Title = 'Filtrar branches' $this.CommitPicker = [FilteredListPicker]::new() $this.CommitPicker.Title = 'Filtrar commits' # Match contra subject + hash juntos (igual que el v2). $this.CommitPicker.MatchFn = { param($item, $needleLower) return ($item.Subject + ' ' + $item.Hash).ToLowerInvariant().Contains($needleLower) } # Render row: hash coloreado + subject + ' · author · date' atenuado. $this.CommitPicker.RenderRowFn = { param($item, $sel, $theme, $reset) $marker = if ($sel) { $theme.Fg('acc') + '▎ ▶ ' + $reset } else { ' · ' } $hashFg = if ($sel) { $theme.Fg('acc') } else { $theme.Fg('gitAhead') } $hash = $hashFg + $item.Hash + $reset $subject = $theme.Fg($(if ($sel) { 'acc' } else { 'fg1' })) + $item.Subject + $reset $meta = $theme.Fg('fg3') + (' · ' + $item.Author + ' · ' + $item.Date) + $reset return $marker + $hash + ' ' + $subject + $meta } } [void] Open([object]$repo) { if ($null -eq $repo) { throw "CherryPickScreen.Open: repo no puede ser null." } $this.Repo = $repo $this.PickedCount = 0 $this.StatusMessage = '' $this.StatusKind = '' $this.Mode = 'select-source' # Detectar current branch. $current = $this.Git.RunGit($repo.Path, @('rev-parse', '--abbrev-ref', 'HEAD')) if (-not $current -or -not $current[0]) { return } $this.CurrentBranch = $current[0].Trim() $this.LoadOtherBranches() if ([Console]::IsInputRedirected) { $rendered = $this.BuildLines() foreach ($l in $rendered) { Write-Host $l } return } $errOut = [Console]::Error $this.Render($errOut) try { while ($true) { $key = [Console]::ReadKey($true) $exit = $this.HandleKey($key) if ($exit -eq 'back') { return } if ($exit -eq 'quit') { $this.QuitRequested = $true; return } $this.Render($errOut) } } finally { } } hidden [void] LoadOtherBranches() { $branches = @($this.Git.GetBranches($this.Repo.Path)) $items = @($branches | ForEach-Object Name | Where-Object { $_ -ne $this.CurrentBranch }) $this.BranchPicker.SetItems($items) $this.BranchPicker.Reset() } hidden [void] LoadCommitsFromSource() { $items = @($this.Git.GetBranchLog($this.Repo.Path, $this.SourceBranch, $this.CurrentBranch, 100)) $this.CommitPicker.SetItems($items) $this.CommitPicker.Reset() } hidden [void] Render([object]$errOut) { if ([Console]::IsInputRedirected) { return } $rendered = $this.BuildLines() $framePayload = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + ($rendered -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync() $errOut.Write($framePayload) $errOut.Flush() } hidden [string] HandleKey([System.ConsoleKeyInfo]$key) { if ($this.Mode -eq 'select-source') { return $this.HandleKeyBranchSelector($key) } if ($this.Mode -eq 'pick-commits') { return $this.HandleKeyCommitSelector($key) } return '' } hidden [string] HandleKeyBranchSelector([System.ConsoleKeyInfo]$key) { $c = $key.KeyChar if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' } $action = $this.BranchPicker.HandleKey($key) if ($action -eq 'enter') { $value = $this.BranchPicker.Selected() if ($null -ne $value) { $this.SourceBranch = [string]$value $this.SetStatus('cargando commits…', 'ok') $this.LoadCommitsFromSource() $this.SetStatus('', '') $this.Mode = 'pick-commits' } } elseif ($action -eq 'escape') { # Esc con filter activo limpia filter, sino vuelve atrás (al menú). if ($this.BranchPicker.Filter) { $this.BranchPicker.Reset() } else { return 'back' } } return '' } hidden [string] HandleKeyCommitSelector([System.ConsoleKeyInfo]$key) { $c = $key.KeyChar if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' } $action = $this.CommitPicker.HandleKey($key) if ($action -eq 'enter') { $picked = $this.CommitPicker.Selected() if ($null -ne $picked) { $r = $this.Git.CherryPick($this.Repo.Path, $picked.Hash) if ($r.Ok) { $this.PickedCount++ $this.SetStatus("✓ pickeado $($picked.Hash) — $($picked.Subject)", 'ok') # Reload commits — el que acabamos de picar ya no está pendiente. $this.LoadCommitsFromSource() } else { $this.SetStatus($r.Message, 'error') } } } elseif ($action -eq 'escape') { # Esc con filter activo limpia filter, sino vuelve al source selector. if ($this.CommitPicker.Filter) { $this.CommitPicker.Reset() } else { $this.Mode = 'select-source' $this.LoadOtherBranches() } } return '' } hidden [void] SetStatus([string]$msg, [string]$kind) { $this.StatusMessage = $msg $this.StatusKind = $kind } [string[]] BuildLines() { $reset = [AnsiService]::Reset $r = $this.Renderer $out = [System.Collections.Generic.List[string]]::new() $repoName = if ($this.Repo) { $this.Repo.Name } else { '—' } $title = if ($this.SourceBranch) { "Cherry-Pick · $repoName · ← $($this.SourceBranch)" } else { "Cherry-Pick · $repoName" } $out.Add($this.Frame.TitleBar('repo-nav', $title, '1.0.0')) $bc = [BreadcrumbBuilder]::Build($this.Repo.Path) $segs = @($bc.Segs) + @($bc.Current) + @('Branches') $out.Add($this.AppHeader.Render($segs, 'Cherry-Pick', @())) $out.Add($r.HRule()) # Status / prompt if ($this.StatusMessage) { $color = if ($this.StatusKind -eq 'error') { 'gitConflict' } else { 'gitClean' } $out.Add(' ' + $this.Theme.Fg($color) + $this.StatusMessage + $reset) } else { $out.Add('') } # Cuerpo según modo — el picker render ya incluye prompt + count + lista. $maxVisible = [Math]::Max(5, [Console]::WindowHeight - 13) if ($this.Mode -eq 'select-source') { $body = $this.BranchPicker.BuildBody($this.Theme, $r, $maxVisible) } else { $body = $this.CommitPicker.BuildBody($this.Theme, $r, $maxVisible) } foreach ($l in $body) { $out.Add($l) } # Sufijo informativo en pick-commits — pickeados hasta ahora. if ($this.Mode -eq 'pick-commits' -and $this.PickedCount -gt 0) { $out.Add($this.Theme.Fg('fg3') + (' pickeados hasta ahora: ' + $this.PickedCount) + $reset) } # StatusBar $hints = $this.StatusBarHints() $out.Add($r.HRule()) return @($out.ToArray()) + $this.StatusBar.RenderAnchored($hints, '', $out.Count) } hidden [string] T([string]$key) { if ($null -ne $this.I18n) { return $this.I18n.T($key) } return ([I18nService]::new('es')).T($key) } hidden [array] StatusBarHints() { if ($this.Mode -eq 'select-source') { return @( @{ k = '↑↓'; label = $this.T('hint.move') } @{ k = $this.T('hint.type'); label = $this.T('hint.search') } @{ k = '↵'; label = 'Pick branch' } @{ k = 'Esc'; label = $this.T('hint.back') } @{ k = 'Q'; label = $this.T('hint.quit') } ) } return @( @{ k = '↑↓'; label = $this.T('hint.move') } @{ k = $this.T('hint.type'); label = $this.T('hint.search') } @{ k = '↵'; label = 'Pick commit' } @{ k = 'Esc'; label = $this.T('hint.back') } @{ k = 'Q'; label = $this.T('hint.quit') } ) } } |