Skip to content

Commit 8b868d5

Browse files
Mpdreamzclaude
andcommitted
Fix scope gaps, depth-protect git root discovery, consolidate API
Move checkout directories to ApplicationData: - AssembleContext: .artifacts/checkouts/{env} → ApplicationData/checkouts/{env} - CodexContext: .artifacts/codex/clone → ApplicationData/codex/clone Both now resolve within the existing ApplicationData scope root; no worktree detection or scope expansion needed. Paths.ResolveGitCommonRoot (IFileSystem) had no remaining callers and is removed along with its tests. Depth protection on git root discovery: - DetermineWorkingDirectoryRoot: only adopts a .git anchor ≤ 1 directory above CWD in release builds. Debug builds allow deeper traversal when a *.slnx is adjacent (developer running from IDE output directory). - FindGitRoot(string) gets the same depth limit — documentation is not expected to live deep inside a repo tree. Consolidate FindGitRoot / DetermineSourceDirectoryRoot: - DetermineSourceDirectoryRoot removed; FindGitRoot(IDirectoryInfo) overload added with the same semantics (IFileSystem-abstracted, same depth protection, returns IDirectoryInfo?). - Callers updated: BuildContext, ChangelogCommand, LocalChangesService. Rename ForPath/ForPathWrite → RealForPath/RealForPathWrite: - Signals these always create a real FileSystem. - Doc comment notes suitability for command layer; service layer is tested via InMemory() at unit test level. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent a420346 commit 8b868d5

16 files changed

Lines changed: 87 additions & 173 deletions

File tree

src/Elastic.Codex/CodexContext.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ public CodexContext(
4646
ReadFileSystem = readFileSystem;
4747
WriteFileSystem = writeFileSystem;
4848

49-
var workingRoot = ReadFileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
50-
var gitCommonRoot = Paths.ResolveGitCommonRoot(ReadFileSystem, workingRoot);
51-
var defaultCheckoutDirectory = Path.Join(gitCommonRoot.FullName, ".artifacts", "codex", "clone");
49+
var defaultCheckoutDirectory = Path.Join(Paths.ApplicationData.FullName, "codex", "clone");
5250
CheckoutDirectory = ReadFileSystem.DirectoryInfo.New(checkoutDirectory ?? defaultCheckoutDirectory);
5351

5452
var defaultOutputDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "codex", "docs");

src/Elastic.Documentation.Configuration/BuildContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public BuildContext(
108108

109109
(DocumentationSourceDirectory, ConfigurationPath) = Paths.FindDocsFolderFromRoot(ReadFileSystem, rootFolder);
110110

111-
DocumentationCheckoutDirectory = Paths.DetermineSourceDirectoryRoot(DocumentationSourceDirectory);
111+
DocumentationCheckoutDirectory = Paths.FindGitRoot(DocumentationSourceDirectory);
112112

113113
OutputDirectory = !string.IsNullOrWhiteSpace(output)
114114
? WriteFileSystem.DirectoryInfo.New(output)

src/Elastic.Documentation.Configuration/FileSystemFactory.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ public static class FileSystemFactory
3030
};
3131

3232
// AppData-only options: for components that only access caches/state files.
33-
private static readonly ScopedFileSystemOptions AppDataOptions = new(
34-
[Paths.ApplicationData.FullName])
33+
private static readonly ScopedFileSystemOptions AppDataOptions = new([Paths.ApplicationData.FullName])
3534
{
3635
// .git needed for codex-link-index clone directory inside ApplicationData
3736
AllowedHiddenFolderNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".git" }
@@ -103,7 +102,7 @@ public static ScopedFileSystem WrapToWrite(IFileSystem inner) =>
103102
/// <paramref name="path"/>. Falls back to <see cref="RealRead"/> when <paramref name="path"/>
104103
/// is <see langword="null"/>. Use in commands that accept an explicit <c>--path</c> argument.
105104
/// </summary>
106-
public static ScopedFileSystem ForPath(string? path)
105+
public static ScopedFileSystem RealForPath(string? path)
107106
{
108107
if (path is null)
109108
return RealRead;
@@ -121,7 +120,7 @@ public static ScopedFileSystem ForPath(string? path)
121120
/// Falls back to <see cref="RealWrite"/> when both are <see langword="null"/>.
122121
/// Use in commands that accept explicit <c>--path</c> and/or <c>--output</c> arguments.
123122
/// </summary>
124-
public static ScopedFileSystem ForPathWrite(string? path, string? output = null)
123+
public static ScopedFileSystem RealForPathWrite(string? path, string? output = null)
125124
{
126125
if (path is null && output is null)
127126
return RealWrite;

src/Elastic.Documentation.Configuration/Paths.cs

Lines changed: 70 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,86 +16,103 @@ public static class Paths
1616
/// <summary>
1717
/// Walks up from <paramref name="startPath"/> until a <c>.git</c> directory or file
1818
/// (worktree pointer) is found and returns that ancestor. Returns <paramref name="startPath"/>
19-
/// itself when no git root is found.
19+
/// itself when no git root is found within the allowed depth.
2020
/// </summary>
21+
/// <remarks>
22+
/// Depth protection: in release builds the <c>.git</c> anchor must be at most 1 directory
23+
/// above <paramref name="startPath"/> — documentation is not expected to live deep inside
24+
/// a repo. In debug builds a deeper <c>.git</c> is accepted when a <c>*.slnx</c> file is
25+
/// adjacent (developer running the binary from an IDE output directory).
26+
/// </remarks>
2127
public static string FindGitRoot(string startPath)
2228
{
2329
var resolved = Path.IsPathRooted(startPath) ? startPath : Path.GetFullPath(startPath);
2430
var dir = Directory.Exists(resolved)
2531
? new DirectoryInfo(resolved)
2632
: new DirectoryInfo(Path.GetDirectoryName(resolved) ?? resolved);
33+
var depth = 0;
2734
while (dir != null)
2835
{
29-
if (dir.GetDirectories(".git").Length > 0 || dir.GetFiles(".git").Length > 0)
30-
return dir.FullName;
36+
var hasGit = dir.GetDirectories(".git").Length > 0 || dir.GetFiles(".git").Length > 0;
37+
if (hasGit)
38+
{
39+
#if DEBUG
40+
if (depth <= 1 || dir.GetFiles("*.slnx").Length > 0)
41+
return dir.FullName;
42+
#else
43+
if (depth <= 1)
44+
return dir.FullName;
45+
#endif
46+
// .git found but too deep — stop searching
47+
return resolved;
48+
}
49+
depth++;
3150
dir = dir.Parent;
3251
}
3352
return resolved;
3453
}
3554

36-
private static DirectoryInfo DetermineWorkingDirectoryRoot()
55+
/// <summary>
56+
/// Walks up from <paramref name="startDirectory"/> via <see cref="IFileSystem"/> until
57+
/// a <c>.git</c> directory or file (worktree pointer) is found.
58+
/// Returns <see langword="null"/> if no git root is found within the allowed depth.
59+
/// </summary>
60+
/// <remarks>Same depth protection as <see cref="FindGitRoot(string)"/>.</remarks>
61+
public static IDirectoryInfo? FindGitRoot(IDirectoryInfo startDirectory)
3762
{
38-
var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
63+
var directory = startDirectory;
64+
var depth = 0;
3965
while (directory != null)
4066
{
41-
if (directory.GetFiles("*.slnx").Length > 0)
42-
break;
43-
if (directory.GetDirectories(".git").Length > 0)
44-
break;
45-
// support for git worktrees
46-
if (directory.GetFiles(".git").Length > 0)
47-
break;
67+
var hasGit = directory.GetDirectories(".git").Length > 0
68+
|| directory.GetFiles(".git").Length > 0;
69+
if (hasGit)
70+
{
71+
#if DEBUG
72+
if (depth <= 1 || directory.GetFiles("*.slnx").Length > 0)
73+
return directory;
74+
#else
75+
if (depth <= 1)
76+
return directory;
77+
#endif
78+
// .git found but too deep
79+
return null;
80+
}
81+
depth++;
4882
directory = directory.Parent;
4983
}
50-
return directory ?? new DirectoryInfo(Directory.GetCurrentDirectory());
84+
return null;
5185
}
5286

53-
/// <summary>
54-
/// Walks up from <paramref name="sourceDirectory"/> via <see cref="IFileSystem"/> until a
55-
/// <c>.git</c> directory or file (worktree pointer) is found.
56-
/// </summary>
57-
public static IDirectoryInfo? DetermineSourceDirectoryRoot(IDirectoryInfo sourceDirectory)
87+
private static DirectoryInfo DetermineWorkingDirectoryRoot()
5888
{
59-
var directory = sourceDirectory;
89+
var cwd = new DirectoryInfo(Directory.GetCurrentDirectory());
90+
var directory = cwd;
91+
var depth = 0;
6092
while (directory != null)
6193
{
62-
if (directory.GetDirectories(".git").Length > 0)
63-
return directory;
64-
// support for git worktrees
65-
if (directory.GetFiles(".git").Length > 0)
94+
if (directory.GetFiles("*.slnx").Length > 0)
6695
return directory;
96+
var hasGit = directory.GetDirectories(".git").Length > 0
97+
|| directory.GetFiles(".git").Length > 0;
98+
if (hasGit)
99+
{
100+
// Only accept .git beyond 1 level up in debug when a *.slnx is adjacent
101+
// (developer running from IDE output directory such as bin/Debug/net10.0/).
102+
#if DEBUG
103+
if (depth <= 1 || directory.GetFiles("*.slnx").Length > 0)
104+
return directory;
105+
#else
106+
if (depth <= 1)
107+
return directory;
108+
#endif
109+
// .git found but too deep — stop without adopting it
110+
return cwd;
111+
}
112+
depth++;
67113
directory = directory.Parent;
68114
}
69-
return null;
70-
}
71-
72-
/// <summary>Resolves the root of the main git repository, following worktree links when present. Disabled on CI.</summary>
73-
public static IDirectoryInfo ResolveGitCommonRoot(IFileSystem fileSystem, IDirectoryInfo workingDirectoryRoot, bool? isCI = null)
74-
{
75-
if (isCI ?? !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")))
76-
return workingDirectoryRoot;
77-
78-
var gitPath = Path.Join(workingDirectoryRoot.FullName, ".git");
79-
80-
if (fileSystem.Directory.Exists(gitPath))
81-
return workingDirectoryRoot;
82-
83-
if (!fileSystem.File.Exists(gitPath))
84-
return workingDirectoryRoot;
85-
86-
var content = fileSystem.File.ReadAllText(gitPath).Trim();
87-
if (!content.StartsWith("gitdir:", StringComparison.OrdinalIgnoreCase))
88-
return workingDirectoryRoot;
89-
90-
var gitDirPath = content["gitdir:".Length..].Trim();
91-
if (!Path.IsPathRooted(gitDirPath))
92-
gitDirPath = Path.GetFullPath(gitDirPath, workingDirectoryRoot.FullName);
93-
94-
var dir = fileSystem.DirectoryInfo.New(gitDirPath);
95-
while (dir != null && dir.Name != ".git")
96-
dir = dir.Parent;
97-
98-
return dir?.Parent ?? workingDirectoryRoot;
115+
return cwd;
99116
}
100117

101118
/// Used in debug to locate static folder, so we can change js/css files while the server is running

src/authoring/Elastic.Documentation.Refactor/Tracking/LocalChangesService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public Task<bool> ValidateRedirects(IDiagnosticsCollector collector, string? pat
3939
return Task.FromResult(false);
4040
}
4141

42-
var root = Paths.DetermineSourceDirectoryRoot(buildContext.DocumentationSourceDirectory);
42+
var root = Paths.FindGitRoot(buildContext.DocumentationSourceDirectory);
4343
if (root is null)
4444
{
4545
collector.EmitError(redirectFile.Source, $"Unable to determine the root of the source directory {buildContext.DocumentationSourceDirectory}.");

src/services/Elastic.Documentation.Assembler/AssembleContext.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,7 @@ public AssembleContext(
8989
Endpoints.Environment = environment;
9090

9191
var contentSource = Environment.ContentSource.ToStringFast(true);
92-
var workingRoot = ReadFileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
93-
var gitCommonRoot = Paths.ResolveGitCommonRoot(ReadFileSystem, workingRoot);
94-
var defaultCheckoutDirectory = Path.Join(gitCommonRoot.FullName, ".artifacts", "checkouts", contentSource);
92+
var defaultCheckoutDirectory = Path.Join(Paths.ApplicationData.FullName, "checkouts", contentSource);
9593
CheckoutDirectory = ReadFileSystem.DirectoryInfo.New(checkoutDirectory ?? defaultCheckoutDirectory);
9694
var defaultOutputDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly");
9795
OutputDirectory = ReadFileSystem.DirectoryInfo.New(output ?? defaultOutputDirectory);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public Task<int> Init(
9191

9292
var useNonDefaultChangelogDir = changelogDir != null;
9393
var useNonDefaultBundlesDir = bundlesDir != null;
94-
var repoRoot = Paths.DetermineSourceDirectoryRoot(docsFolder)?.FullName ?? docsFolder.FullName;
94+
var repoRoot = Paths.FindGitRoot(docsFolder)?.FullName ?? docsFolder.FullName;
9595

9696
// Create changelog.yml from example if it does not exist
9797
if (!_fileSystem.File.Exists(configPath))

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.ForPath(path);
32+
var fs = FileSystemFactory.RealForPath(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.ForPath(path);
46+
var fs = FileSystemFactory.RealForPath(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.ForPath(path);
89+
var fs = FileSystemFactory.RealForPath(path);
9090
var service = new IsolatedIndexService(logFactory, configurationContext, githubActionsService, environmentVariables);
9191
var state = (fs, path,
9292
// endpoint options

0 commit comments

Comments
 (0)