Forge
csharp15d33a96
1using System.Globalization;
2
3namespace GitMcp.Core;
4
5/// <summary>Единая сборка аргументов для вызова <c>git</c> (паритет GitMcp и Cascade IDE).</summary>
6public static class GitCommandBuilder
7{
8 public const int LogCountDefault = 20;
9 public const int LogCountMax = 500;
10
11 /// <summary>Совпадает с вкладкой Git / телеметрией IDE: <c>git status --short --branch</c>.</summary>
12 public static IReadOnlyList<string> StatusShortBranch() => ["status", "--short", "--branch"];
13
14 /// <summary>Две команды подряд, как в MCP git_status: ветка и полный <c>status</c>.</summary>
15 public static IReadOnlyList<IReadOnlyList<string>> StatusMcpSequence() =>
16 [
17 ["rev-parse", "--abbrev-ref", "HEAD"],
18 ["status"]
19 ];
20
21 public static IReadOnlyList<string> Diff(bool staged, string? path)
22 {
23 var list = new List<string> { "diff" };
24 if (staged)
25 list.Add("--staged");
26 if (!string.IsNullOrWhiteSpace(path))
27 {
28 list.Add("--");
29 list.Add(path.Trim());
30 }
31 return list;
32 }
33
34 /// <summary>
35 /// Список изменённых путей (name-only) для preflight-классификации шума.
36 /// </summary>
37 public static IReadOnlyList<string> DiffNameOnly(bool staged, bool ignoreWhitespace = false, bool ignoreCrAtEol = false)
38 {
39 var list = new List<string> { "diff", "--name-only" };
40 if (staged)
41 list.Add("--staged");
42 if (ignoreWhitespace)
43 list.Add("-w");
44 if (ignoreCrAtEol)
45 list.Add("--ignore-cr-at-eol");
46 return list;
47 }
48
49 /// <summary>
50 /// Патч одного файла (для эвристик BOM-only и диагностики preflight).
51 /// </summary>
52 public static GitArgsResult DiffPatchForPath(bool staged, string path)
53 {
54 if (string.IsNullOrWhiteSpace(path))
55 return GitArgsResult.Fail("git_preflight: file path is required for DiffPatchForPath.");
56 var list = new List<string> { "diff" };
57 if (staged)
58 list.Add("--staged");
59 list.Add("--");
60 list.Add(path.Trim());
61 return GitArgsResult.Ok(list);
62 }
63
64 /// <summary>
65 /// Безопасная нормализация line endings по .gitattributes.
66 /// </summary>
67 public static IReadOnlyList<string> AddRenormalize() => ["add", "--renormalize", "."];
68
69 /// <summary>
70 /// Показать только untracked пути (без ignored) для preflight.
71 /// </summary>
72 public static IReadOnlyList<string> ListUntracked() => ["ls-files", "--others", "--exclude-standard"];
73
74 public static int ClampLogCount(int n)
75 {
76 if (n <= 0)
77 return LogCountDefault;
78 return n > LogCountMax ? LogCountMax : n;
79 }
80
81 public static IReadOnlyList<string> Log(int count) =>
82 ["log", "-n", ClampLogCount(count).ToString(CultureInfo.InvariantCulture), "--oneline"];
83
84 public static IReadOnlyList<string> Add(IReadOnlyList<string>? paths)
85 {
86 var list = new List<string> { "add" };
87 if (paths is { Count: > 0 })
88 {
89 var nonEmpty = paths.Where(p => !string.IsNullOrWhiteSpace(p)).Select(p => p.Trim()).ToList();
90 if (nonEmpty.Count > 0)
91 {
92 list.Add("--");
93 list.AddRange(nonEmpty);
94 return list;
95 }
96 }
97 list.Add("-A");
98 return list;
99 }
100
101 public static IReadOnlyList<string> Commit(string message) => ["commit", "-m", message];
102
103 /// <param name="defaultOriginWhenRemoteEmpty">Если true и remote пуст — подставить <c>origin</c> (поведение MCP git_push).</param>
104 /// <param name="dryRun">Если true — <c>git push --dry-run</c> (без отправки объектов).</param>
105 public static IReadOnlyList<string> Push(string? remote, string? branch, bool defaultOriginWhenRemoteEmpty, bool dryRun = false)
106 {
107 var list = new List<string> { "push" };
108 if (dryRun)
109 list.Add("--dry-run");
110 string? r = string.IsNullOrWhiteSpace(remote) ? null : remote.Trim();
111 if (r is null && defaultOriginWhenRemoteEmpty)
112 r = "origin";
113 if (r is not null)
114 list.Add(r);
115 if (!string.IsNullOrWhiteSpace(branch))
116 list.Add(branch.Trim());
117 return list;
118 }
119
120 /// <param name="dryRun">Если true — <c>git fetch --dry-run</c> (что бы сделал fetch без обновления refs).</param>
121 public static GitArgsResult Fetch(bool all, bool prune, string? remote, bool dryRun = false)
122 {
123 if (all && !string.IsNullOrWhiteSpace(remote))
124 return GitArgsResult.Fail("git_fetch: do not pass remote when all=true.");
125 var list = new List<string> { "fetch" };
126 if (dryRun)
127 list.Add("--dry-run");
128 if (all)
129 {
130 list.Add("--all");
131 if (prune)
132 list.Add("--prune");
133 }
134 else
135 {
136 if (prune)
137 list.Add("--prune");
138 if (!string.IsNullOrWhiteSpace(remote))
139 list.Add(remote.Trim());
140 }
141 return GitArgsResult.Ok(list);
142 }
143
144 /// <param name="dryRun">Если true — <c>git pull --dry-run</c> (без изменения рабочей копии; требуется Git 2.27+).</param>
145 public static GitArgsResult Pull(string? remote, string? branch, bool ffOnly, bool dryRun = false)
146 {
147 var pullRem = remote?.Trim() ?? "";
148 var pullBr = branch?.Trim() ?? "";
149 if (string.IsNullOrWhiteSpace(pullRem) != string.IsNullOrWhiteSpace(pullBr))
150 return GitArgsResult.Fail("git_pull: specify both remote and branch, or neither (pull upstream).");
151 var list = new List<string> { "pull" };
152 if (dryRun)
153 list.Add("--dry-run");
154 if (ffOnly)
155 list.Add("--ff-only");
156 if (!string.IsNullOrWhiteSpace(pullRem))
157 {
158 list.Add(pullRem);
159 list.Add(pullBr);
160 }
161 return GitArgsResult.Ok(list);
162 }
163
164 public static GitArgsResult BranchList() => GitArgsResult.Ok(["branch", "-vv"]);
165
166 public static GitArgsResult BranchCreate(string name, string? startPoint)
167 {
168 if (string.IsNullOrWhiteSpace(name))
169 return GitArgsResult.Fail("git_branch create: name is required.");
170 var list = new List<string> { "branch", name.Trim() };
171 if (!string.IsNullOrWhiteSpace(startPoint))
172 list.Add(startPoint.Trim());
173 return GitArgsResult.Ok(list);
174 }
175
176 public static GitArgsResult BranchDelete(string name, bool force)
177 {
178 if (string.IsNullOrWhiteSpace(name))
179 return GitArgsResult.Fail("git_branch delete: name is required.");
180 return GitArgsResult.Ok(["branch", force ? "-D" : "-d", name.Trim()]);
181 }
182
183 public static GitArgsResult Show(string rev, string? path, bool statOnly)
184 {
185 if (string.IsNullOrWhiteSpace(rev))
186 return GitArgsResult.Fail("git_show: rev is required.");
187 var r = rev.Trim();
188 if (statOnly)
189 return GitArgsResult.Ok(["show", "--stat", r]);
190 if (!string.IsNullOrWhiteSpace(path))
191 return GitArgsResult.Ok(["show", r, "--", path.Trim()]);
192 return GitArgsResult.Ok(["show", r]);
193 }
194
195 public static GitArgsResult SubmoduleStatus() => GitArgsResult.Ok(["submodule", "status"]);
196
197 public static GitArgsResult SubmoduleUpdate(bool recursive, string? path)
198 {
199 var list = new List<string> { "submodule", "update", "--init" };
200 if (recursive)
201 list.Add("--recursive");
202 if (!string.IsNullOrWhiteSpace(path))
203 {
204 list.Add("--");
205 list.Add(path.Trim());
206 }
207 return GitArgsResult.Ok(list);
208 }
209}
210
View only · write via MCP/CIDE