Skip to content

Commit a385472

Browse files
Mpdreamzclaude
andcommitted
Scope file systems to the path argument rather than CWD
RealRead/RealWrite are pre-allocated singletons scoped to Paths.WorkingDirectoryRoot (the CWD git root). Commands that accept an explicit --path or --output argument may target a repo outside that root, which would cause ScopedFileSystemException. Add FileSystemFactory.ForPath(string? path) and ForPathWrite(string? path, string? output) that derive the scope root dynamically by walking up from the given path to find its .git boundary. Both fall back to RealRead/RealWrite when no path is provided (CWD-relative operation). ForPathWrite also adds the output directory as an extra scope root when it falls outside the git root. Update all commands that accept --path/--output to use these: IndexCommand, IsolatedBuildCommand, ServeCommand, FormatCommand, MoveCommand, DiffCommands, InMemoryBuildState, StaticWebHost. Commands that always operate relative to CWD (assembler, codex, changelog) continue using RealRead since their scope is determined by assembler config or git context, not an arbitrary user-provided path. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent f0782e4 commit a385472

9 files changed

Lines changed: 71 additions & 9 deletions

File tree

src/Elastic.Documentation.Configuration/FileSystemFactory.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,66 @@ public static ScopedFileSystem WrapToRead(IFileSystem inner, IEnumerable<string>
9797
/// <summary>Wraps <paramref name="inner"/> with write workspace options (.git not allowed).</summary>
9898
public static ScopedFileSystem WrapToWrite(IFileSystem inner) =>
9999
new(inner, WriteOptions);
100+
101+
/// <summary>
102+
/// Creates a read <see cref="ScopedFileSystem"/> scoped to the git root of
103+
/// <paramref name="path"/>. Falls back to <see cref="RealRead"/> when <paramref name="path"/>
104+
/// is <see langword="null"/>. Use in commands that accept an explicit <c>--path</c> argument.
105+
/// </summary>
106+
public static ScopedFileSystem ForPath(string? path)
107+
{
108+
if (path is null)
109+
return RealRead;
110+
var root = FindGitRoot(path);
111+
return new ScopedFileSystem(new FileSystem(), new ScopedFileSystemOptions([root, Paths.ApplicationData.FullName])
112+
{
113+
AllowedHiddenFolderNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
114+
AllowedHiddenFileNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state" }
115+
});
116+
}
117+
118+
/// <summary>
119+
/// Creates a write <see cref="ScopedFileSystem"/> scoped to the git root of
120+
/// <paramref name="path"/> (and <paramref name="output"/> if it falls outside that root).
121+
/// Falls back to <see cref="RealWrite"/> when both are <see langword="null"/>.
122+
/// Use in commands that accept explicit <c>--path</c> and/or <c>--output</c> arguments.
123+
/// </summary>
124+
public static ScopedFileSystem ForPathWrite(string? path, string? output = null)
125+
{
126+
if (path is null && output is null)
127+
return RealWrite;
128+
129+
var gitRoot = path is not null ? FindGitRoot(path) : Paths.WorkingDirectoryRoot.FullName;
130+
var roots = new List<string> { gitRoot, Paths.ApplicationData.FullName };
131+
132+
if (output is not null)
133+
{
134+
var absOutput = Path.IsPathRooted(output) ? output : Path.GetFullPath(output);
135+
if (!absOutput.StartsWith(gitRoot, StringComparison.OrdinalIgnoreCase))
136+
roots.Add(absOutput);
137+
}
138+
139+
return new ScopedFileSystem(new FileSystem(), new ScopedFileSystemOptions([.. roots])
140+
{
141+
AllowedHiddenFolderNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".artifacts" },
142+
AllowedHiddenFileNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".doc.state" }
143+
});
144+
}
145+
146+
// Walks up from startPath to find the nearest .git directory or file (worktree support).
147+
// Uses System.IO directly — acceptable bootstrap, same pattern as Paths.DetermineWorkingDirectoryRoot.
148+
private static string FindGitRoot(string startPath)
149+
{
150+
var resolved = Path.IsPathRooted(startPath) ? startPath : Path.GetFullPath(startPath);
151+
var dir = Directory.Exists(resolved)
152+
? new DirectoryInfo(resolved)
153+
: new DirectoryInfo(Path.GetDirectoryName(resolved) ?? resolved);
154+
while (dir != null)
155+
{
156+
if (dir.GetDirectories(".git").Length > 0 || dir.GetFiles(".git").Length > 0)
157+
return dir.FullName;
158+
dir = dir.Parent;
159+
}
160+
return Path.GetPathRoot(resolved) ?? resolved;
161+
}
100162
}

src/tooling/docs-builder/Commands/DiffCommands.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public async Task<int> ValidateRedirects(string? path = null, Cancel ctx = defau
2929
await using var serviceInvoker = new ServiceInvoker(collector);
3030

3131
var service = new LocalChangeTrackingService(logFactory, configurationContext);
32-
var fs = FileSystemFactory.RealRead;
32+
var fs = FileSystemFactory.ForPath(path);
3333

3434
serviceInvoker.AddCommand(service, (path, fs),
3535
async static (s, collector, state, _) => await s.ValidateRedirects(collector, state.path, state.fs)

src/tooling/docs-builder/Commands/FormatCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public async Task<int> Format(
4343
await using var serviceInvoker = new ServiceInvoker(collector);
4444

4545
var service = new FormatService(logFactory, configurationContext);
46-
var fs = FileSystemFactory.RealRead;
46+
var fs = FileSystemFactory.ForPath(path);
4747

4848
serviceInvoker.AddCommand(service, (path, check, fs),
4949
async static (s, collector, state, ctx) => await s.Format(collector, state.path, state.check, state.fs, ctx)

src/tooling/docs-builder/Commands/IndexCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public async Task<int> Index(
8686
)
8787
{
8888
await using var serviceInvoker = new ServiceInvoker(collector);
89-
var fs = FileSystemFactory.RealRead;
89+
var fs = FileSystemFactory.ForPath(path);
9090
var service = new IsolatedIndexService(logFactory, configurationContext, githubActionsService, environmentVariables);
9191
var state = (fs, path,
9292
// endpoint options

src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ public async Task<int> Build(
6262
await using var serviceInvoker = new ServiceInvoker(collector);
6363

6464
var service = new IsolatedBuildService(logFactory, configurationContext, githubActionsService, environmentVariables);
65-
var readFs = inMemory ? FileSystemFactory.InMemory() : FileSystemFactory.RealRead;
65+
var readFs = inMemory ? FileSystemFactory.InMemory() : FileSystemFactory.ForPath(path);
6666
// For real builds supply an explicit write FS without .git access; for in-memory null falls back to readFs
67-
var writeFs = inMemory ? null : FileSystemFactory.RealWrite;
67+
var writeFs = inMemory ? null : FileSystemFactory.ForPathWrite(path, output);
6868
var strictCommand = service.IsStrict(strict);
6969

7070
serviceInvoker.AddCommand(service,

src/tooling/docs-builder/Commands/MoveCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public async Task<int> Move(
3838
await using var serviceInvoker = new ServiceInvoker(collector);
3939

4040
var service = new MoveFileService(logFactory, configurationContext);
41-
var fs = FileSystemFactory.RealRead;
41+
var fs = FileSystemFactory.ForPath(path);
4242

4343
serviceInvoker.AddCommand(service, (source, target, dryRun, path, fs),
4444
async static (s, collector, state, ctx) => await s.Move(collector, state.source, state.target, state.dryRun, state.path, state.fs, ctx)

src/tooling/docs-builder/Commands/ServeCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal sealed class ServeCommand(ILoggerFactory logFactory, IConfigurationCont
2727
[Command("")]
2828
public async Task Serve(string? path = null, int port = 3000, bool watch = false, Cancel ctx = default)
2929
{
30-
var host = new DocumentationWebHost(logFactory, path, port, FileSystemFactory.RealRead, FileSystemFactory.InMemory(), configurationContext, watch);
30+
var host = new DocumentationWebHost(logFactory, path, port, FileSystemFactory.ForPath(path), FileSystemFactory.InMemory(), configurationContext, watch);
3131
await host.RunAsync(ctx);
3232
_logger.LogInformation("Find your documentation at http://localhost:{Port}/{Path}", port,
3333
host.GeneratorState.Generator.DocumentationSet.FirstInterestingUrl.TrimStart('/')

src/tooling/docs-builder/Http/InMemoryBuildState.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ private async Task ExecuteBuildAsync(string sourcePath, Cancel ct)
169169
// Create a diagnostics collector that streams to our channel
170170
var streamingCollector = new StreamingDiagnosticsCollector(_loggerFactory, this);
171171

172-
var readFs = FileSystemFactory.RealRead;
172+
var readFs = FileSystemFactory.ForPath(sourcePath);
173173
var service = new IsolatedBuildService(_loggerFactory, _configurationContext, new NullCoreService(), SystemEnvironmentVariables.Instance);
174174

175175
_logger.LogInformation("Starting in-memory validation build for {Path}", sourcePath);

src/tooling/docs-builder/Http/StaticWebHost.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class StaticWebHost
2525
public StaticWebHost(int port, string? path)
2626
{
2727
_contentRoot = path ?? Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly");
28-
var fs = FileSystemFactory.RealRead;
28+
var fs = FileSystemFactory.ForPath(path);
2929
var dir = fs.DirectoryInfo.New(_contentRoot);
3030
if (!dir.Exists)
3131
throw new Exception($"Can not serve empty directory: {_contentRoot}");

0 commit comments

Comments
 (0)