-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWorktreeHelper.cs
More file actions
167 lines (141 loc) · 5.95 KB
/
WorktreeHelper.cs
File metadata and controls
167 lines (141 loc) · 5.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
using System.Diagnostics;
namespace CodeAtlas;
/// <summary>
/// Temporary git worktree at the MR branch head so Roslyn can see newly created files.
/// WorktreePath = root of the worktree checkout (git root equivalent).
/// OriginalRepoPath = the git root of the untouched repo — run glab/git diff from here.
/// SubDirectory = relative path from git root to the --repo dir (e.g. "Application").
/// Returns null from CreateAsync on failure (caller falls back to current behavior).
/// </summary>
public sealed class WorktreeHelper : IAsyncDisposable
{
public string WorktreePath { get; }
public string OriginalRepoPath { get; }
public string SubDirectory { get; }
public string WorktreeSubPath => string.IsNullOrEmpty(SubDirectory)
? WorktreePath
: Path.Combine(WorktreePath, SubDirectory);
private WorktreeHelper(string worktreePath, string originalRepoPath, string subDirectory)
{
WorktreePath = worktreePath;
OriginalRepoPath = originalRepoPath;
SubDirectory = subDirectory;
}
public static async Task<WorktreeHelper?> CreateAsync(string repoPath, string branchOrMrId)
{
var fullRepoPath = Path.GetFullPath(repoPath);
var gitRoot = await GetGitRootAsync(fullRepoPath);
if (gitRoot is null)
{
Console.Error.WriteLine(" Worktree: not a git repository, falling back.");
return null;
}
var subDir = Path.GetRelativePath(gitRoot, fullRepoPath);
if (subDir == ".") subDir = "";
await RunGitAsync(gitRoot, "worktree prune");
string? checkoutRef = null;
if (int.TryParse(branchOrMrId, out var mrIid))
{
// GitLab exposes every MR as refs/merge-requests/<iid>/head — no local branch needed
var fetchRef = $"refs/merge-requests/{mrIid}/head";
Console.Error.WriteLine($" Worktree: fetching MR ref {fetchRef}...");
var (fetchOk, _) = await RunGitAsync(gitRoot, $"fetch origin {fetchRef}");
if (!fetchOk)
{
Console.Error.WriteLine(" Worktree: failed to fetch MR ref, falling back.");
return null;
}
checkoutRef = "FETCH_HEAD";
}
else
{
Console.Error.WriteLine($" Worktree: fetching branch origin/{branchOrMrId}...");
var (fetchOk, _) = await RunGitAsync(gitRoot, $"fetch origin {branchOrMrId}");
if (!fetchOk)
{
Console.Error.WriteLine($" Worktree: failed to fetch origin/{branchOrMrId}, falling back.");
return null;
}
checkoutRef = $"origin/{branchOrMrId}";
}
var tmpDir = Path.Combine(Path.GetTempPath(), $"codeatlas-{branchOrMrId}-{Guid.NewGuid().ToString()[..8]}");
Console.Error.WriteLine($" Worktree: creating at {tmpDir}...");
var (addOk, addErr) = await RunGitAsync(gitRoot, $"worktree add \"{tmpDir}\" {checkoutRef} --detach");
if (!addOk)
{
Console.Error.WriteLine($" Worktree: failed to create worktree: {addErr}");
TryDeleteDirectory(tmpDir);
return null;
}
Console.Error.WriteLine($" Worktree: ready at {tmpDir}" + (subDir != "" ? $" (subdir: {subDir})" : ""));
return new WorktreeHelper(tmpDir, gitRoot, subDir);
}
public async ValueTask DisposeAsync()
{
Console.Error.WriteLine($" Worktree: cleaning up {WorktreePath}...");
var (ok, _) = await RunGitAsync(OriginalRepoPath, $"worktree remove \"{WorktreePath}\" --force");
if (!ok)
{
TryDeleteDirectory(WorktreePath);
await RunGitAsync(OriginalRepoPath, "worktree prune");
}
}
private static async Task<string?> GetGitRootAsync(string workDir)
{
var (ok, stdout) = await RunGitWithOutputAsync(workDir, "rev-parse --show-toplevel");
return ok ? stdout.Trim() : null;
}
private static async Task<(bool Success, string Stderr)> RunGitAsync(string workDir, string arguments)
{
var (success, _, stderr) = await RunGitCoreAsync(workDir, arguments);
return (success, stderr);
}
private static async Task<(bool Success, string Stdout)> RunGitWithOutputAsync(string workDir, string arguments)
{
var (success, stdout, _) = await RunGitCoreAsync(workDir, arguments);
return (success, stdout);
}
private static async Task<(bool Success, string Stdout, string Stderr)> RunGitCoreAsync(string workDir, string arguments)
{
try
{
var psi = new ProcessStartInfo("git", arguments)
{
WorkingDirectory = workDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process is null) return (false, "", "Failed to start git process");
var stdoutTask = process.StandardOutput.ReadToEndAsync();
var stderrTask = process.StandardError.ReadToEndAsync();
using var cts = new CancellationTokenSource(Defaults.WorktreeTimeout);
await process.WaitForExitAsync(cts.Token);
var stdout = await stdoutTask;
var stderr = await stderrTask;
return (process.ExitCode == 0, stdout.Trim(), stderr.Trim());
}
catch (OperationCanceledException)
{
return (false, "", "Timed out after 60s");
}
catch (Exception ex)
{
return (false, "", ex.Message);
}
}
private static void TryDeleteDirectory(string path)
{
try
{
if (Directory.Exists(path))
Directory.Delete(path, recursive: true);
}
catch (Exception ex)
{
Console.Error.WriteLine($" Worktree: failed to delete {path}: {ex.Message}");
}
}
}