using System.Diagnostics; using System.Text; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L7-1: Git 빠른 조회 핸들러. "git" 프리픽스로 사용합니다. /// /// 예: git → 최근 작업 폴더(또는 현재 앱 폴더)의 git 상태 요약 /// git status → git status --short 출력 /// git log → 최근 커밋 10개 /// git branch → 브랜치 목록 (현재 브랜치 강조) /// git stash → stash 목록 /// git diff → git diff --stat 요약 /// git pull → git pull 실행 /// Enter → 결과를 클립보드에 복사. /// public class GitHandler : IActionHandler { public string? Prefix => "git"; public PluginMetadata Metadata => new( "Git", "Git 빠른 조회 — git status · log · branch · stash", "1.0", "AX"); // ── 서브커맨드 정의 ────────────────────────────────────────────────────── private static readonly (string Sub, string Args, string Label, string Icon)[] SubCommands = [ ("status", "status --short", "변경 파일 목록", "\uE9F5"), ("log", "log --oneline -10", "최근 커밋 10개", "\uE81C"), ("branch", "branch -a", "브랜치 목록", "\uE8FB"), ("stash", "stash list", "Stash 목록", "\uE7C4"), ("diff", "diff --stat", "변경 통계", "\uE8A1"), ("pull", "pull", "git pull 실행", "\uE8AF"), ]; public async Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim().ToLowerInvariant(); var items = new List(); // 작업 디렉토리 결정 var workDir = FindGitRoot(); if (string.IsNullOrEmpty(q)) { // 빠른 상태 요약 if (!string.IsNullOrEmpty(workDir)) { var branch = await RunGitAsync("branch --show-current", workDir, ct); var statusOut = await RunGitAsync("status --short", workDir, ct); var changed = statusOut?.Split('\n', StringSplitOptions.RemoveEmptyEntries).Length ?? 0; items.Add(new LauncherItem( $"Git: {branch?.Trim() ?? "unknown"}", changed == 0 ? "변경 없음" : $"{changed}개 파일 변경됨 · {System.IO.Path.GetFileName(workDir)}", null, ("status_summary", workDir), Symbol: "\uE9F5")); } else { items.Add(new LauncherItem("Git 저장소 없음", "현재 작업 폴더에 .git 디렉토리가 없습니다", null, null, Symbol: "\uE783")); } // 서브커맨드 목록 foreach (var (sub, args, label, icon) in SubCommands) { items.Add(new LauncherItem( $"git {sub}", label, null, (sub, workDir ?? ""), Symbol: icon)); } return items; } // 서브커맨드 매칭 var matched = SubCommands .Where(sc => sc.Sub.StartsWith(q, StringComparison.OrdinalIgnoreCase)) .ToList(); if (matched.Count > 0) { foreach (var (sub, args, label, icon) in matched) { string? preview = null; if (!string.IsNullOrEmpty(workDir)) preview = await RunGitAsync(args, workDir, ct); var subtitle = preview != null ? TruncateLines(preview, 3) : label; items.Add(new LauncherItem( $"git {sub}", subtitle, null, (sub, workDir ?? "", args), Symbol: icon)); } } else { // 자유 명령 실행 (git ) string? output = null; if (!string.IsNullOrEmpty(workDir)) output = await RunGitAsync(q, workDir, ct); items.Add(new LauncherItem( $"git {query.Trim()}", output != null ? TruncateLines(output, 3) : "실행 후 결과 클립보드 복사", null, ("custom", workDir ?? "", query.Trim()), Symbol: "\uE9F5")); } return items; } public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) { string? result = null; switch (item.Data) { // 상태 요약 항목 case ("status_summary", string workDir): result = await RunGitAsync("status", workDir, ct); break; // 서브커맨드 항목 (sub, workDir, args) case (string sub, string workDir, string args): if (sub == "pull") { // pull은 별도 터미널 창으로 실행 if (!string.IsNullOrEmpty(workDir)) { Process.Start(new ProcessStartInfo { FileName = "powershell.exe", Arguments = $"-NoProfile -Command \"cd '{workDir}'; git pull; Read-Host 'Enter 키를 누르면 닫힙니다'\"", UseShellExecute = true, }); } return; } result = await RunGitAsync(args, workDir, ct); break; // 서브커맨드 항목 (sub, workDir) — args 없는 경우 case (string sub2, string workDir2): var found = SubCommands.FirstOrDefault(sc => sc.Sub == sub2); if (found != default) result = await RunGitAsync(found.Args, workDir2, ct); break; default: break; } if (!string.IsNullOrWhiteSpace(result)) { try { System.Windows.Application.Current.Dispatcher.Invoke( () => Clipboard.SetText(result)); NotificationService.Notify("Git", "결과를 클립보드에 복사했습니다."); } catch { /* 비핵심 */ } } } // ── 헬퍼 ──────────────────────────────────────────────────────────────── /// 현재 앱 설정의 작업 폴더에서 .git root를 찾습니다. private static string? FindGitRoot() { var app = System.Windows.Application.Current as App; var workDir = app?.SettingsService?.Settings.Llm.WorkFolder ?? ""; if (string.IsNullOrEmpty(workDir) || !System.IO.Directory.Exists(workDir)) workDir = AppDomain.CurrentDomain.BaseDirectory; // .git 폴더를 찾아 상위로 이동 var dir = new System.IO.DirectoryInfo(workDir); while (dir != null) { if (System.IO.Directory.Exists(System.IO.Path.Combine(dir.FullName, ".git"))) return dir.FullName; dir = dir.Parent; } return null; } /// git 명령을 비동기로 실행하고 출력을 반환합니다. private static async Task RunGitAsync(string args, string workDir, CancellationToken ct) { if (string.IsNullOrEmpty(workDir)) return null; try { var psi = new ProcessStartInfo("git", args) { WorkingDirectory = workDir, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, }; using var proc = Process.Start(psi); if (proc == null) return null; var output = await proc.StandardOutput.ReadToEndAsync(ct); var error = await proc.StandardError.ReadToEndAsync(ct); await proc.WaitForExitAsync(ct); var text = output.Trim(); if (string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(error)) text = error.Trim(); return string.IsNullOrWhiteSpace(text) ? "(출력 없음)" : text; } catch (OperationCanceledException) { return null; } catch (Exception ex) { return $"오류: {ex.Message}"; } } /// 긴 출력을 maxLines줄로 자릅니다. private static string TruncateLines(string text, int maxLines) { var lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries); if (lines.Length <= maxLines) return string.Join(" · ", lines.Take(maxLines)).Trim(); return string.Join(" · ", lines.Take(maxLines)) + $" … (+{lines.Length - maxLines}줄)"; } }