Forge
csharpdeeb25a2
1using System.Diagnostics.CodeAnalysis;
2using CascadeIDE.Services;
3using GitMcp.Core;
4
5namespace CascadeIDE.Features.IdeMcp.Application;
6
7/// <summary>
8/// MCP-обёртка над <see cref="IGitCommandRunner"/> для текущего workspace: команды GitMcp.Core + формирование JSON ответов.
9/// ViewModel задаёт только путь каталога и делегирует сюды.
10/// </summary>
11public sealed class IdeMcpGitWorkspaceSession
12{
13 private static readonly string[] PreflightSafeFixAppliedCommands = ["git add --renormalize ."];
14
15 private readonly IGitCommandRunner _runner;
16 private readonly string _workspace;
17
18 private IdeMcpGitWorkspaceSession(IGitCommandRunner runner, string workspace)
19 {
20 _runner = runner;
21 _workspace = workspace;
22 }
23
24 /// <summary>
25 /// Невалидный каталог — JSON как у неуспешного <c>git</c>-шага (<c>exit_code</c> -1).
26 /// </summary>
27 public static bool TryCreate(
28 IGitCommandRunner runner,
29 string? workspacePath,
30 [NotNullWhen(true)] out IdeMcpGitWorkspaceSession? session,
31 out string errorJson)
32 {
33 if (string.IsNullOrWhiteSpace(workspacePath) || !Directory.Exists(workspacePath))
34 {
35 session = null;
36 errorJson = IdeMcpGitOrchestrator.BuildCommandResult(
37 false,
38 -1,
39 IdeMcpGitOrchestrator.WorkspaceUnavailableMessage());
40 return false;
41 }
42
43 session = new IdeMcpGitWorkspaceSession(runner, workspacePath);
44 errorJson = "";
45 return true;
46 }
47
48 public Task<string> GitStatusAsync() =>
49 RunGitCommandJsonAsync(GitCommandBuilder.StatusShortBranch());
50
51 public Task<string> GitDiffAsync(string? path, bool staged) =>
52 RunGitCommandJsonAsync(GitCommandBuilder.Diff(staged, path));
53
54 public Task<string> GitLogAsync(int n) =>
55 RunGitCommandJsonAsync(GitCommandBuilder.Log(n));
56
57 public Task<string> GitFetchAsync(string? remote, bool all, bool prune, bool dryRun)
58 {
59 var r = GitCommandBuilder.Fetch(all, prune, remote, dryRun);
60 return r.IsSuccess
61 ? RunGitCommandJsonAsync(r.Args!)
62 : Task.FromResult(IdeMcpGitOrchestrator.BuildValidationError(r.Error!));
63 }
64
65 public Task<string> GitPullAsync(string? remote, string? branch, bool ffOnly, bool dryRun)
66 {
67 var r = GitCommandBuilder.Pull(remote, branch, ffOnly, dryRun);
68 return r.IsSuccess
69 ? RunGitCommandJsonAsync(r.Args!)
70 : Task.FromResult(IdeMcpGitOrchestrator.BuildValidationError(r.Error!));
71 }
72
73 public async Task<string> GitBranchAsync(string? action, string? name, string? startPoint, bool force)
74 {
75 var a = IdeMcpGitOrchestrator.NormalizeAction(action, "list");
76 switch (a)
77 {
78 case "list":
79 return await RunGitCommandJsonAsync(GitCommandBuilder.BranchList().Args!).ConfigureAwait(false);
80 case "create":
81 var cr = GitCommandBuilder.BranchCreate(name ?? "", startPoint);
82 return cr.IsSuccess
83 ? await RunGitCommandJsonAsync(cr.Args!).ConfigureAwait(false)
84 : IdeMcpGitOrchestrator.BuildValidationError(cr.Error!);
85 case "delete":
86 var dr = GitCommandBuilder.BranchDelete(name ?? "", force);
87 return dr.IsSuccess
88 ? await RunGitCommandJsonAsync(dr.Args!).ConfigureAwait(false)
89 : IdeMcpGitOrchestrator.BuildValidationError(dr.Error!);
90 default:
91 return IdeMcpGitOrchestrator.BuildInvalidGitBranchActionError();
92 }
93 }
94
95 public Task<string> GitShowAsync(string rev, string? path, bool statOnly)
96 {
97 var r = GitCommandBuilder.Show(rev, path, statOnly);
98 return r.IsSuccess
99 ? RunGitCommandJsonAsync(r.Args!)
100 : Task.FromResult(IdeMcpGitOrchestrator.BuildValidationError(r.Error!));
101 }
102
103 public Task<string> GitSubmoduleAsync(string? action, string? path, bool recursive)
104 {
105 var a = IdeMcpGitOrchestrator.NormalizeAction(action, "status");
106 switch (a)
107 {
108 case "status":
109 return RunGitCommandJsonAsync(GitCommandBuilder.SubmoduleStatus().Args!);
110 case "update":
111 var r = GitCommandBuilder.SubmoduleUpdate(recursive, path);
112 return r.IsSuccess
113 ? RunGitCommandJsonAsync(r.Args!)
114 : Task.FromResult(IdeMcpGitOrchestrator.BuildValidationError(r.Error!));
115 default:
116 return Task.FromResult(IdeMcpGitOrchestrator.BuildInvalidGitSubmoduleActionError());
117 }
118 }
119
120 public async Task<string> GitPreflightAsync(bool staged, bool includeUntracked, bool includePatches)
121 {
122 var changedOutput = await RunGitCommandAsync(GitCommandBuilder.DiffNameOnly(staged)).ConfigureAwait(false);
123 if (!changedOutput.Success)
124 return IdeMcpGitOrchestrator.BuildCommandResult(false, changedOutput.ExitCode, IdeMcpGitOrchestrator.TruncateOutput(changedOutput.Output, 4000));
125
126 var ignoreCrOutput = await RunGitCommandAsync(GitCommandBuilder.DiffNameOnly(staged, ignoreCrAtEol: true)).ConfigureAwait(false);
127 if (!ignoreCrOutput.Success)
128 return IdeMcpGitOrchestrator.BuildCommandResult(false, ignoreCrOutput.ExitCode, IdeMcpGitOrchestrator.TruncateOutput(ignoreCrOutput.Output, 4000));
129
130 var ignoreWsOutput = await RunGitCommandAsync(GitCommandBuilder.DiffNameOnly(staged, ignoreWhitespace: true, ignoreCrAtEol: true)).ConfigureAwait(false);
131 if (!ignoreWsOutput.Success)
132 return IdeMcpGitOrchestrator.BuildCommandResult(false, ignoreWsOutput.ExitCode, IdeMcpGitOrchestrator.TruncateOutput(ignoreWsOutput.Output, 4000));
133
134 var changed = GitPreflight.ParseNameOnlyOutput(changedOutput.Output);
135 var untracked = includeUntracked
136 ? GitPreflight.ParseNameOnlyOutput((await RunGitCommandAsync(GitCommandBuilder.ListUntracked()).ConfigureAwait(false)).Output)
137 : [];
138 var ignoreCr = GitPreflight.ParseNameOnlyOutput(ignoreCrOutput.Output);
139 var ignoreWs = GitPreflight.ParseNameOnlyOutput(ignoreWsOutput.Output);
140
141 Dictionary<string, string>? patches = null;
142 if (includePatches && changed.Count > 0)
143 {
144 patches = new Dictionary<string, string>(StringComparer.Ordinal);
145 foreach (var file in changed)
146 {
147 var patchArgs = GitCommandBuilder.DiffPatchForPath(staged, file);
148 if (!patchArgs.IsSuccess)
149 continue;
150 var patchResult = await RunGitCommandAsync(patchArgs.Args!).ConfigureAwait(false);
151 if (patchResult.Success)
152 patches[file] = patchResult.Output;
153 }
154 }
155
156 var report = GitPreflight.BuildReport(changed, untracked, ignoreCr, ignoreWs, patches);
157 return IdeMcpGitOrchestrator.BuildPreflightReport(staged, report);
158 }
159
160 public async Task<string> GitPreflightFixSafeAsync(bool includePatches)
161 {
162 var renormResult = await RunGitCommandAsync(GitCommandBuilder.AddRenormalize()).ConfigureAwait(false);
163 if (!renormResult.Success)
164 return IdeMcpGitOrchestrator.BuildCommandResult(false, renormResult.ExitCode, IdeMcpGitOrchestrator.TruncateOutput(renormResult.Output, 4000));
165
166 var post = await GitPreflightAsync(staged: false, includeUntracked: true, includePatches).ConfigureAwait(false);
167 return IdeMcpGitOrchestrator.BuildPreflightFixSafeResult(post, PreflightSafeFixAppliedCommands);
168 }
169
170 public async Task<string> GitCommitAsync(string message, IReadOnlyList<string>? paths)
171 {
172 if (string.IsNullOrWhiteSpace(message))
173 return IdeMcpGitOrchestrator.BuildMissingCommitMessageError();
174
175 var addResult = await RunGitCommandAsync(GitCommandBuilder.Add(paths)).ConfigureAwait(false);
176 if (!addResult.Success)
177 return IdeMcpGitOrchestrator.BuildStepFailure("add", addResult.ExitCode, addResult.Output);
178
179 var commitResult = await RunGitCommandAsync(GitCommandBuilder.Commit(message)).ConfigureAwait(false);
180 return IdeMcpGitOrchestrator.BuildCommitResult(commitResult.Success, commitResult.ExitCode, IdeMcpGitOrchestrator.TruncateOutput(commitResult.Output, 4000));
181 }
182
183 public Task<string> GitPushAsync(string? remote, string? branch, bool dryRun)
184 {
185 var args = GitCommandBuilder.Push(remote, branch, defaultOriginWhenRemoteEmpty: false, dryRun);
186 return RunGitCommandJsonAsync(args);
187 }
188
189 private async Task<string> RunGitCommandJsonAsync(IReadOnlyList<string> args)
190 {
191 var result = await RunGitCommandAsync(args).ConfigureAwait(false);
192 return IdeMcpGitOrchestrator.BuildCommandResult(result.Success, result.ExitCode, IdeMcpGitOrchestrator.TruncateOutput(result.Output, 4000));
193 }
194
195 private Task<(bool Success, int ExitCode, string Output)> RunGitCommandAsync(IReadOnlyList<string> args) =>
196 _runner.RunAsync(args, _workspace);
197}
198
View only · write via MCP/CIDE