| 1 | using System.Security.Cryptography; |
| 2 | using System.Text; |
| 3 | using CascadeIDE.Services; |
| 4 | |
| 5 | namespace CascadeIDE.Features.Agent.Environment; |
| 6 | |
| 7 | public static class VerifySnapshot |
| 8 | { |
| 9 | public static string Create(string solutionPath) => |
| 10 | Create(solutionPath, git: null, workspaceRoot: null); |
| 11 | |
| 12 | public static string Create( |
| 13 | string solutionPath, |
| 14 | IGitCommandRunner? git, |
| 15 | string? workspaceRoot) |
| 16 | { |
| 17 | var normalized = Path.GetFullPath(solutionPath.Trim()); |
| 18 | var parts = new List<string> { normalized }; |
| 19 | |
| 20 | if (git is not null |
| 21 | && !string.IsNullOrWhiteSpace(workspaceRoot) |
| 22 | && Directory.Exists(workspaceRoot)) |
| 23 | { |
| 24 | try |
| 25 | { |
| 26 | parts.AddRange(CollectGitParts(git, workspaceRoot)); |
| 27 | } |
| 28 | catch |
| 29 | { |
| 30 | parts.Add("tick:" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); |
| 31 | } |
| 32 | } |
| 33 | else |
| 34 | { |
| 35 | parts.Add("tick:" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); |
| 36 | } |
| 37 | |
| 38 | var payload = string.Join("|", parts); |
| 39 | var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(payload)))[..16]; |
| 40 | return hash; |
| 41 | } |
| 42 | |
| 43 | private static List<string> CollectGitParts(IGitCommandRunner git, string workspaceRoot) => |
| 44 | CollectGitPartsCore(git, workspaceRoot); |
| 45 | |
| 46 | private static List<string> CollectGitPartsCore(IGitCommandRunner git, string workspaceRoot) |
| 47 | { |
| 48 | var parts = new List<string>(); |
| 49 | var head = TryGitHead(git, workspaceRoot); |
| 50 | if (head is not null) |
| 51 | parts.Add("head:" + head); |
| 52 | |
| 53 | var dirty = TryGitDirtyFingerprint(git, workspaceRoot); |
| 54 | if (dirty is not null) |
| 55 | parts.Add("dirty:" + dirty); |
| 56 | |
| 57 | return parts; |
| 58 | } |
| 59 | |
| 60 | private static string? TryGitHead(IGitCommandRunner git, string workspaceRoot) |
| 61 | { |
| 62 | try |
| 63 | { |
| 64 | var wd = Path.GetFullPath(workspaceRoot); |
| 65 | var (ok, _, output) = git.RunAsync(["-c", "core.quotepath=false", "rev-parse", "HEAD"], wd) |
| 66 | .GetAwaiter().GetResult(); |
| 67 | if (!ok) |
| 68 | return null; |
| 69 | |
| 70 | var head = output.Trim(); |
| 71 | return head.Length >= 7 ? head[..7] : head; |
| 72 | } |
| 73 | catch |
| 74 | { |
| 75 | return null; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | private static string? TryGitDirtyFingerprint(IGitCommandRunner git, string workspaceRoot) |
| 80 | { |
| 81 | try |
| 82 | { |
| 83 | var wd = Path.GetFullPath(workspaceRoot); |
| 84 | var unstaged = git.RunAsync(["-c", "core.quotepath=false", "diff", "--name-only"], wd) |
| 85 | .GetAwaiter().GetResult(); |
| 86 | var staged = git.RunAsync(["-c", "core.quotepath=false", "diff", "--name-only", "--cached"], wd) |
| 87 | .GetAwaiter().GetResult(); |
| 88 | if (!unstaged.Success && !staged.Success) |
| 89 | return null; |
| 90 | |
| 91 | var merged = AgentDiagnoseFilesCsScopeParser.MergeGitNameOnlyOutputs(unstaged.Output, staged.Output); |
| 92 | if (merged.Count == 0) |
| 93 | return "clean"; |
| 94 | |
| 95 | var payload = string.Join("\n", merged.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)); |
| 96 | return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(payload)))[..12]; |
| 97 | } |
| 98 | catch |
| 99 | { |
| 100 | return null; |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | |