using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace AxCopilot.Services.Agent; public class GitTool : IAgentTool { private static readonly string[] BlockedPatterns = new string[16] { "push", "push --force", "push -f", "pull", "fetch", "reset --hard", "clean -f", "rebase", "merge", "remote add", "remote remove", "remote set-url", "branch -D", "branch -d", "tag -d", "tag -D" }; public string Name => "git_tool"; public string Description => "Execute safe Git operations. Supports: status, diff, log, add, commit, branch, checkout. Push operations are blocked for safety — user must push manually. Works with enterprise GitHub (on-premise) repositories."; public ToolParameterSchema Parameters { get { ToolParameterSchema toolParameterSchema = new ToolParameterSchema(); Dictionary dictionary = new Dictionary(); ToolProperty obj = new ToolProperty { Type = "string", Description = "Git action: status, diff, log, add, commit, branch, checkout, stash, remote" }; int num = 9; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); span[0] = "status"; span[1] = "diff"; span[2] = "log"; span[3] = "add"; span[4] = "commit"; span[5] = "branch"; span[6] = "checkout"; span[7] = "stash"; span[8] = "remote"; obj.Enum = list; dictionary["action"] = obj; dictionary["args"] = new ToolProperty { Type = "string", Description = "Additional arguments. For commit: commit message. For add: file path(s). For log: '--oneline -10'. For diff: file path or '--staged'." }; toolParameterSchema.Properties = dictionary; num = 1; List list2 = new List(num); CollectionsMarshal.SetCount(list2, num); CollectionsMarshal.AsSpan(list2)[0] = "action"; toolParameterSchema.Required = list2; return toolParameterSchema; } } public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { if (!args.TryGetProperty("action", out var actionEl)) { return ToolResult.Fail("action이 필요합니다."); } string action = actionEl.GetString() ?? "status"; JsonElement a; string extraArgs = (args.TryGetProperty("args", out a) ? (a.GetString() ?? "") : ""); string workDir = context.WorkFolder; if (string.IsNullOrEmpty(workDir) || !Directory.Exists(workDir)) { return ToolResult.Fail("작업 폴더가 설정되지 않았습니다."); } string gitPath = FindGit(); if (gitPath == null) { return ToolResult.Fail("Git이 설치되어 있지 않습니다. PATH에 git이 있는지 확인하세요."); } if (!Directory.Exists(Path.Combine(workDir, ".git"))) { string checkDir = workDir; bool found = false; while (!string.IsNullOrEmpty(checkDir)) { if (Directory.Exists(Path.Combine(checkDir, ".git"))) { found = true; break; } string parent = Directory.GetParent(checkDir)?.FullName; if (parent == checkDir) { break; } checkDir = parent; } if (!found) { return ToolResult.Fail("현재 작업 폴더는 Git 저장소가 아닙니다."); } } if (1 == 0) { } string text = action switch { "status" => "status --short --branch", "diff" => string.IsNullOrEmpty(extraArgs) ? "diff" : ("diff " + extraArgs), "log" => string.IsNullOrEmpty(extraArgs) ? "log --oneline -15" : ("log " + extraArgs), "add" => string.IsNullOrEmpty(extraArgs) ? "add -A" : ("add " + extraArgs), "commit" => string.IsNullOrEmpty(extraArgs) ? null : ("commit -m \"" + extraArgs.Replace("\"", "\\\"") + "\""), "branch" => string.IsNullOrEmpty(extraArgs) ? "branch -a" : ("branch " + extraArgs), "checkout" => string.IsNullOrEmpty(extraArgs) ? null : ("checkout " + extraArgs), "stash" => string.IsNullOrEmpty(extraArgs) ? "stash list" : ("stash " + extraArgs), "remote" => "remote -v", _ => null, }; if (1 == 0) { } string gitCommand = text; if (gitCommand == null) { if (action == "commit") { return ToolResult.Fail("커밋 메시지가 필요합니다. args에 커밋 메시지를 지정하세요."); } if (action == "checkout") { return ToolResult.Fail("체크아웃할 브랜치/파일을 args에 지정하세요."); } return ToolResult.Fail("알 수 없는 액션: " + action); } string fullCmd = "git " + gitCommand; string[] blockedPatterns = BlockedPatterns; foreach (string pattern in blockedPatterns) { if (fullCmd.Contains(pattern, StringComparison.OrdinalIgnoreCase)) { return ToolResult.Fail("안전을 위해 '" + pattern + "' 작업은 차단됩니다.\n원격 저장소 작업(push/pull/fetch)과 이력 변경 작업은 사용자가 직접 수행하세요."); } } HashSet writeActions = new HashSet { "add", "commit", "checkout", "stash" }; if (writeActions.Contains(action) && !(await context.CheckWritePermissionAsync(Name, workDir))) { return ToolResult.Fail("Git 쓰기 권한이 거부되었습니다."); } if (action == "commit") { return ToolResult.Fail("Git 커밋 기능은 현재 비활성 상태입니다.\n안전을 위해 커밋은 사용자가 직접 수행하세요.\n향후 버전에서 활성화될 예정입니다."); } try { ProcessStartInfo psi = new ProcessStartInfo(gitPath, gitCommand) { WorkingDirectory = workDir, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(30.0)); using Process proc = Process.Start(psi); if (proc == null) { return ToolResult.Fail("Git 프로세스 시작 실패"); } string stdout = await proc.StandardOutput.ReadToEndAsync(cts.Token); string stderr = await proc.StandardError.ReadToEndAsync(cts.Token); await proc.WaitForExitAsync(cts.Token); if (stdout.Length > 8000) { stdout = stdout.Substring(0, 8000) + "\n... (출력 잘림)"; } StringBuilder sb = new StringBuilder(); StringBuilder stringBuilder = sb; StringBuilder stringBuilder2 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(18, 2, stringBuilder); handler.AppendLiteral("[git "); handler.AppendFormatted(action); handler.AppendLiteral("] Exit code: "); handler.AppendFormatted(proc.ExitCode); stringBuilder2.AppendLine(ref handler); if (!string.IsNullOrWhiteSpace(stdout)) { sb.Append(stdout); } if (!string.IsNullOrWhiteSpace(stderr) && proc.ExitCode != 0) { stringBuilder = sb; StringBuilder stringBuilder3 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(10, 1, stringBuilder); handler.AppendLiteral("\n[stderr] "); handler.AppendFormatted(stderr.Trim()); stringBuilder3.AppendLine(ref handler); } return (proc.ExitCode == 0) ? ToolResult.Ok(sb.ToString()) : ToolResult.Fail(sb.ToString()); } catch (OperationCanceledException) { return ToolResult.Fail("Git 명령 타임아웃 (30초)"); } catch (Exception ex2) { return ToolResult.Fail("Git 실행 오류: " + ex2.Message); } } private static string? FindGit() { try { ProcessStartInfo startInfo = new ProcessStartInfo("where.exe", "git") { RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; using Process process = Process.Start(startInfo); if (process == null) { return null; } string text = process.StandardOutput.ReadToEnd().Trim(); process.WaitForExit(5000); return string.IsNullOrWhiteSpace(text) ? null : text.Split('\n')[0].Trim(); } catch { return null; } } }