Forge
csharpdeeb25a2
1namespace CascadeIDE.Features.Agent.Environment;
2
3internal static class AgentDiagnoseFilesCsScopeParser
4{
5 public const string OpenTabsOnly = "open_tabs";
6
7 /// <summary>Открытые вкладки + отслеживаемые <c>.cs</c> из <c>git diff</c> (рабочее дерево + индекс).</summary>
8 public const string OpenTabsAndGitDirtyCs = "open_tabs_and_git_dirty_cs";
9
10 public static bool IncludesGitDirtyWorktreeCs(string? scope) =>
11 string.Equals(scope?.Trim(), OpenTabsAndGitDirtyCs, StringComparison.OrdinalIgnoreCase);
12
13 /// <summary>Объединяет несколько блоков stdout <c>git diff --name-only</c>; только непустые <c>.cs</c>, сохраняя порядок первого появления.</summary>
14 internal static IReadOnlyList<string> MergeGitNameOnlyOutputs(params string?[] blocks)
15 {
16 var ordered = new List<string>();
17 var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
18 foreach (var block in blocks)
19 {
20 if (string.IsNullOrWhiteSpace(block))
21 continue;
22 foreach (var line in block.Split(["\r\n", "\n", "\r"], StringSplitOptions.None))
23 {
24 var trimmed = line.Trim();
25 if (trimmed.Length == 0)
26 continue;
27 if (!trimmed.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
28 continue;
29 if (!seen.Add(trimmed))
30 continue;
31 ordered.Add(trimmed);
32 }
33 }
34
35 return ordered;
36 }
37
38 /// <summary>Относительный путь из git → абсолютный только если попадает в workspace.</summary>
39 internal static bool TryResolveWorkspaceCs(string workspaceRootFull, string relativeFromGit, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? fullPath)
40 {
41 fullPath = null;
42 if (relativeFromGit.Length == 0)
43 return false;
44
45 try
46 {
47 var collapsed = relativeFromGit.Replace('/', Path.DirectorySeparatorChar);
48 var root = Path.GetFullPath(workspaceRootFull).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
49 var candidate = Path.GetFullPath(Path.Combine(root, collapsed));
50 if (!IsStrictSubPathOrEqualFile(root, candidate))
51 return false;
52 if (!File.Exists(candidate))
53 return false;
54
55 fullPath = candidate;
56 return true;
57 }
58 catch
59 {
60 return false;
61 }
62 }
63
64 private static bool IsStrictSubPathOrEqualFile(string rootFull, string candidateFull)
65 {
66 var cmp = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
67
68 if (string.Equals(rootFull, candidateFull, cmp))
69 return false;
70
71 var prefix = rootFull + Path.DirectorySeparatorChar;
72 if (!candidateFull.StartsWith(prefix, cmp))
73 return false;
74
75 try
76 {
77 return Path.GetRelativePath(rootFull, candidateFull)
78 .Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
79 .All(s => !string.Equals(s, "..", cmp));
80 }
81 catch
82 {
83 return false;
84 }
85 }
86}
87
View only · write via MCP/CIDE