diff --git a/Directory.Packages.props b/Directory.Packages.props
index a19d2e2a75..ef0e163115 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -46,7 +46,7 @@
-
+
diff --git a/src/Elastic.Codex/Building/CodexBuildService.cs b/src/Elastic.Codex/Building/CodexBuildService.cs
index 2b4faacb6f..ad361ed781 100644
--- a/src/Elastic.Codex/Building/CodexBuildService.cs
+++ b/src/Elastic.Codex/Building/CodexBuildService.cs
@@ -14,6 +14,7 @@
using Elastic.Documentation.Configuration.Codex;
using Elastic.Documentation.Configuration.ReleaseNotes;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Isolated;
using Elastic.Documentation.LinkIndex;
using Elastic.Documentation.Links;
@@ -69,7 +70,7 @@ public async Task BuildAll(
var buildContexts = new List();
var environment = context.Configuration.Environment ?? "internal";
- using var codexLinkIndexReader = new GitLinkIndexReader(environment, FileSystemFactory.AppData, skipFetch: true);
+ using var codexLinkIndexReader = new GitLinkIndexReader(environment, new ApplicationDataFileSystem(), skipFetch: true);
// Phase 1: Load and parse all documentation sets
foreach (var checkout in cloneResult.Checkouts)
@@ -187,16 +188,13 @@ public async Task BuildAll(
// The docset file itself is passed explicitly so build reuses the docset clone discovery already
// selected (which may prefer a non-default path such as `docs-dev/`), rather than rediscovering
// one from the repository root and always landing on `docs/`.
- var buildContext = new BuildContext(
- context.Collector,
- fileSystem,
- fileSystem,
- configurationContext,
- ExportOptions.Default,
- checkout.RepositoryDirectory.FullName,
- outputPath,
- git,
- configurationFile: checkout.DocsetFile)
+ var docFs = DocumentationFileSystem.Resolve(checkout.RepositoryDirectory, new DocumentationScopeOptions
+ {
+ Output = outputPath,
+ Git = git,
+ ConfigurationFile = checkout.DocsetFile.FullName,
+ });
+ var buildContext = new BuildContext(context.Collector, docFs, configurationContext)
{
UrlPathPrefix = pathPrefix,
SiteRootPath = siteRootPath,
@@ -414,10 +412,7 @@ internal sealed class CodexDocumentationContext(CodexContext codexContext) : ICo
public IDiagnosticsCollector Collector => codexContext.Collector;
///
- public ScopedFileSystem ReadFileSystem => codexContext.ReadFileSystem;
-
- ///
- public ScopedFileSystem WriteFileSystem => codexContext.WriteFileSystem;
+ public DocumentationWriteFileSystem WriteFileSystem => codexContext.WriteFileSystem;
///
public IDirectoryInfo OutputDirectory => codexContext.OutputDirectory;
diff --git a/src/Elastic.Codex/CodexContext.cs b/src/Elastic.Codex/CodexContext.cs
index 327a9743af..ef99f62a62 100644
--- a/src/Elastic.Codex/CodexContext.cs
+++ b/src/Elastic.Codex/CodexContext.cs
@@ -7,7 +7,7 @@
using Elastic.Documentation.Configuration.Codex;
using Elastic.Documentation.Deploying.Synchronization;
using Elastic.Documentation.Diagnostics;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Codex;
@@ -16,8 +16,8 @@ namespace Elastic.Codex;
///
public class CodexContext : IDocsSyncContext
{
- public ScopedFileSystem ReadFileSystem { get; }
- public ScopedFileSystem WriteFileSystem { get; }
+ public CheckoutsFileSystem ReadFileSystem { get; }
+ public DocumentationWriteFileSystem WriteFileSystem { get; }
public IDiagnosticsCollector Collector { get; }
public CodexConfiguration Configuration { get; }
public IFileInfo ConfigurationPath { get; }
@@ -37,23 +37,23 @@ public CodexContext(
CodexConfiguration configuration,
IFileInfo configurationPath,
IDiagnosticsCollector collector,
- ScopedFileSystem readFileSystem,
- ScopedFileSystem writeFileSystem,
- string? checkoutDirectory,
- string? outputDirectory)
+ CheckoutsFileSystem fileSystem,
+ string? checkoutDirectory = null,
+ string? outputDirectory = null
+ )
{
Configuration = configuration;
ConfigurationPath = configurationPath;
Collector = collector;
- ReadFileSystem = readFileSystem;
- WriteFileSystem = writeFileSystem;
+ ReadFileSystem = fileSystem;
+ WriteFileSystem = fileSystem.Write;
EnvironmentName = string.IsNullOrEmpty(configuration.Environment) ? "codex" : configuration.Environment;
var defaultCheckoutDirectory = Path.Join(Paths.ApplicationData.FullName, "codex", "clone");
CheckoutDirectory = checkoutDirectory is null
- ? FileSystemFactory.AppData.DirectoryInfo.New(defaultCheckoutDirectory)
- : ReadFileSystem.DirectoryInfo.New(checkoutDirectory);
+ ? fileSystem.DirectoryInfo.New(defaultCheckoutDirectory)
+ : fileSystem.DirectoryInfo.New(checkoutDirectory);
var defaultOutputDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "codex", "docs");
OutputDirectory = WriteFileSystem.DirectoryInfo.New(outputDirectory ?? defaultOutputDirectory);
diff --git a/src/Elastic.Codex/CodexFileSystem.cs b/src/Elastic.Codex/CodexFileSystem.cs
new file mode 100644
index 0000000000..4e1b16e8bf
--- /dev/null
+++ b/src/Elastic.Codex/CodexFileSystem.cs
@@ -0,0 +1,41 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
+
+namespace Elastic.Codex;
+
+///
+/// Scope over the working directory for codex operations. Rooted at the process working directory
+/// with the config file's git root added as an extra allowed root, so the config file and any
+/// paths inside its repository are always readable.
+///
+/// Use this in codex commands instead of constructing a manually.
+///
+///
+public class CodexFileSystem : CheckoutsFileSystem
+{
+ private static readonly FileSystem Physical = new();
+
+ /// The codex configuration file, resolved through this scoped filesystem.
+ public IFileInfo ConfigurationFile { get; }
+
+ /// Full path to the codex configuration file. Its directory is used to locate the git root.
+ /// Optional explicit output directory.
+ /// Underlying filesystem — defaults to the physical filesystem when .
+ public CodexFileSystem(string config, string? output = null, IFileSystem? inner = null)
+ : this(inner ?? Physical, config, output, inner)
+ { }
+
+ private CodexFileSystem(IFileSystem fs, string config, string? output, IFileSystem? inner)
+ : base(
+ root: fs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName),
+ output: output is not null ? fs.DirectoryInfo.New(output) : null,
+ inner: inner,
+ extraRoots: [Paths.FindGitRoot(fs.DirectoryInfo.New(fs.Path.GetDirectoryName(config)!))?.FullName ?? fs.Path.GetDirectoryName(config)!]
+ )
+ => ConfigurationFile = FileInfo.New(config);
+}
diff --git a/src/Elastic.Documentation.Configuration/BuildContext.cs b/src/Elastic.Documentation.Configuration/BuildContext.cs
index 1d0a285bda..d4337ac18a 100644
--- a/src/Elastic.Documentation.Configuration/BuildContext.cs
+++ b/src/Elastic.Documentation.Configuration/BuildContext.cs
@@ -13,6 +13,7 @@
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Configuration;
@@ -22,46 +23,42 @@ public record BuildContext : IDocumentationSetContext, IDocumentationConfigurati
public static string Version { get; } = Assembly.GetExecutingAssembly().GetCustomAttributes()
.FirstOrDefault()?.InformationalVersion ?? "0.0.0";
- public ScopedFileSystem ReadFileSystem { get; }
- public ScopedFileSystem WriteFileSystem { get; }
- public IReadOnlySet AvailableExporters { get; }
+ /// The resolved documentation filesystem. All other path/scope properties are computed from this.
+ public DocumentationFileSystem FileSystem { get; }
- public IDirectoryInfo? DocumentationCheckoutDirectory { get; }
- public IDirectoryInfo DocumentationSourceDirectory { get; }
- public IDirectoryInfo OutputDirectory { get; }
+ ///
+ /// Read scope. Satisfies .
+ /// Use directly when the richer type is needed.
+ ///
+ public IDocumentationFileSystem ReadFileSystem => FileSystem.Read;
- public ConfigurationFile Configuration { get; private set; }
+ /// Write scope. Does not permit .git writes.
+ public DocumentationWriteFileSystem WriteFileSystem => FileSystem.Write;
+
+ public IReadOnlySet AvailableExporters { get; init; }
+ public IDirectoryInfo DocumentationCheckoutDirectory => FileSystem.Paths.CheckoutDirectory;
+ public IDirectoryInfo DocumentationSourceDirectory => FileSystem.Paths.SourceDirectory;
+ public IDirectoryInfo OutputDirectory => FileSystem.Paths.OutputDirectory;
+ public IFileInfo ConfigurationPath => FileSystem.Paths.ConfigurationPath;
+ public GitCheckoutInformation Git => FileSystem.Paths.Git;
+
+ public ConfigurationFile Configuration { get; private set; }
public DocumentationSetFile ConfigurationYaml { get; set; }
public VersionsConfiguration VersionsConfiguration { get; }
public ConfigurationFileProvider ConfigurationFileProvider { get; }
public DocumentationEndpoints Endpoints { get; }
-
public ProductsConfiguration ProductsConfiguration { get; }
public LegacyUrlMappingConfiguration LegacyUrlMappings { get; }
public SearchConfiguration SearchConfiguration { get; }
-
- public IFileInfo ConfigurationPath { get; }
-
- public GitCheckoutInformation Git { get; }
-
public IEnvironmentVariables Environment { get; }
-
public IDiagnosticsCollector Collector { get; }
-
public bool Force { get; init; }
-
public BuildType BuildType { get; init; } = BuildType.Isolated;
-
- // This property is used to determine if the site should be indexed by search engines
public bool AllowIndexing { get; init; }
-
public GoogleTagManagerConfiguration GoogleTagManager { get; init; }
-
public OptimizelyConfiguration Optimizely { get; init; }
-
- // This property is used for the canonical URL
public Uri? CanonicalBaseUrl { get; init; }
public string? UrlPathPrefix
@@ -73,33 +70,20 @@ public string? UrlPathPrefix
/// Site root path for HTMX (e.g. codex root). When set, overrides derivation from UrlPathPrefix.
public string? SiteRootPath { get; init; }
+ ///
+ /// Primary constructor. Pass a resolved from
+ /// .
+ ///
public BuildContext(
IDiagnosticsCollector collector,
- ScopedFileSystem fileSystem,
+ DocumentationFileSystem fileSystem,
IConfigurationContext configurationContext,
IEnvironmentVariables? environment = null
- )
- : this(collector, fileSystem, fileSystem, configurationContext, ExportOptions.Default, null, null, environment: environment)
- {
- }
-
- public BuildContext(
- IDiagnosticsCollector collector,
- ScopedFileSystem readFileSystem,
- ScopedFileSystem writeFileSystem,
- IConfigurationContext configurationContext,
- IReadOnlySet availableExporters,
- string? source = null,
- string? output = null,
- GitCheckoutInformation? gitCheckoutInformation = null,
- IEnvironmentVariables? environment = null,
- IFileInfo? configurationFile = null
)
{
Collector = collector;
- ReadFileSystem = readFileSystem;
- WriteFileSystem = writeFileSystem;
- AvailableExporters = availableExporters;
+ FileSystem = fileSystem;
+ AvailableExporters = ExportOptions.Default;
Environment = environment ?? SystemEnvironmentVariables.Instance;
SearchConfiguration = configurationContext.SearchConfiguration;
VersionsConfiguration = configurationContext.VersionsConfiguration;
@@ -108,42 +92,14 @@ public BuildContext(
LegacyUrlMappings = configurationContext.LegacyUrlMappings;
Endpoints = configurationContext.Endpoints;
- var rootFolder = !string.IsNullOrWhiteSpace(source)
- ? ReadFileSystem.DirectoryInfo.New(source)
- : ReadFileSystem.DirectoryInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName));
-
- // When the caller already discovered the docset (e.g. Codex clone discovery, which may prefer
- // a non-default docset such as `docs-dev/` over `docs/`), use it directly instead of letting
- // Paths.FindDocsFolderFromRoot rediscover a different one from `source`.
- (DocumentationSourceDirectory, ConfigurationPath) = configurationFile is not null
- ? (configurationFile.Directory!, configurationFile)
- : Paths.FindDocsFolderFromRoot(ReadFileSystem, rootFolder);
-
- DocumentationCheckoutDirectory = Paths.FindGitRoot(DocumentationSourceDirectory, ceiling: rootFolder);
-
- OutputDirectory = !string.IsNullOrWhiteSpace(output)
- ? WriteFileSystem.DirectoryInfo.New(output)
- : WriteFileSystem.DirectoryInfo.New(Path.Join(rootFolder.FullName, Path.Join(".artifacts", "docs", "html")));
-
- if (ConfigurationPath.FullName != DocumentationSourceDirectory.FullName)
- DocumentationSourceDirectory = ConfigurationPath.Directory!;
-
- Git = gitCheckoutInformation ?? GitCheckoutInformationFactory.Create(DocumentationCheckoutDirectory, ReadFileSystem);
+ GoogleTagManager = new GoogleTagManagerConfiguration { Enabled = false };
+ Optimizely = new OptimizelyConfiguration { Enabled = false };
- // Load and resolve the docset file, or create an empty one if it doesn't exist
ConfigurationYaml = ConfigurationPath.Exists
- ? DocumentationSetFile.LoadAndResolve(collector, ConfigurationPath, readFileSystem)
+ ? DocumentationSetFile.LoadAndResolve(collector, ConfigurationPath, fileSystem.Read)
: new DocumentationSetFile();
Configuration = new ConfigurationFile(ConfigurationYaml, this, VersionsConfiguration, ProductsConfiguration);
- GoogleTagManager = new GoogleTagManagerConfiguration
- {
- Enabled = false
- };
- Optimizely = new OptimizelyConfiguration
- {
- Enabled = false
- };
}
/// Re-reads docset.yml from disk and rebuilds the configuration. Used by the serve command on file changes.
@@ -151,7 +107,7 @@ public void ReloadConfiguration()
{
var previousFeatures = Configuration.Features;
ConfigurationYaml = ConfigurationPath.Exists
- ? DocumentationSetFile.LoadAndResolve(Collector, ConfigurationPath, ReadFileSystem)
+ ? DocumentationSetFile.LoadAndResolve(Collector, ConfigurationPath, ReadFileSystem as ScopedFileSystem)
: new DocumentationSetFile();
Configuration = new ConfigurationFile(ConfigurationYaml, this, VersionsConfiguration, ProductsConfiguration);
Configuration.Features.DiagnosticsPanelEnabled = previousFeatures.DiagnosticsPanelEnabled;
diff --git a/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs b/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs
index 45c7b676b5..fe7a508013 100644
--- a/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs
+++ b/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs
@@ -8,6 +8,7 @@
using Elastic.Documentation.Configuration.Converters;
using Elastic.Documentation.Configuration.Serialization;
using Elastic.Documentation.Configuration.Toc;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using YamlDotNet.Serialization;
@@ -17,7 +18,7 @@ namespace Elastic.Documentation.Configuration;
public partial class ConfigurationFileProvider
{
- private readonly IFileSystem _fileSystem;
+ private readonly IAppDataFileSystem _fileSystem;
private readonly string _assemblyName;
private readonly ILogger _logger;
@@ -37,7 +38,7 @@ public partial class ConfigurationFileProvider
public ConfigurationFileProvider(
ILoggerFactory logFactory,
- IFileSystem fileSystem,
+ IAppDataFileSystem fileSystem,
bool skipPrivateRepositories = false,
ConfigurationSource? configurationSource = null
)
@@ -272,7 +273,7 @@ public static IServiceCollection AddConfigurationFileProvider(this IServiceColle
{
using var sp = services.BuildServiceProvider();
var logFactory = sp.GetRequiredService();
- var provider = new ConfigurationFileProvider(logFactory, FileSystemFactory.RealRead, skipPrivateRepositories, configurationSource);
+ var provider = new ConfigurationFileProvider(logFactory, new ConfigurationFileSystem(), skipPrivateRepositories, configurationSource);
_ = services.AddSingleton(provider);
configure(services, provider);
return services;
diff --git a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs
index 5ea875eb28..9c561c809f 100644
--- a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs
+++ b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs
@@ -8,6 +8,7 @@
using Elastic.Documentation.Configuration.Toc.DetectionRules;
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Extensions;
+using Elastic.Documentation.FileSystems;
using Nullean.ScopedFileSystem;
using YamlDotNet.Serialization;
using static Elastic.Documentation.SymlinkValidator;
@@ -126,7 +127,7 @@ public static DocumentationSetFile LoadMetadata(IFileInfo file)
///
public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collector, IFileInfo docsetPath, ScopedFileSystem? fileSystem = null, HashSet? noSuppress = null)
{
- fileSystem ??= FileSystemFactory.ScopeSourceDirectory(docsetPath.FileSystem, docsetPath.Directory!.FullName);
+ fileSystem ??= new CheckoutsFileSystem(docsetPath.Directory!, inner: docsetPath.FileSystem);
// Validate that the docset.yml is not a symlink (security: prevents path traversal attacks)
EnsureNotSymlink(docsetPath);
var yaml = fileSystem.File.ReadAllText(docsetPath.FullName);
@@ -148,7 +149,7 @@ public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collecto
///
public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collector, string yaml, IDirectoryInfo sourceDirectory, ScopedFileSystem? fileSystem = null, HashSet? noSuppress = null)
{
- fileSystem ??= FileSystemFactory.ScopeSourceDirectory(sourceDirectory.FileSystem, sourceDirectory.FullName);
+ fileSystem ??= new CheckoutsFileSystem(sourceDirectory, inner: sourceDirectory.FileSystem);
var docSet = Deserialize(yaml);
var docsetPath = fileSystem.Path.Join(sourceDirectory.FullName, "docset.yml").OptionalWindowsReplace();
docSet.SuppressDiagnostics.ExceptWith(noSuppress ?? []);
diff --git a/src/Elastic.Documentation.LinkIndex/GitLinkIndexReader.cs b/src/Elastic.Documentation.LinkIndex/GitLinkIndexReader.cs
index d302caa764..205c6a6432 100644
--- a/src/Elastic.Documentation.LinkIndex/GitLinkIndexReader.cs
+++ b/src/Elastic.Documentation.LinkIndex/GitLinkIndexReader.cs
@@ -5,6 +5,7 @@
using System.Diagnostics;
using System.IO.Abstractions;
using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Links;
using Nullean.ScopedFileSystem;
@@ -27,13 +28,13 @@ public class GitLinkIndexReader : ILinkIndexReader, IDisposable
private readonly SemaphoreSlim _cloneLock = new(1, 1);
private bool _ensuredClone;
- public GitLinkIndexReader(string environment, ScopedFileSystem? fileSystem = null, bool skipFetch = false)
+ public GitLinkIndexReader(string environment, ApplicationDataFileSystem? fileSystem = null, bool skipFetch = false)
{
if (string.IsNullOrWhiteSpace(environment))
throw new ArgumentException("Environment must be specified in the codex configuration (e.g., 'internal', 'security').", nameof(environment));
_environment = environment;
- _fileSystem = fileSystem ?? FileSystemFactory.AppData;
+ _fileSystem = fileSystem ?? new ApplicationDataFileSystem();
_skipFetch = skipFetch;
}
diff --git a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs
index d4dc8d5cca..1baf0e7562 100644
--- a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs
+++ b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs
@@ -8,6 +8,7 @@
using System.Text.Json;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Builder;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.LinkIndex;
using Elastic.Documentation.Serialization;
using Microsoft.Extensions.Logging;
@@ -58,10 +59,10 @@ public record FetchedCrossLinks
};
}
-public abstract class CrossLinkFetcher(ILoggerFactory logFactory, ILinkIndexReader linkIndexProvider, ScopedFileSystem? fileSystem = null) : IDisposable
+public abstract class CrossLinkFetcher(ILoggerFactory logFactory, ILinkIndexReader linkIndexProvider, ApplicationDataFileSystem? fileSystem = null) : IDisposable
{
protected ILogger Logger { get; } = logFactory.CreateLogger(nameof(CrossLinkFetcher));
- private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.AppData;
+ private readonly IFileSystem _fileSystem = fileSystem ?? new ApplicationDataFileSystem();
private LinkRegistry? _linkIndex;
public static RepositoryLinks Deserialize(string json) =>
diff --git a/src/Elastic.Documentation.Tooling/DocumentationPathsResolver.cs b/src/Elastic.Documentation.Tooling/DocumentationPathsResolver.cs
new file mode 100644
index 0000000000..8b83146305
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/DocumentationPathsResolver.cs
@@ -0,0 +1,317 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Extensions;
+using Elastic.Documentation.FileSystems;
+using Nullean.ScopedFileSystem;
+
+namespace Elastic.Documentation;
+
+///
+/// The resolved set of paths and git information for a documentation build or serve invocation.
+/// Produced by following the bootstrap first principles:
+///
+/// - <path> = --path argument, or the current working directory when omitted.
+/// - SourceDirectory = docset scan from <path>:
+/// <path>/docset.yml, <path>/_docset.yml, or any subfolder.
+/// This is the anchor — all subsequent resolution is relative to it.
+/// - CheckoutDirectory = --git-dir?.Parent
+/// ?? FindGitRoot(SourceDirectory, maxParents: N)
+/// ?? error (required; use --git-dir when the heuristic cannot find .git).
+/// - GitDirectories = the real .git directory (or worktree commondir)
+/// resolved from CheckoutDirectory. Needed to add the main repo's .git to the read scope
+/// when the checkout is a worktree.
+/// - Git = anchored exclusively on
+/// CheckoutDirectory — resolved once here, never re-derived downstream.
+/// - OutputDirectory = --output argument,
+/// or <path>/.artifacts/docs/html.
+///
+///
+public sealed record ResolvedDocumentationPaths
+{
+ ///
+ /// The invocation path — the --path argument, or the process current directory when omitted.
+ /// All other paths are derived from this starting point.
+ ///
+ public required IDirectoryInfo InvocationPath { get; init; }
+
+ ///
+ /// The docset anchor: the directory that contains docset.yml or _docset.yml.
+ /// Found by scanning from via the known-location heuristic,
+ /// then a bounded recursive fallback.
+ ///
+ public required IDirectoryInfo SourceDirectory { get; init; }
+
+ /// The resolved docset configuration file (docset.yml or _docset.yml).
+ public required IFileInfo ConfigurationPath { get; init; }
+
+ ///
+ /// The repository checkout root — the directory whose immediate child is .git
+ /// (or whose immediate child is a worktree .git pointer file).
+ /// Always non-: the resolver emits a hard error when no .git
+ /// is found within maxParents of and no --git-dir
+ /// override was supplied.
+ ///
+ public required IDirectoryInfo CheckoutDirectory { get; init; }
+
+ ///
+ /// The real .git directories that must be added to the read scope. For a regular
+ /// checkout this is [CheckoutDirectory/.git]. For a worktree this also includes the
+ /// main repo's .git (or the commondir target) so that git config and refs
+ /// can be read without a scope violation.
+ ///
+ public IReadOnlyList GitDirectories { get; init; } = [];
+
+ ///
+ /// Git checkout information (branch, ref, remote, repository name) resolved from
+ /// . Never re-derived downstream; always set from this record.
+ ///
+ public required GitCheckoutInformation Git { get; init; }
+
+ ///
+ /// The build output directory — the --output argument, or
+ /// /.artifacts/docs/html.
+ ///
+ public required IDirectoryInfo OutputDirectory { get; init; }
+
+ ///
+ /// Extra scope roots (e.g. RUNNER_TEMP, extension roots from
+ /// IDocsBuilderExtension.ExternalScopeRoots) to include in the read scope.
+ /// Disjointness-filtered: roots nested inside are dropped.
+ ///
+ public IReadOnlyList ExtraRoots { get; init; } = [];
+}
+
+///
+/// Arguments that tune .
+///
+public sealed record DocumentationScopeOptions
+{
+ /// Explicit output directory (--output).
+ public string? Output { get; init; }
+
+ ///
+ /// Explicit --git-dir override — the .git directory; its .Parent is the checkout.
+ /// Worktrees are handled automatically through the commondir path; do not point this at a
+ /// worktree's internal gitdir (.git/worktrees/<name>).
+ ///
+ public string? GitDir { get; init; }
+
+ /// Pre-discovered docset configuration file. When set, the docset scan is skipped.
+ public string? ConfigurationFile { get; init; }
+
+ ///
+ /// Git checkout information override (for tests). Replaces the GitCheckoutInformationFactory
+ /// call; goes through resolution rather than bypassing it.
+ ///
+ public GitCheckoutInformation? Git { get; init; }
+
+ ///
+ /// Extra scope roots (e.g. RUNNER_TEMP, extension roots). Disjointness-filtered: roots
+ /// nested inside the resolved checkout are dropped instead of throwing.
+ ///
+ public IEnumerable? ExtraRoots { get; init; }
+
+ ///
+ /// Minimum number of parent directories to walk above the docset anchor when searching for
+ /// .git (default: 1). This is a floor, not a cap: the resolver raises it to at least the
+ /// distance between the invocation root and the resolved anchor, so a docset discovered several
+ /// levels deep by the recursive scan (step 2) doesn't need this raised just to reach a .git
+ /// at the invocation root.
+ ///
+ public int MaxParents { get; init; } = 1;
+
+ /// The underlying filesystem for reads. Defaults to the real filesystem. Pass a mock in tests.
+ public IFileSystem? Inner { get; init; }
+
+ ///
+ /// Override the inner filesystem used for writes. When (the default),
+ /// is used for both read and write scopes.
+ /// Use this to wire a mock write scope against a real read scope (e.g. navigation tests that read
+ /// the real docs tree but write output to an in-memory filesystem).
+ ///
+ public IFileSystem? InnerWrite { get; init; }
+}
+
+///
+/// Thrown when cannot find a docset or checkout.
+///
+public sealed class DocumentationPathException(string message) : Exception(message);
+
+///
+/// Orchestrates the six ordered steps documented on .
+/// Each step's bootstrap scope is created from what the previous step resolved, then discarded.
+///
+public static class DocumentationPathsResolver
+{
+ ///
+ /// Resolve all paths for a documentation build/serve invocation.
+ ///
+ ///
+ /// No docset found, or no .git within MaxParents of the anchor and no
+ /// --git-dir override.
+ ///
+ public static ResolvedDocumentationPaths Resolve(
+ IDirectoryInfo invocation,
+ DocumentationScopeOptions options,
+ IFileSystem inner)
+ {
+ // 1-2. Anchor. Scoped to the invocation path only; skipped when the docset is already known.
+ var (source, configuration) = options.ConfigurationFile is { } known
+ ? (inner.NewFileInfo(known).Directory!, inner.NewFileInfo(known))
+ : ScanForDocset(invocation, inner);
+
+ // 2b. Widen the depth guard to at least the distance between the invocation root and the
+ // anchor. The recursive docset scan (step 2) can legitimately land several levels down
+ // (e.g. a monorepo docset at `docs//docset.yml`); without this, `--git-dir`-less
+ // resolution would need `MaxParents` raised globally just to reach a `.git` that sits
+ // at the invocation root. Anchoring the allowance to the invocation keeps the guard
+ // meaningful for `--path` pointing deep into an unrelated tree, while making the
+ // no-`--path` case (invocation == repo root) a non-issue.
+ var maxParents = Math.Max(options.MaxParents, AnchorDepthBelowInvocation(invocation, source));
+
+ // 3. Checkout, derived from the anchor — never from the invocation.
+ var gitScope = new GitResolveFileSystem(source, maxParents, inner: inner);
+ var checkout = ResolveCheckout(gitScope, source, options, maxParents, inner);
+
+ // 4. Real git directories (the .git pointer path + resolved target for worktrees).
+ // inner (unscoped) is used for worktree resolution: the resolved gitdir lives outside the
+ // anchor's ancestry by design, so a scoped FS would block the commondir traversal.
+ // When --git-dir is explicit the checkout is gitDir.Parent, so gitDir itself must be
+ // carried forward — ResolveGitDirectories can't find it via the gitScope (out-of-tree).
+ var gitDirectories = options.GitDir is { } defined && inner.NewDirInfo(defined) is { Exists: true } explicitGitDir
+ ? [explicitGitDir.FullName]
+ : ResolveGitDirectories(gitScope, checkout, inner);
+
+ // 5. Git information, through a scope widened by step 4.
+ // TryCreate reads config/HEAD from the resolved gitdir, which for a worktree lies outside
+ // the anchor's ancestry — so this is a second instance rather than the same one.
+ // This step uses a GitResolveFileSystem (for .git-aware scoping) because it reads FILES
+ // inside .git/ rather than listing directories at the scope root.
+ var git = options.Git ?? GitCheckoutInformationFactory.Create(checkout,
+ new GitResolveFileSystem(source, maxParents, gitDirectories, inner));
+
+ // 6. Output. Default is relative to the checkout, not the invocation.
+ // --path repo/docs and --path repo/ must both write to repo/.artifacts, not repo/docs/.artifacts.
+ var output = inner.NewDirInfo(options.Output ?? inner.Path.Join(checkout.FullName, ".artifacts", "docs", "html"));
+
+ // 7. Disjointness-filter the extra roots.
+ var extraRoots = FilterExtraRoots(options.ExtraRoots, checkout);
+
+ return new ResolvedDocumentationPaths
+ {
+ InvocationPath = invocation,
+ SourceDirectory = source,
+ ConfigurationPath = configuration,
+ CheckoutDirectory = checkout,
+ GitDirectories = gitDirectories,
+ Git = git,
+ OutputDirectory = output,
+ ExtraRoots = extraRoots
+ };
+ }
+
+ private static (IDirectoryInfo, IFileInfo) ScanForDocset(IDirectoryInfo invocation, IFileSystem inner)
+ {
+ var scan = new DocsetScanFileSystem(invocation, inner);
+ if (!Paths.TryFindDocsFolderFromRoot(scan, scan.DirectoryInfo.New(invocation.FullName), out var dir, out var file))
+ throw new DocumentationPathException(
+ $"No docset.yml or _docset.yml found in '{invocation.FullName}' or any subfolder.");
+ return (dir, file);
+ }
+
+ ///
+ /// Counts how many directory levels sits below .
+ /// Returns 0 when the anchor is at or above the invocation root, or is not one of its descendants
+ /// (e.g. a pre-supplied ConfigurationFile living outside the invocation tree).
+ ///
+ private static int AnchorDepthBelowInvocation(IDirectoryInfo invocation, IDirectoryInfo anchor)
+ {
+ var invocationPath = invocation.FullName.TrimEnd('/', '\\');
+ var depth = 0;
+ var directory = anchor;
+ while (directory is not null)
+ {
+ if (string.Equals(directory.FullName.TrimEnd('/', '\\'), invocationPath, StringComparison.OrdinalIgnoreCase))
+ return depth;
+ directory = directory.Parent;
+ depth++;
+ }
+ return 0;
+ }
+
+ private static IDirectoryInfo ResolveCheckout(
+ IFileSystem gitScope,
+ IDirectoryInfo source,
+ DocumentationScopeOptions options,
+ int maxParents,
+ IFileSystem inner)
+ {
+ if (options.GitDir is { } configured && inner.NewDirInfo(configured) is { } explicitGitDir)
+ {
+ if (!inner.Directory.Exists(explicitGitDir.FullName))
+ throw new DocumentationPathException($"--git-dir '{explicitGitDir.FullName}' does not exist.");
+ if (!inner.File.Exists(inner.Path.Join(explicitGitDir.FullName, "HEAD")))
+ throw new DocumentationPathException(
+ $"--git-dir '{explicitGitDir.FullName}' does not appear to be a valid .git directory (no HEAD file found).");
+ return explicitGitDir.Parent
+ ?? throw new DocumentationPathException($"--git-dir '{explicitGitDir.FullName}' has no parent directory.");
+ }
+
+ var gitRoot = Paths.FindGitRoot(gitScope.DirectoryInfo.New(source.FullName), maxParents);
+ if (gitRoot is not null)
+ return gitRoot;
+
+ // Graceful fallback for mock filesystems without a .git layout (pure-in-memory test scenarios
+ // that do not need a real checkout boundary). Real filesystems always require an explicit checkout.
+ var innerType = inner is ScopedFileSystem sf ? sf.InnerType : inner.GetType();
+ if (innerType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase))
+ return source;
+
+ throw new DocumentationPathException(
+ $"No .git found at '{source.FullName}' or within {maxParents} parent directory(ies). "
+ + "Pass --git-dir to point at the repository's .git directory explicitly.");
+ }
+
+ private static IReadOnlyList ResolveGitDirectories(IFileSystem gitScope, IDirectoryInfo checkout, IFileSystem inner)
+ {
+ var gitPath = gitScope.Path.Join(checkout.FullName, ".git");
+ if (gitScope.Directory.Exists(gitPath))
+ return [gitPath];
+
+ // Worktree: .git is a pointer file. Use inner (unscoped) for TryReadGitDirPointer so that
+ // the commondir traversal can reach the main .git directory, which lies outside the gitScope
+ // root by design (the worktree gitdir is inside the main repo's .git tree).
+ return Paths.TryReadGitDirPointer(inner, inner.FileInfo.New(gitPath), out var resolved) && resolved is not null
+ ? [gitPath, resolved.FullName]
+ : [];
+ }
+
+ private static IReadOnlyList FilterExtraRoots(
+ IEnumerable? extraRoots,
+ IDirectoryInfo checkout)
+ {
+ if (extraRoots is null)
+ return [];
+
+ var fs = checkout.FileSystem;
+ var checkoutPath = checkout.FullName;
+ var result = new List();
+ foreach (var root in extraRoots)
+ {
+ if (string.IsNullOrEmpty(root))
+ continue;
+ // Drop descendants of checkout (already in scope) and ancestors (would subsume checkout).
+ if (!IDirectoryInfoExtensions.IsSubPath(root, checkoutPath, fs)
+ && !IDirectoryInfoExtensions.IsSubPath(checkoutPath, root, fs)
+ && !result.Contains(root, StringComparer.OrdinalIgnoreCase))
+ {
+ result.Add(root);
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/Elastic.Documentation.Tooling/FileSystemFactory.cs b/src/Elastic.Documentation.Tooling/FileSystemFactory.cs
deleted file mode 100644
index 9df45537b8..0000000000
--- a/src/Elastic.Documentation.Tooling/FileSystemFactory.cs
+++ /dev/null
@@ -1,280 +0,0 @@
-// Licensed to Elasticsearch B.V under one or more agreements.
-// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
-// See the LICENSE file in the project root for more information
-
-using System.IO.Abstractions;
-using System.IO.Abstractions.TestingHelpers;
-using Elastic.Documentation.Extensions;
-using Nullean.ScopedFileSystem;
-using DirectoryInfoExtensions = Elastic.Documentation.Extensions.IDirectoryInfoExtensions;
-
-// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes
-#pragma warning disable IDE0130
-namespace Elastic.Documentation.Configuration;
-
-public static class FileSystemFactory
-{
- // Read options: workspace + app data, all confirmed hidden names allowed.
- // Includes .git (GitCheckoutInformation reads it) and .artifacts/.doc.state
- // (incremental build reads existing output state).
- private static readonly ScopedFileSystemOptions WorkingDirectoryReadOptions = new(
- [Paths.WorkingDirectoryRoot.FullName, Paths.ApplicationData.FullName])
- {
- AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
- AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state", ".pagefind-net-frontend-version" }
- };
-
- // Write options: same scope roots but no .git — nothing in the build output
- // pipeline should ever write into the git repository metadata.
- // Temp is allowed because deploy operations (e.g. S3 sync) stage files there.
- private static readonly ScopedFileSystemOptions WorkingDirectoryWriteOptions = new(
- [Paths.WorkingDirectoryRoot.FullName, Paths.ApplicationData.FullName])
- {
- AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".artifacts" },
- AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".doc.state", ".pagefind-net-frontend-version" },
- AllowedSpecialFolders = AllowedSpecialFolder.Temp
- };
-
- // AppData-only options: for components that only access caches/state files.
- private static readonly ScopedFileSystemOptions AppDataOptions = new([Paths.ApplicationData.FullName])
- {
- // .git needed for codex-link-index clone directory inside ApplicationData
- AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" }
- };
-
- ///
- /// A pre-allocated for reading workspace files.
- /// Scoped to the working directory root and per-user app data; allows .git
- /// (read by GitCheckoutInformation), .artifacts and .doc.state
- /// (read for incremental build state).
- ///
- public static ScopedFileSystem RealRead { get; } = new(new FileSystem(), WorkingDirectoryReadOptions);
-
- ///
- /// A pre-allocated for writing build output.
- /// Same scope as but without .git access —
- /// nothing in the output pipeline should write into git repository metadata.
- ///
- public static ScopedFileSystem RealWrite { get; } = new(new FileSystem(), WorkingDirectoryWriteOptions);
-
- ///
- /// A pre-allocated scoped only to the per-user
- /// elastic/docs-builder application data folder. Use for components that
- /// access caches or state and have no need for workspace files
- /// (e.g. CrossLinkFetcher, CheckForUpdatesFilter, GitLinkIndexReader).
- ///
- public static ScopedFileSystem AppData { get; } = new(new FileSystem(), AppDataOptions);
-
- ///
- /// Creates a new wrapping a fresh ,
- /// using the working-directory read options. Each call returns a new independent in-memory file system.
- ///
- public static ScopedFileSystem InMemory() => new(new MockFileSystem(), WorkingDirectoryReadOptions);
-
- ///
- /// Like but additionally scopes the mock filesystem to 's
- /// git root. Use when serving docs from a directory outside the current working tree so that the
- /// in-memory output path (<source>/.artifacts/docs/html) passes scope validation.
- ///
- public static ScopedFileSystem InMemoryForPath(string? path)
- {
- if (path is null)
- return InMemory();
- var root = Paths.FindGitRoot(path);
- if (root == Paths.WorkingDirectoryRoot.FullName)
- return InMemory();
- return new(new MockFileSystem(), new ScopedFileSystemOptions(
- [Paths.WorkingDirectoryRoot.FullName, Paths.ApplicationData.FullName, root])
- {
- AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
- AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state", ".pagefind-net-frontend-version" }
- });
- }
-
- ///
- /// Scopes to and
- /// for reading. Use when the inner FS contains files
- /// that live within the current working-directory tree (e.g. a test MockFileSystem
- /// seeded with workspace-relative paths).
- ///
- public static ScopedFileSystem ScopeCurrentWorkingDirectory(IFileSystem inner) =>
- new(inner, WorkingDirectoryReadOptions);
-
- ///
- /// Scopes to and
- /// for reading, extended by
- /// (e.g. detection-rules folders declared via
- /// ).
- ///
- public static ScopedFileSystem ScopeCurrentWorkingDirectory(IFileSystem inner, IEnumerable? extensionRoots)
- {
- if (extensionRoots is null)
- return ScopeCurrentWorkingDirectory(inner);
-
- var workingRoot = inner.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
- var externalRoots = extensionRoots
- .Select(r => inner.DirectoryInfo.New(r))
- // Drop descendants of the working root (already covered by the base scope).
- // Also drop ancestors of the working root — they would subsume the working root,
- // producing overlapping roots that ScopedFileSystem rejects with ArgumentException.
- .Where(d => !DirectoryInfoExtensions.IsSubPathOf(d, workingRoot) && !DirectoryInfoExtensions.IsSubPathOf(workingRoot, d))
- .Select(d => d.FullName)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray();
-
- if (externalRoots.Length == 0)
- return ScopeCurrentWorkingDirectory(inner);
-
- var roots = new[] { Paths.WorkingDirectoryRoot.FullName, Paths.ApplicationData.FullName }
- .Concat(externalRoots)
- .ToArray();
-
- return new ScopedFileSystem(inner, new ScopedFileSystemOptions(roots)
- {
- AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
- AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state", ".pagefind-net-frontend-version" }
- });
- }
-
- // Builds write options that include AllowedSpecialFolders.Temp PLUS the inner FS's own
- // GetTempPath() as an explicit root — but only when the inner FS is MockFileSystem.
- //
- // MockFileSystem hardcodes its temp path ("C:\temp" on Windows, unix-ified to "/temp/"
- // elsewhere) instead of calling System.IO.Path.GetTempPath(). AllowedSpecialFolder.Temp uses
- // the real GetTempPath() (e.g. "/tmp/" on Linux, "C:\Users\\AppData\Local\Temp" on
- // Windows), so the two diverge on every OS and scope validation fails for any path created
- // via mockFs.Path.GetTempPath().
- //
- // Fix tracked upstream: https://github.com/TestableIO/System.IO.Abstractions/pull/1454
- // Once that ships and we update the package reference we can drop this workaround.
- //
- // We use ScopedFileSystem.InnerType (added in Nullean.ScopedFileSystem 0.4.0) to avoid a
- // fragile string-based type check.
- private static ScopedFileSystemOptions BuildWriteOptions(IFileSystem inner, params string[] roots)
- {
- var allRoots = roots.ToList();
- var innerType = inner is ScopedFileSystem sf ? sf.InnerType : inner.GetType();
- if (innerType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase))
- {
- // Cover MockFileSystem's hardcoded temp path
- var innerTemp = inner.Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
- if (!string.IsNullOrEmpty(innerTemp) && !allRoots.Contains(innerTemp, StringComparer.OrdinalIgnoreCase))
- allRoots.Add(innerTemp);
- }
- return new ScopedFileSystemOptions([.. allRoots])
- {
- AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".artifacts" },
- AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".doc.state", ".pagefind-net-frontend-version" },
- AllowedSpecialFolders = AllowedSpecialFolder.Temp
- };
- }
-
- ///
- /// Scopes to and
- /// for writing (.git not allowed). Use when
- /// the inner FS writes into the working-directory tree.
- ///
- public static ScopedFileSystem ScopeCurrentWorkingDirectoryForWrite(IFileSystem inner) =>
- new(inner, BuildWriteOptions(
- inner, Paths.WorkingDirectoryRoot.FullName, Paths.ApplicationData.FullName));
-
- ///
- /// Scopes to an explicit and
- /// for reading. Use when the files to be read live under
- /// a specific known root that is not — for example
- /// test fixtures with assembler-checkout paths or service code operating on a given directory.
- ///
- public static ScopedFileSystem ScopeSourceDirectory(IFileSystem inner, string sourceRoot) =>
- new(inner, new ScopedFileSystemOptions([sourceRoot, Paths.ApplicationData.FullName])
- {
- AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
- AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state", ".pagefind-net-frontend-version" }
- });
-
- ///
- /// Scopes to an explicit and
- /// for writing (.git not allowed). Write variant
- /// of .
- ///
- public static ScopedFileSystem ScopeSourceDirectoryForWrite(IFileSystem inner, string sourceRoot) =>
- new(inner, BuildWriteOptions(inner, sourceRoot, Paths.ApplicationData.FullName));
-
- ///
- /// Creates a read scoped to the git root of
- /// . Falls back to when
- /// is . Use in commands that accept an explicit --path argument.
- ///
- /// Suitable for command-layer code. Service-layer tests use directly
- /// and do not exercise this method.
- ///
- ///
- public static ScopedFileSystem RealGitRootForPath(string? path)
- {
- var root = path is null ? Paths.WorkingDirectoryRoot.FullName : Paths.FindGitRoot(path);
- var roots = new List { root, Paths.ApplicationData.FullName };
-
- // In a git worktree the local .git entry is a file pointing to the main repo's
- // .git/worktrees/ directory. GitCheckoutInformation needs to read config and
- // HEAD from the main repo's .git dir, which lives outside the worktree root.
- // Add it as an explicit scope root so those reads are not rejected.
- var worktreePointer = Path.Join(root, ".git");
- if (File.Exists(worktreePointer))
- {
- var gitdir = File.ReadAllText(worktreePointer).Replace("gitdir:", "").Trim();
- // gitdir = /main/repo/.git/worktrees/ — go up two levels to reach .git
- var mainGitDir = Path.GetFullPath(Path.Join(gitdir, "..", ".."));
- if (Directory.Exists(mainGitDir))
- roots.Add(mainGitDir);
- }
-
- // Fast path: no worktree detected and path was null — reuse the pre-built instance
- if (roots.Count == 2 && path is null)
- return RealRead;
-
- return new ScopedFileSystem(new FileSystem(), new ScopedFileSystemOptions([.. roots])
- {
- AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
- AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state", ".pagefind-net-frontend-version" }
- });
- }
-
- ///
- /// Creates a write scoped to the git root of
- /// (and if it falls outside that root).
- /// Falls back to when both are .
- /// Use in commands that accept explicit --path and/or --output arguments.
- ///
- public static ScopedFileSystem RealGitRootForPathWrite(string? path, string? output = null)
- {
- if (path is null && output is null)
- return RealWrite;
-
- var gitRoot = path is not null ? Paths.FindGitRoot(path) : Paths.WorkingDirectoryRoot.FullName;
- var roots = new List { gitRoot, Paths.ApplicationData.FullName };
-
- var plain = new FileSystem();
- if (output is not null)
- {
- var absOutput = Path.IsPathRooted(output) ? output : Path.GetFullPath(output);
- if (!DirectoryInfoExtensions.IsSubPathOf(plain.DirectoryInfo.New(absOutput), plain.DirectoryInfo.New(gitRoot)))
- roots.Add(absOutput);
- }
-
- return new ScopedFileSystem(plain, BuildWriteOptions(plain, [.. roots]));
- }
-
- ///
- /// Creates a read for CI environments,
- /// extending the default scope with RUNNER_TEMP when available.
- /// Falls back to when RUNNER_TEMP is not set.
- /// Use in CI commands that need to read temporary files staged in the GitHub Actions runner.
- ///
- public static ScopedFileSystem RealReadForRunnerTemp(IEnvironmentVariables? environmentVariables = null)
- {
- var runnerTemp = environmentVariables?.GetEnvironmentVariable("RUNNER_TEMP");
- if (string.IsNullOrWhiteSpace(runnerTemp))
- return RealRead;
-
- return ScopeCurrentWorkingDirectory(new FileSystem(), [runnerTemp]);
- }
-}
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/ApplicationDataFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/ApplicationDataFileSystem.cs
new file mode 100644
index 0000000000..c52651b934
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/ApplicationDataFileSystem.cs
@@ -0,0 +1,26 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Elastic.Documentation.Configuration;
+using Nullean.ScopedFileSystem;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// A scoped filesystem rooted at the per-user elastic/docs-builder application data folder.
+/// Use for components that access caches or state and have no need for workspace files
+/// (e.g. CrossLinkFetcher, CheckForUpdatesFilter, GitLinkIndexReader).
+///
+public class ApplicationDataFileSystem(IFileSystem? inner = null)
+ : ScopedFileSystem(
+ inner ?? new FileSystem(),
+ new ScopedFileSystemOptions([Paths.ApplicationData.FullName])
+ {
+ // .git needed for codex-link-index clone directory inside ApplicationData
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" }
+ }),
+ IAppDataFileSystem
+{
+}
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/ChangelogFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/ChangelogFileSystem.cs
new file mode 100644
index 0000000000..266d91f065
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/ChangelogFileSystem.cs
@@ -0,0 +1,38 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Elastic.Documentation.Configuration;
+using Nullean.ScopedFileSystem;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Scope for changelog commands: the git root of the target repository.
+/// Allows reading .git metadata (remote URL, branch); does not include
+/// AppData or build artifacts — changelog operates only within the repo working tree.
+///
+public class ChangelogFileSystem(IDirectoryInfo root, IFileSystem? inner = null)
+ : ScopedFileSystem(inner ?? Physical, new ScopedFileSystemOptions([root.FullName])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" }
+ }),
+ IChangelogFileSystem
+{
+ private static readonly FileSystem Physical = new();
+
+ ///
+ /// Creates a scope anchored at the git root of the current working directory.
+ /// Falls back to the working directory itself when no .git root is found —
+ /// changelog init is designed to run before a git repository exists.
+ ///
+ public static ChangelogFileSystem FromWorkingDirectory(IFileSystem? inner = null)
+ {
+ var fs = inner ?? Physical;
+ var workingRoot = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory());
+ var gitRoot = Paths.FindGitRoot(workingRoot) ?? workingRoot;
+ return new ChangelogFileSystem(gitRoot, inner);
+ }
+}
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/CheckoutsFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/CheckoutsFileSystem.cs
new file mode 100644
index 0000000000..448ee8cd9b
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/CheckoutsFileSystem.cs
@@ -0,0 +1,84 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Elastic.Documentation.Configuration;
+using Nullean.ScopedFileSystem;
+using static Elastic.Documentation.Extensions.IDirectoryInfoExtensions;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Scope over a directory containing cloned documentation checkouts. No docset anchoring, no git
+/// information — the assembler reads configuration and per-clone output, not a documentation set.
+///
+/// Use this for the assembler and changelog commands which work with a tree of clones.
+/// Use when you have a single documentation set with a docset anchor.
+///
+///
+public class CheckoutsFileSystem : ScopedFileSystem, ICheckoutsFileSystem
+{
+ private static readonly FileSystem Physical = new();
+
+ private readonly IFileSystem _inner;
+
+ public CheckoutsFileSystem(IDirectoryInfo root,
+ IDirectoryInfo? output = null,
+ IFileSystem? inner = null,
+ IEnumerable? extraRoots = null)
+ : base(inner ?? Physical, BuildReadOptions(root, extraRoots))
+ {
+ _inner = inner ?? Physical;
+ Write = new DocumentationWriteFileSystem(root, output, _inner);
+ }
+
+ ///
+ /// This instance as a read scope. Always prefer .Read at call sites over passing the
+ /// instance directly — a slot that wants a read scope should say so, symmetrically with .Write.
+ ///
+ public CheckoutsFileSystem Read => this;
+
+ /// Write scope for this checkout tree.
+ public DocumentationWriteFileSystem Write { get; }
+
+ private static ScopedFileSystemOptions BuildReadOptions(IDirectoryInfo root, IEnumerable? extraRoots)
+ {
+ var fs = root.FileSystem;
+ var rootPath = root.FullName;
+ var roots = new List { rootPath };
+
+ // AppData is disjointness-filtered too: on CI the checkouts directory lives inside AppData
+ // (/home/runner/.local/share/elastic/docs-builder/checkouts/...), so AppData would subsume
+ // root and the ScopedFileSystem constructor would throw.
+ var appData = Paths.ApplicationData.FullName;
+ if (!IsSubPath(appData, rootPath, fs) && !IsSubPath(rootPath, appData, fs))
+ roots.Add(appData);
+
+ if (extraRoots is not null)
+ {
+ foreach (var extra in extraRoots)
+ {
+ if (string.IsNullOrEmpty(extra))
+ continue;
+ // Drop descendants of root (already covered) and ancestors (would subsume root, causing overlap).
+ if (!IsSubPath(extra, rootPath, fs) && !IsSubPath(rootPath, extra, fs) && !roots.Contains(extra, StringComparer.OrdinalIgnoreCase))
+ roots.Add(extra);
+ }
+ }
+
+ return new ScopedFileSystemOptions([.. roots])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state", ".pagefind-net-frontend-version" }
+ };
+ }
+
+ ///
+ /// Creates a scope over the current working directory root. Suitable for assembler and navigation
+ /// commands that operate on the local checkout tree without a specific docset anchor.
+ ///
+ /// The underlying filesystem. Defaults to a new when .
+ public static CheckoutsFileSystem FromWorkingDirectory(IFileSystem? inner = null) =>
+ new((inner ?? Physical).DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), inner: inner);
+}
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/ConfigurationFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/ConfigurationFileSystem.cs
new file mode 100644
index 0000000000..01d36090b5
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/ConfigurationFileSystem.cs
@@ -0,0 +1,31 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Elastic.Documentation.Configuration;
+using Nullean.ScopedFileSystem;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// A scoped filesystem covering the docs configuration tree (<cwd>/config) and
+/// per-user application data. Used by ConfigurationFileProvider, which reads
+/// config/*.yml and writes runtime artefacts under AppData/config-runtime.
+///
+public class ConfigurationFileSystem(IFileSystem? inner = null)
+ : ScopedFileSystem(
+ // ScopedFileSystem cannot wrap another ScopedFileSystem. When a ScopedFileSystem is
+ // supplied (e.g. a DocumentationFileSystem from a test context) use the physical FS instead,
+ // since config files live outside a docset scope anyway.
+ inner is ScopedFileSystem ? new FileSystem() : (inner ?? new FileSystem()),
+ new ScopedFileSystemOptions([
+ System.IO.Path.Join(Paths.WorkingDirectoryRoot.FullName, "config"),
+ Paths.ApplicationData.FullName
+ ])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" }
+ }),
+ IAppDataFileSystem
+{
+}
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/DocsetScanFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/DocsetScanFileSystem.cs
new file mode 100644
index 0000000000..dd3bf65839
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/DocsetScanFileSystem.cs
@@ -0,0 +1,22 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Nullean.ScopedFileSystem;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Bootstrap-only filesystem rooted at the invocation path. Used only for the docset scan step of
+/// ; discarded immediately after the anchor is found.
+///
+/// Rooted at (and below) the invocation path because both scan strategies read at-or-below their root:
+/// the known-location heuristic checks the path itself and its docs/ subdirectory, and the
+/// recursive fallback enumerates downward. Nothing about the docset scan needs a parent directory.
+///
+///
+internal sealed class DocsetScanFileSystem(IDirectoryInfo path, IFileSystem? inner = null)
+ : ScopedFileSystem(inner ?? new FileSystem(), new ScopedFileSystemOptions([path.FullName]))
+{
+}
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/DocumentationFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/DocumentationFileSystem.cs
new file mode 100644
index 0000000000..e3738dcc8f
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/DocumentationFileSystem.cs
@@ -0,0 +1,119 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Extensions;
+using Nullean.ScopedFileSystem;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// The read scope for a single documentation set. Anchored on a resolved docset.yml and its
+/// checkout root. Exposes a matching scope derived from the same paths — so read and
+/// write cannot disagree about the checkout.
+///
+/// Construction is via only. The constructor is private; it takes
+/// already-resolved paths so that can run its bootstrap
+/// scopes before the final scope is built.
+///
+///
+public class DocumentationFileSystem : ScopedFileSystem, IDocumentationFileSystem
+{
+ private static readonly FileSystem Physical = new();
+
+ private DocumentationFileSystem(ResolvedDocumentationPaths paths, IFileSystem inner, IFileSystem? innerWrite = null)
+ : base(inner, BuildReadOptions(paths))
+ {
+ Paths = paths;
+ Write = new DocumentationWriteFileSystem(paths.CheckoutDirectory, paths.OutputDirectory, innerWrite ?? inner);
+ }
+
+ /// Everything the anchoring resolved. Read and write scopes are derived from exactly this.
+ public ResolvedDocumentationPaths Paths { get; }
+
+ ///
+ /// This instance as a read scope. Always prefer .Read at call sites over passing the
+ /// instance directly — a slot that wants a read scope should say so, symmetrically with .Write.
+ ///
+ public DocumentationFileSystem Read => this;
+
+ /// Write scope derived from the same resolved paths. Never wraps this.
+ public DocumentationWriteFileSystem Write { get; }
+
+ ///
+ /// Anchor on the docset under , derive the checkout from it, and scope to
+ /// the result. For build/serve commands where the user supplied --path (or nothing).
+ ///
+ ///
+ /// The directory to start the docset scan from. When , the current working
+ /// directory is used.
+ ///
+ ///
+ /// Optional tuning: output directory, explicit --git-dir, pre-discovered configuration file,
+ /// extra scope roots, max parents for the git walk, and the mock seam.
+ ///
+ ///
+ /// No docset found under , or no .git within MaxParents of the
+ /// anchor and no --git-dir override.
+ ///
+ public static DocumentationFileSystem Resolve(IDirectoryInfo? path = null, DocumentationScopeOptions? options = null)
+ {
+ var opts = options ?? new DocumentationScopeOptions();
+ var inner = opts.Inner ?? Physical;
+ var invocation = path ?? inner.DirectoryInfo.New(inner.Directory.GetCurrentDirectory());
+
+ // Docset first (scoped to the invocation path), then git from the anchor it produced.
+ // The resolver constructs its own bootstrap scopes in that order and discards them.
+ var paths = DocumentationPathsResolver.Resolve(invocation, opts, inner);
+ return new DocumentationFileSystem(paths, inner, opts.InnerWrite);
+ }
+
+ public static DocumentationFileSystem Resolve(string? path, DocumentationScopeOptions? options = null)
+ {
+ var opts = options ?? new DocumentationScopeOptions();
+ var inner = opts.Inner ?? Physical;
+ var invocation = path is not null ? inner.DirectoryInfo.New(path) : null;
+ return Resolve(invocation, opts);
+ }
+
+ private static ScopedFileSystemOptions BuildReadOptions(ResolvedDocumentationPaths paths)
+ {
+ var fs = paths.CheckoutDirectory.FileSystem;
+ var checkoutPath = paths.CheckoutDirectory.FullName;
+ var roots = new List { checkoutPath };
+
+ // AppData is disjointness-filtered: on CI each individual docset checkout lives inside AppData
+ // (/home/runner/.local/share/elastic/docs-builder/checkouts/current/), so AppData would
+ // subsume checkoutPath and trigger ValidateRootsAreDisjoint.
+ var appData = Configuration.Paths.ApplicationData.FullName;
+ if (!IDirectoryInfoExtensions.IsSubPath(appData, checkoutPath, fs)
+ && !IDirectoryInfoExtensions.IsSubPath(checkoutPath, appData, fs))
+ {
+ roots.Add(appData);
+ }
+
+ foreach (var gitDir in paths.GitDirectories)
+ {
+ if (!IDirectoryInfoExtensions.IsSubPath(gitDir, checkoutPath, fs))
+ roots.Add(gitDir);
+ }
+
+ foreach (var extra in paths.ExtraRoots)
+ {
+ if (!string.IsNullOrEmpty(extra)
+ && !IDirectoryInfoExtensions.IsSubPath(extra, checkoutPath, fs)
+ && !roots.Contains(extra, StringComparer.OrdinalIgnoreCase))
+ {
+ roots.Add(extra);
+ }
+ }
+
+ return new ScopedFileSystemOptions([.. roots])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state", ".pagefind-net-frontend-version" }
+ };
+ }
+}
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/GitResolveFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/GitResolveFileSystem.cs
new file mode 100644
index 0000000000..4769ffdee9
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/GitResolveFileSystem.cs
@@ -0,0 +1,71 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Elastic.Documentation.Extensions;
+using Nullean.ScopedFileSystem;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Bootstrap-only filesystem for git resolution steps in .
+/// Discarded before BuildContext exists.
+///
+/// Rooted at maxParents levels above the docset anchor —
+/// git resolution measures from the anchor, not the invocation path, so that both
+/// --path repo/ and --path repo/docs converge on the same checkout under a single bound.
+///
+///
+/// The optional gitDirectories parameter widens the scope to include the resolved
+/// .git directories (needed for the second pass that reads config, HEAD, and
+/// refs/heads/* — for worktrees these lie outside the anchor's ancestry).
+///
+///
+#pragma warning disable IDE0290 // Cannot use primary constructor — delegates to static helper
+public class GitResolveFileSystem : ScopedFileSystem
+{
+ public GitResolveFileSystem(
+ IDirectoryInfo anchor,
+ int maxParents = 1,
+ IReadOnlyList? gitDirectories = null,
+ IFileSystem? inner = null)
+ : base(inner ?? new FileSystem(), BuildOptions(anchor, maxParents, gitDirectories))
+ {
+ }
+#pragma warning restore IDE0290
+
+ private static ScopedFileSystemOptions BuildOptions(
+ IDirectoryInfo anchor,
+ int maxParents,
+ IReadOnlyList? gitDirectories)
+ {
+ // Walk maxParents above the anchor to get the scope root.
+ var root = anchor;
+ for (var i = 0; i < maxParents; i++)
+ root = root.Parent ?? root;
+
+ // ScopedFileSystem normalises scope roots via TrimEnd(separator). On Unix, "/" trimmed is "".
+ // An empty scope root makes every path fail the IsWithinRoot check, so guard against it:
+ // if the computed root IS the filesystem root, fall back to the anchor itself.
+ var fs = anchor.FileSystem;
+ var normalised = root.FullName.TrimEnd(fs.Path.DirectorySeparatorChar, fs.Path.AltDirectorySeparatorChar);
+ var rootPath = string.IsNullOrEmpty(normalised) ? anchor.FullName : root.FullName;
+ var roots = new List { rootPath };
+
+ if (gitDirectories is { Count: > 0 })
+ {
+ foreach (var gitDir in gitDirectories)
+ {
+ if (!IDirectoryInfoExtensions.IsSubPath(gitDir, rootPath, fs))
+ roots.Add(gitDir);
+ }
+ }
+
+ return new ScopedFileSystemOptions([.. roots])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" }
+ };
+ }
+}
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/IAppDataFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/IAppDataFileSystem.cs
new file mode 100644
index 0000000000..0395f73a7c
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/IAppDataFileSystem.cs
@@ -0,0 +1,15 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Marker interface for filesystems that include the application data directory
+/// () in their scope.
+/// Services that read or write AppData (caches, link indices, config-runtime state) should
+/// declare this interface rather than the bare .
+///
+public interface IAppDataFileSystem : IFileSystem;
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/IChangelogFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/IChangelogFileSystem.cs
new file mode 100644
index 0000000000..d006087101
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/IChangelogFileSystem.cs
@@ -0,0 +1,15 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Marker interface for the changelog scope: the git root of the target repository.
+/// Changelog services declare this rather than bare so that the
+/// compiler enforces that only a properly-scoped filesystem (one that can reach the
+/// changelog YAML files and read .git metadata) is wired in.
+///
+public interface IChangelogFileSystem : IFileSystem;
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/ICheckoutsFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/ICheckoutsFileSystem.cs
new file mode 100644
index 0000000000..d7c6c4369d
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/ICheckoutsFileSystem.cs
@@ -0,0 +1,13 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Marker interface for the checkout-tree scope: a working-directory root paired with the
+/// application data directory. The checkouts filesystem is the read/write aggregate used by
+/// assembler and codex commands that operate across many repository clones.
+/// Extends because the checkouts scope always includes AppData.
+///
+public interface ICheckoutsFileSystem : IAppDataFileSystem;
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/IRunnerTempFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/IRunnerTempFileSystem.cs
new file mode 100644
index 0000000000..561f53905c
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/IRunnerTempFileSystem.cs
@@ -0,0 +1,15 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Marker interface for the CI runner scope: the working directory root plus one or more
+/// runner-provided paths (RUNNER_TEMP, artifact output dirs, metadata paths).
+/// Services that operate across the CI workspace without a fixed docset anchor declare this
+/// rather than bare .
+///
+public interface IRunnerTempFileSystem : IFileSystem;
diff --git a/src/Elastic.Documentation.Tooling/FileSystems/RunnerTempFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/RunnerTempFileSystem.cs
new file mode 100644
index 0000000000..5160871446
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/FileSystems/RunnerTempFileSystem.cs
@@ -0,0 +1,88 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Elastic.Documentation.Configuration;
+using Nullean.ScopedFileSystem;
+using static Elastic.Documentation.Extensions.IDirectoryInfoExtensions;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Scope for CI evaluation commands: the working-directory root plus any runner-provided paths
+/// (RUNNER_TEMP, artifact output dirs, staging dirs, metadata file locations).
+///
+/// Used by evaluate-pr, evaluate-artifact, and prepare-artifact —
+/// all three operate on paths vended by the CI environment, not a fixed docset or checkout.
+/// Permits .git so changelog configuration can be located by the config loader.
+///
+///
+public class RunnerTempFileSystem(
+ IDirectoryInfo workingRoot,
+ IEnumerable? ciPaths = null,
+ IFileSystem? inner = null)
+ : ScopedFileSystem(inner ?? Physical, BuildOptions(workingRoot, ciPaths)),
+ IRunnerTempFileSystem
+{
+ private static readonly FileSystem Physical = new();
+
+ private static ScopedFileSystemOptions BuildOptions(IDirectoryInfo workingRoot, IEnumerable? ciPaths)
+ {
+ var fs = workingRoot.FileSystem;
+ var roots = new List { workingRoot.FullName };
+
+ if (ciPaths is not null)
+ {
+ foreach (var path in ciPaths)
+ {
+ if (string.IsNullOrEmpty(path))
+ continue;
+ // Drop descendants and ancestors of workingRoot to avoid disjointness violations.
+ var isDescendant = IsSubPath(path, workingRoot.FullName, fs);
+ var isAncestor = IsSubPath(workingRoot.FullName, path, fs);
+ var isDuplicate = roots.Contains(path, StringComparer.OrdinalIgnoreCase);
+ if (!isDescendant && !isAncestor && !isDuplicate)
+ roots.Add(path);
+ }
+ }
+
+ return new ScopedFileSystemOptions([.. roots])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" }
+ };
+ }
+
+ public static RunnerTempFileSystem ForEvaluatePr(IEnvironmentVariables env, IFileSystem? inner = null)
+ {
+ var fs = inner ?? Physical;
+ var workingRoot = fs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
+ var runnerTemp = env.GetEnvironmentVariable("RUNNER_TEMP");
+ return new RunnerTempFileSystem(workingRoot,
+ ciPaths: string.IsNullOrWhiteSpace(runnerTemp) ? null : [runnerTemp],
+ inner: inner);
+ }
+
+ public static RunnerTempFileSystem ForEvaluateArtifact(string metadataPath, IFileSystem? inner = null)
+ {
+ var fs = inner ?? Physical;
+ var workingRoot = fs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
+ var metadataDir = System.IO.Path.GetDirectoryName(metadataPath);
+ return new RunnerTempFileSystem(workingRoot,
+ ciPaths: string.IsNullOrWhiteSpace(metadataDir) ? null : [metadataDir],
+ inner: inner);
+ }
+
+ public static RunnerTempFileSystem ForPrepareArtifact(string? stagingDir, string? outputDir, IFileSystem? inner = null)
+ {
+ var fs = inner ?? Physical;
+ var workingRoot = fs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
+ var ciPaths = new List();
+ if (!string.IsNullOrWhiteSpace(stagingDir))
+ ciPaths.Add(stagingDir);
+ if (!string.IsNullOrWhiteSpace(outputDir))
+ ciPaths.Add(outputDir);
+ return new RunnerTempFileSystem(workingRoot, ciPaths: ciPaths.Count > 0 ? ciPaths : null, inner: inner);
+ }
+}
diff --git a/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs b/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs
index b5875c25c8..382ba71407 100644
--- a/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs
+++ b/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs
@@ -4,6 +4,7 @@
using System.IO.Abstractions;
using System.Text.RegularExpressions;
+using Elastic.Documentation.Configuration;
using Elastic.Documentation.Extensions;
using Microsoft.Extensions.Logging;
using Nullean.ScopedFileSystem;
@@ -19,57 +20,90 @@ public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem
if (source is null)
return GitCheckoutInformation.Unavailable;
- // Return test data for in-memory (mock) file systems. Use ScopedFileSystem.InnerType
- // (available since Nullean.ScopedFileSystem 0.4.0) to inspect through the scope wrapper
- // rather than relying on the outer type name.
- var fsType = fileSystem is ScopedFileSystem sf ? sf.InnerType : fileSystem.GetType();
- if (fsType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase))
+ var result = TryCreate(source, fileSystem, logger);
+
+ // Fall back to canned test data only when the inner filesystem is a mock AND no .git entry
+ // exists at all at the source. This preserves back-compat for tests that seed no git layout
+ // (they get the well-known canned instance), while tests that seed a .git file or directory
+ // — even one that fails to resolve — receive the real result (Unavailable).
+ // Use ScopedFileSystem.InnerType (available since Nullean.ScopedFileSystem 0.4.0) to inspect
+ // through the scope wrapper rather than relying on the outer type name.
+ if (result == GitCheckoutInformation.Unavailable)
{
- return new GitCheckoutInformation
+ var fsType = fileSystem is ScopedFileSystem sf ? sf.InnerType : fileSystem.GetType();
+ if (fsType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase) && IsLegacyTestWithoutGitLayout(fileSystem, source))
{
- Branch = $"test-e35fcb27-5f60-4e",
- Remote = "elastic/docs-builder",
- Ref = "e35fcb27-5f60-4e",
- RepositoryName = "docs-builder"
- };
+ return new GitCheckoutInformation
+ {
+ Branch = "test-e35fcb27-5f60-4e",
+ Remote = "elastic/docs-builder",
+ Ref = "e35fcb27-5f60-4e",
+ RepositoryName = "docs-builder"
+ };
+ }
}
- var fakeRef = Guid.NewGuid().ToString()[..16];
- var gitDir = GitDir(source, ".git");
- if (!gitDir.Exists)
+ return result;
+ }
+
+ private static GitCheckoutInformation TryCreate(IDirectoryInfo source, IFileSystem fileSystem, ILogger? logger)
+ {
+ // Resolve the actual .git directory. For regular repos this is source/.git/;
+ // for worktrees source/.git is a file pointing to the real git dir.
+ IDirectoryInfo gitDir;
+ var gitDirPath = fileSystem.Path.Join(source.FullName, ".git");
+
+ if (fileSystem.Directory.Exists(gitDirPath))
{
- var worktreeFile = Git(source, ".git");
- if (!worktreeFile.Exists)
- return GitCheckoutInformation.Unavailable;
- var workTreePath = Read(source, ".git")?.Replace("gitdir: ", string.Empty);
- if (workTreePath is null)
- return GitCheckoutInformation.Unavailable;
- gitDir = fileSystem.DirectoryInfo.New(workTreePath).GetParent(".git");
- if (gitDir is null || !gitDir.Exists)
+ gitDir = fileSystem.DirectoryInfo.New(gitDirPath);
+ }
+ else
+ {
+ var gitFile = fileSystem.FileInfo.New(gitDirPath);
+ if (!Paths.TryReadGitDirPointer(fileSystem, gitFile, out var resolvedGitDir)
+ || resolvedGitDir is null)
return GitCheckoutInformation.Unavailable;
+
+ gitDir = resolvedGitDir;
}
- var gitConfig = Git(gitDir, "config");
- if (!gitConfig.Exists)
+ var gitConfigPath = fileSystem.Path.Join(gitDir.FullName, "config");
+ if (!fileSystem.File.Exists(gitConfigPath))
{
logger?.LogInformation("Git checkout information not available.");
return GitCheckoutInformation.Unavailable;
}
- var head = Read(gitDir, "HEAD") ?? fakeRef;
- var gitRef = head;
- var branch = head.Replace("refs/heads/", string.Empty);
- if (head.StartsWith("ref:", StringComparison.OrdinalIgnoreCase))
+ var headPath = fileSystem.Path.Join(gitDir.FullName, "HEAD");
+ var headText = fileSystem.File.Exists(headPath)
+ ? fileSystem.File.ReadAllText(headPath).Trim()
+ : null;
+
+ if (headText is null)
+ return GitCheckoutInformation.Unavailable;
+
+ string gitRef;
+ string branch;
+ if (headText.StartsWith("ref:", StringComparison.OrdinalIgnoreCase))
{
- head = head.Replace("ref: ", string.Empty);
- gitRef = Read(gitDir, head) ?? fakeRef;
- branch = branch.Replace("ref: ", string.Empty);
+ var refPath = headText["ref:".Length..].Trim();
+ branch = refPath.Replace("refs/heads/", string.Empty);
+ var refFilePath = fileSystem.Path.Join(gitDir.FullName, refPath.Replace('/', fileSystem.Path.DirectorySeparatorChar));
+ gitRef = fileSystem.File.Exists(refFilePath)
+ ? fileSystem.File.ReadAllText(refFilePath).Trim()
+ : headText; // symbolic ref not yet written (new empty repo) — use the ref name itself
}
else
- branch = Environment.GetEnvironmentVariable("GITHUB_PR_REF_NAME") ?? Environment.GetEnvironmentVariable("GITHUB_REF_NAME") ?? "detached/head";
+ {
+ // Detached HEAD: raw SHA
+ gitRef = headText;
+ branch = Environment.GetEnvironmentVariable("GITHUB_PR_REF_NAME")
+ ?? Environment.GetEnvironmentVariable("GITHUB_REF_NAME")
+ ?? "detached/head";
+ }
var ini = new IniFile();
- using var stream = gitConfig.OpenRead();
+ using var stream = fileSystem.File.OpenRead(gitConfigPath);
using var streamReader = new StreamReader(stream);
ini.Load(streamReader);
@@ -113,35 +147,42 @@ public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem
logger?.LogInformation("-> Remote Name: {GitRemote}", info.Remote);
logger?.LogInformation("-> Repository Name: {RepositoryName}", info.RepositoryName);
return info;
+ }
- IFileInfo Git(IDirectoryInfo directoryInfo, string path) =>
- fileSystem.FileInfo.New(Path.Join(directoryInfo.FullName, path));
-
- IDirectoryInfo GitDir(IDirectoryInfo directoryInfo, string path) =>
- fileSystem.DirectoryInfo.New(Path.Join(directoryInfo.FullName, path));
-
- string? Read(IDirectoryInfo directoryInfo, string path)
- {
- var gitPath = Git(directoryInfo, path).FullName;
- return !fileSystem.File.Exists(gitPath)
- ? null
- : fileSystem.File.ReadAllText(gitPath).Trim(Environment.NewLine.ToCharArray());
- }
-
- string BranchTrackingRemote(string b, IniFile c)
- {
- var sections = c.GetSections();
- var branchSection = $"branch \"{b}\"";
- if (!sections.Contains(branchSection))
- return string.Empty;
+ ///
+ /// Returns for test setups that use a mock filesystem but do not seed a
+ /// real git layout — either no .git entry at all, or a bare .git directory without
+ /// a config file. Tests that only add .git/ to make FindGitRoot succeed
+ /// intentionally fall into the second case; they do not need real git metadata.
+ ///
+ /// These test setups pre-date testable git resolution and continue to receive the canned test
+ /// instance so they do not need to be updated. A .git file (worktree pointer) is
+ /// excluded: tests that seed a worktree pointer are explicitly modelling a worktree layout and
+ /// expect real resolution or .
+ ///
+ ///
+ private static bool IsLegacyTestWithoutGitLayout(IFileSystem fileSystem, IDirectoryInfo source)
+ {
+ var gitPath = fileSystem.Path.Join(source.FullName, ".git");
+ var noGitEntry = !fileSystem.Directory.Exists(gitPath) && !fileSystem.File.Exists(gitPath);
+ var gitDirWithoutConfig = fileSystem.Directory.Exists(gitPath)
+ && !fileSystem.File.Exists(fileSystem.Path.Join(gitPath, "config"));
+ return noGitEntry || gitDirWithoutConfig;
+ }
- var remoteName = ini.GetSetting(branchSection, "remote")?.Trim();
+ private static string BranchTrackingRemote(string branch, IniFile config)
+ {
+ var sections = config.GetSections();
+ var branchSection = $"branch \"{branch}\"";
+ if (!sections.Contains(branchSection))
+ return string.Empty;
- var remoteSection = $"remote \"{remoteName}\"";
+ var remoteName = config.GetSetting(branchSection, "remote")?.Trim();
+ if (string.IsNullOrEmpty(remoteName))
+ return string.Empty;
- remote = ini.GetSetting(remoteSection, "url")?.Trim();
- return remote ?? string.Empty;
- }
+ var remoteSection = $"remote \"{remoteName}\"";
+ return config.GetSetting(remoteSection, "url")?.Trim() ?? string.Empty;
}
[GeneratedRegex(@"\.git$", RegexOptions.IgnoreCase)]
diff --git a/src/Elastic.Documentation.Tooling/Paths.cs b/src/Elastic.Documentation.Tooling/Paths.cs
index a3d7011301..fb3be94d69 100644
--- a/src/Elastic.Documentation.Tooling/Paths.cs
+++ b/src/Elastic.Documentation.Tooling/Paths.cs
@@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.IO.Abstractions;
+using System.Security;
using Elastic.Documentation.Extensions;
// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes
@@ -16,87 +17,62 @@ public static class Paths
public static readonly DirectoryInfo ApplicationData = GetApplicationFolder();
- ///
- /// Walks up from until a .git directory or file
- /// (worktree pointer) is found and returns that ancestor. Returns
- /// itself when no git root is found within the allowed depth.
- ///
- ///
- /// Depth protection: in release builds the .git anchor must be at most 1 directory
- /// above — documentation is not expected to live deep inside
- /// a repo. In debug builds a deeper .git is accepted when a *.slnx file is
- /// adjacent (developer running the binary from an IDE output directory).
- ///
- public static string FindGitRoot(string startPath)
- {
- var resolved = Path.IsPathRooted(startPath) ? startPath : Path.GetFullPath(startPath);
- var dir = Directory.Exists(resolved)
- ? new DirectoryInfo(resolved)
- : new DirectoryInfo(Path.GetDirectoryName(resolved) ?? resolved);
- var startDir = dir.FullName; // always a directory, used as fallback
- var depth = 0;
- while (dir != null)
- {
- var hasGit = dir.GetDirectories(".git").Length > 0 || dir.GetFiles(".git").Length > 0;
- if (hasGit)
- {
-#if DEBUG
- if (depth <= 1 || dir.GetFiles("*.slnx").Length > 0)
- return dir.FullName;
-#else
- if (depth <= 1)
- return dir.FullName;
-#endif
- // .git found but too deep — stop searching
- return startDir;
- }
- depth++;
- dir = dir.Parent;
- }
- return startDir;
- }
-
///
/// Walks up from via until
/// a .git directory or file (worktree pointer) is found.
/// Returns if no git root is found within the allowed depth.
///
- /// Directory to start the upward search from.
- ///
- /// Optional upper bound for the search. When provided, the walk may reach
- /// but never goes above it, replacing the fixed depth limit with a directory boundary.
- /// When , the original depth-1 limit applies.
+ /// Directory to start the upward search from (typically the docset anchor).
+ ///
+ /// Maximum number of parent directories to walk above
+ /// (default: 1, i.e. self or one parent). The depth is 0-based: at depth 0 we check
+ /// itself; at depth 1 its immediate parent, and so on.
///
///
- /// Without a ceiling the same depth protection as applies.
- /// With a ceiling the caller guarantees the boundary is trustworthy (e.g. the working directory
- /// root), so any .git found at or below it is accepted regardless of depth.
+ /// In DEBUG builds a .git found beyond is still accepted
+ /// when it has an adjacent *.slnx file — this covers the developer case of running a
+ /// binary from an IDE output directory (e.g. bin/Debug/net10.0/) where the solution
+ /// root is several levels up.
///
- public static IDirectoryInfo? FindGitRoot(IDirectoryInfo startDirectory, IDirectoryInfo? ceiling = null)
+ public static IDirectoryInfo? FindGitRoot(IDirectoryInfo startDirectory, int maxParents = 1)
{
var directory = startDirectory;
var depth = 0;
while (directory != null)
{
- if (ceiling is not null && !directory.IsSubPathOf(ceiling))
+ bool hasGit;
+ try
+ {
+ hasGit = directory.GetDirectories(".git").Length > 0
+ || directory.GetFiles(".git").Length > 0;
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // Directory does not exist in the (mock) filesystem — no .git here.
+ // Continue up the tree so the caller can decide.
+ hasGit = false;
+ }
+ catch (SecurityException)
+ {
+ // A ScopedFileSystem is blocking access to this directory (e.g. the scope root
+ // is the anchor itself so the parent is outside scope). Stop searching.
return null;
+ }
- var hasGit = directory.GetDirectories(".git").Length > 0
- || directory.GetFiles(".git").Length > 0;
if (hasGit)
{
- if (ceiling is not null)
- return directory;
#if DEBUG
- if (depth <= 1 || directory.GetFiles("*.slnx").Length > 0)
+ if (depth <= maxParents || directory.GetFiles("*.slnx").Length > 0)
return directory;
#else
- if (depth <= 1)
+ if (depth <= maxParents)
return directory;
#endif
- // .git found but too deep
+ // .git found but too deep — stop searching
return null;
}
+ if (depth >= maxParents)
+ return null;
depth++;
directory = directory.Parent;
}
@@ -120,15 +96,8 @@ private static DirectoryInfo DetermineWorkingDirectoryRoot()
|| directory.GetFiles(".git").Length > 0;
if (hasGit)
{
- // Only accept .git beyond 1 level up in debug when a *.slnx is adjacent
- // (developer running from IDE output directory such as bin/Debug/net10.0/).
-#if DEBUG
- if (depth <= 1 || directory.GetFiles("*.slnx").Length > 0)
- return directory;
-#else
if (depth <= 1)
return directory;
-#endif
// .git found but too deep — stop without adopting it
return cwd;
}
@@ -215,6 +184,61 @@ public static (IDirectoryInfo, IFileInfo) FindDocsFolderFromRoot(IFileSystem rea
return (docsFolder, configurationPath);
}
+ ///
+ /// Resolves the real git directory from a worktree pointer (.git file containing
+ /// gitdir: <path>). Handles both absolute and relative gitdir paths, and follows
+ /// commondir to the shared object store when present (linked/nested worktree).
+ ///
+ /// The filesystem to read through.
+ /// The .git file (worktree pointer) to read.
+ ///
+ /// On success, the resolved git directory (.git/ or the worktrees subdirectory's
+ /// parent when a commondir is present).
+ ///
+ /// when the pointer was read and resolved;
+ /// when the file is absent, malformed, or the resolved path does not exist.
+ public static bool TryReadGitDirPointer(IFileSystem fileSystem, IFileInfo gitFile, out IDirectoryInfo? gitDir)
+ {
+ gitDir = null;
+ if (!fileSystem.File.Exists(gitFile.FullName))
+ return false;
+
+ var text = fileSystem.File.ReadAllText(gitFile.FullName);
+ var firstLineBreak = text.IndexOfAny(['\r', '\n']);
+ var firstLine = (firstLineBreak >= 0 ? text[..firstLineBreak] : text).Trim();
+ if (!firstLine.StartsWith("gitdir:", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ var rawGitDir = firstLine["gitdir:".Length..].Trim();
+ if (string.IsNullOrEmpty(rawGitDir))
+ return false;
+
+ // Resolve relative paths against the directory that contains the .git file
+ var containingDir = gitFile.Directory?.FullName ?? string.Empty;
+ var resolvedGitDir = fileSystem.Path.IsPathFullyQualified(rawGitDir)
+ ? rawGitDir
+ : fileSystem.Path.GetFullPath(fileSystem.Path.Combine(containingDir, rawGitDir));
+
+ if (!fileSystem.Directory.Exists(resolvedGitDir))
+ return false;
+
+ // Follow commondir to reach the shared .git root (linked/nested worktrees)
+ var commonDirFile = fileSystem.Path.Combine(resolvedGitDir, "commondir");
+ if (fileSystem.File.Exists(commonDirFile))
+ {
+ var commonDirRelative = fileSystem.File.ReadAllText(commonDirFile).Trim();
+ var commonDir = fileSystem.Path.IsPathFullyQualified(commonDirRelative)
+ ? commonDirRelative
+ : fileSystem.Path.GetFullPath(fileSystem.Path.Combine(resolvedGitDir, commonDirRelative));
+
+ if (fileSystem.Directory.Exists(commonDir))
+ resolvedGitDir = commonDir;
+ }
+
+ gitDir = fileSystem.DirectoryInfo.New(resolvedGitDir);
+ return true;
+ }
+
/// Validates that is a single path segment with no separators or traversal components.
/// Throws when the value is blank, contains separators, or equals "." / "..".
public static void ValidateSinglePathSegment(string value, string paramName)
diff --git a/src/Elastic.Documentation/Extensions/IFileInfoExtensions.cs b/src/Elastic.Documentation/Extensions/IFileInfoExtensions.cs
index c3e1af48f7..8985ff2a23 100644
--- a/src/Elastic.Documentation/Extensions/IFileInfoExtensions.cs
+++ b/src/Elastic.Documentation/Extensions/IFileInfoExtensions.cs
@@ -101,6 +101,10 @@ public static bool IsCaseSensitiveFileSystem
public static bool IsSubPathOf(this IDirectoryInfo directory, IDirectoryInfo parentDirectory)
{
var cmp = IsCaseSensitiveFileSystem ? Ordinal : OrdinalIgnoreCase;
+ // Fast reject: if directory's full path is shorter than parentDirectory's it can't be a
+ // descendant (+1 accounts for a trailing separator on one side but not the other).
+ if (directory.FullName.Length + 1 < parentDirectory.FullName.Length)
+ return false;
var parent = directory;
do
{
@@ -112,6 +116,11 @@ public static bool IsSubPathOf(this IDirectoryInfo directory, IDirectoryInfo par
return false;
}
+ /// Constructs for both paths from
+ /// and delegates to .
+ public static bool IsSubPath(string path, string parent, IFileSystem fs) =>
+ fs.DirectoryInfo.New(path).IsSubPathOf(fs.DirectoryInfo.New(parent));
+
/// Checks if has parent directory , defaults to OrdinalIgnoreCase comparison
public static bool HasParent(this IDirectoryInfo directory, string parentName, StringComparison comparison = OrdinalIgnoreCase)
{
diff --git a/src/Elastic.Documentation/FileSystems/AssemblyWriteFileSystem.cs b/src/Elastic.Documentation/FileSystems/AssemblyWriteFileSystem.cs
new file mode 100644
index 0000000000..cdf2374ed7
--- /dev/null
+++ b/src/Elastic.Documentation/FileSystems/AssemblyWriteFileSystem.cs
@@ -0,0 +1,97 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Elastic.Documentation.Extensions;
+using Nullean.ScopedFileSystem;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Write scope for the assembler and codex pipeline. Sibling of DocumentationWriteFileSystem
+/// under ScopedFileSystem — it does not derive from
+/// DocumentationWriteFileSystem so that write slots typed to one cannot silently accept the other.
+///
+/// Roots: the checkouts directory and the output directory (if they are not already nested inside each
+/// other), plus for S3 upload staging.
+///
+///
+/// The checkouts root (e.g. AppData/checkouts/<source>).
+///
+/// Optional explicit output directory. When it falls outside , it is added
+/// as a second scope root; when , output is assumed to be under
+/// /.artifacts and is therefore already covered.
+///
+///
+/// The underlying filesystem. Defaults to a new when .
+/// Pass a mock in tests.
+///
+public class AssemblyWriteFileSystem(
+ IDirectoryInfo checkout,
+ IDirectoryInfo? output = null,
+ IFileSystem? inner = null)
+ : ScopedFileSystem(inner ?? new FileSystem(), BuildOptions(checkout, output, inner))
+{
+ ///
+ /// The per-user application data directory for elastic/docs-builder.
+ /// Computed inline (rather than via Paths) to avoid a circular project reference.
+ ///
+ private static string ApplicationDataPath
+ {
+ get
+ {
+ var localPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ if (string.IsNullOrEmpty(localPath))
+ localPath = System.IO.Path.GetTempPath();
+ return System.IO.Path.Join(localPath, "elastic", "docs-builder");
+ }
+ }
+
+ private static ScopedFileSystemOptions BuildOptions(
+ IDirectoryInfo checkout,
+ IDirectoryInfo? output,
+ IFileSystem? inner)
+ {
+ var fs = inner ?? checkout.FileSystem;
+ var checkoutPath = checkout.FullName;
+ var roots = new List { checkoutPath };
+
+ // AppData is disjointness-filtered: on CI the checkouts directory lives inside AppData
+ // (/home/runner/.local/share/elastic/docs-builder/checkouts/...), so AppData would subsume
+ // checkoutPath and trigger ValidateRootsAreDisjoint.
+ var appData = ApplicationDataPath;
+ if (!IDirectoryInfoExtensions.IsSubPath(appData, checkoutPath, fs)
+ && !IDirectoryInfoExtensions.IsSubPath(checkoutPath, appData, fs))
+ {
+ roots.Add(appData);
+ }
+
+ if (output is not null && !IDirectoryInfoExtensions.IsSubPath(output.FullName, checkoutPath, fs))
+ roots.Add(output.FullName);
+
+ // MockFileSystem hardcodes its temp path ("C:\temp" on Windows, unix-ified to "/temp/"
+ // elsewhere) instead of calling System.IO.Path.GetTempPath(). AllowedSpecialFolder.Temp uses
+ // the real GetTempPath() (e.g. "/tmp/" on Linux, "C:\Users\\AppData\Local\Temp" on
+ // Windows), so the two diverge on every OS and scope validation fails for any path created
+ // via mockFs.Path.GetTempPath().
+ //
+ // Fix tracked upstream: https://github.com/TestableIO/System.IO.Abstractions/pull/1454
+ // Once that ships and we update the package reference we can drop this workaround.
+ var innerType = fs is ScopedFileSystem sf ? sf.InnerType : fs.GetType();
+ if (innerType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase))
+ {
+ var innerTemp = fs.Path.GetTempPath().TrimEnd(
+ System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
+ if (!string.IsNullOrEmpty(innerTemp) && !roots.Contains(innerTemp, StringComparer.OrdinalIgnoreCase))
+ roots.Add(innerTemp);
+ }
+
+ return new ScopedFileSystemOptions([.. roots])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".artifacts" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".doc.state", ".pagefind-net-frontend-version" },
+ AllowedSpecialFolders = AllowedSpecialFolder.Temp
+ };
+ }
+}
diff --git a/src/Elastic.Documentation/FileSystems/DocumentationWriteFileSystem.cs b/src/Elastic.Documentation/FileSystems/DocumentationWriteFileSystem.cs
new file mode 100644
index 0000000000..3620699bf0
--- /dev/null
+++ b/src/Elastic.Documentation/FileSystems/DocumentationWriteFileSystem.cs
@@ -0,0 +1,103 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using Elastic.Documentation.Extensions;
+using Nullean.ScopedFileSystem;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Write scope for a documentation set or checkout tree. Sibling of DocumentationFileSystem
+/// under ScopedFileSystem — it does not derive from
+/// DocumentationFileSystem so that write slots typed to this class cannot silently accept
+/// the read aggregate, and vice versa.
+///
+/// The write scope intentionally omits .git from :
+/// nothing in the build output pipeline should ever write into git repository metadata.
+///
+///
+///
+/// Constructs the write scope for a documentation set.
+///
+/// The repository checkout root (the directory containing .git).
+///
+/// Optional explicit output directory. When it falls outside (e.g.
+/// --output /tmp/build), it is added as a second scope root. When ,
+/// output is assumed to be under /.artifacts and is therefore already covered.
+///
+///
+/// The underlying filesystem. Defaults to a new when .
+/// Pass a mock in tests.
+///
+public class DocumentationWriteFileSystem(
+ IDirectoryInfo checkout,
+ IDirectoryInfo? output = null,
+ IFileSystem? inner = null)
+ : ScopedFileSystem(inner ?? new FileSystem(), BuildOptions(checkout, output, inner))
+{
+
+ ///
+ /// The per-user application data directory for elastic/docs-builder.
+ /// Same value as Paths.ApplicationData.FullName from the Tooling project, computed here to
+ /// avoid a circular project reference.
+ ///
+ private static string ApplicationDataPath
+ {
+ get
+ {
+ var localPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ if (string.IsNullOrEmpty(localPath))
+ localPath = System.IO.Path.GetTempPath();
+ return System.IO.Path.Join(localPath, "elastic", "docs-builder");
+ }
+ }
+
+ private static ScopedFileSystemOptions BuildOptions(
+ IDirectoryInfo checkout,
+ IDirectoryInfo? output,
+ IFileSystem? inner)
+ {
+ var fs = inner ?? checkout.FileSystem;
+ var checkoutPath = checkout.FullName;
+ var roots = new List { checkoutPath };
+
+ // AppData is disjointness-filtered: on CI each docset checkout lives inside AppData
+ // (/home/runner/.local/share/elastic/docs-builder/checkouts/current/), so AppData would
+ // subsume checkoutPath and trigger ValidateRootsAreDisjoint.
+ var appData = ApplicationDataPath;
+ if (!IDirectoryInfoExtensions.IsSubPath(appData, checkoutPath, fs)
+ && !IDirectoryInfoExtensions.IsSubPath(checkoutPath, appData, fs))
+ {
+ roots.Add(appData);
+ }
+
+ if (output is not null && !IDirectoryInfoExtensions.IsSubPath(output.FullName, checkout.FullName, fs))
+ roots.Add(output.FullName);
+
+ // MockFileSystem hardcodes its temp path ("C:\temp" on Windows, unix-ified to "/temp/"
+ // elsewhere) instead of calling System.IO.Path.GetTempPath(). AllowedSpecialFolder.Temp uses
+ // the real GetTempPath() (e.g. "/tmp/" on Linux, "C:\Users\\AppData\Local\Temp" on
+ // Windows), so the two diverge on every OS and scope validation fails for any path created
+ // via mockFs.Path.GetTempPath().
+ //
+ // Fix tracked upstream: https://github.com/TestableIO/System.IO.Abstractions/pull/1454
+ // Once that ships and we update the package reference we can drop this workaround.
+ var innerType = fs is ScopedFileSystem sf ? sf.InnerType : fs.GetType();
+ if (innerType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase))
+ {
+ var innerTemp = fs.Path.GetTempPath().TrimEnd(
+ System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
+ if (!string.IsNullOrEmpty(innerTemp) && !roots.Contains(innerTemp, StringComparer.OrdinalIgnoreCase))
+ roots.Add(innerTemp);
+ }
+
+ return new ScopedFileSystemOptions([.. roots])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".artifacts" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".doc.state", ".pagefind-net-frontend-version" },
+ AllowedSpecialFolders = AllowedSpecialFolder.Temp
+ };
+ }
+}
diff --git a/src/Elastic.Documentation/FileSystems/IDocumentationFileSystem.cs b/src/Elastic.Documentation/FileSystems/IDocumentationFileSystem.cs
new file mode 100644
index 0000000000..3bb6fe9984
--- /dev/null
+++ b/src/Elastic.Documentation/FileSystems/IDocumentationFileSystem.cs
@@ -0,0 +1,16 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+
+namespace Elastic.Documentation.FileSystems;
+
+///
+/// Marker interface for the documentation-set scope: a single checked-out repository
+/// anchored to a docset.yml. Only DocumentationFileSystem implements
+/// this; declaring it on parameters ensures the compiler rejects an assembler-scope
+/// ICheckoutsFileSystem or a changelog-scope IChangelogFileSystem
+/// where a docset-anchored read scope is required.
+///
+public interface IDocumentationFileSystem : IFileSystem;
diff --git a/src/Elastic.Documentation/IDocumentationContext.cs b/src/Elastic.Documentation/IDocumentationContext.cs
index 9bc24b4f72..043f1b428c 100644
--- a/src/Elastic.Documentation/IDocumentationContext.cs
+++ b/src/Elastic.Documentation/IDocumentationContext.cs
@@ -4,15 +4,14 @@
using System.IO.Abstractions;
using Elastic.Documentation.Diagnostics;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Documentation;
public interface IDocumentationContext
{
IDiagnosticsCollector Collector { get; }
- ScopedFileSystem ReadFileSystem { get; }
- ScopedFileSystem WriteFileSystem { get; }
+ DocumentationWriteFileSystem WriteFileSystem { get; }
IDirectoryInfo OutputDirectory { get; }
IFileInfo ConfigurationPath { get; }
BuildType BuildType { get; }
@@ -20,6 +19,7 @@ public interface IDocumentationContext
public interface IDocumentationSetContext : IDocumentationContext
{
+ IDocumentationFileSystem ReadFileSystem { get; }
IDirectoryInfo DocumentationSourceDirectory { get; }
GitCheckoutInformation Git { get; }
diff --git a/src/Elastic.Markdown/DocumentationGenerator.cs b/src/Elastic.Markdown/DocumentationGenerator.cs
index e5d4f08fe4..9853605938 100644
--- a/src/Elastic.Markdown/DocumentationGenerator.cs
+++ b/src/Elastic.Markdown/DocumentationGenerator.cs
@@ -239,13 +239,8 @@ private void CopyBrandingResources()
private void CopyFileAcrossFileSystems(IFileInfo source, IFileInfo destination)
{
- if (Context.ReadFileSystem == _writeFileSystem)
- Context.ReadFileSystem.File.Copy(source.FullName, destination.FullName, overwrite: true);
- else
- {
- var bytes = Context.ReadFileSystem.File.ReadAllBytes(source.FullName);
- _writeFileSystem.File.WriteAllBytes(destination.FullName, bytes);
- }
+ var bytes = Context.ReadFileSystem.File.ReadAllBytes(source.FullName);
+ _writeFileSystem.File.WriteAllBytes(destination.FullName, bytes);
}
private void HintUnusedSubstitutionKeys()
diff --git a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs
index d8a732e33d..a67938efa0 100644
--- a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs
+++ b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs
@@ -210,9 +210,9 @@ public async ValueTask ExportAsync(MarkdownExportFileContext fileContext,
var gitHubRepo = fileContext.BuildContext.Git.GitHubRepository;
var branch = fileContext.BuildContext.Git.Branch;
if (gitHubRepo is not null
- && fileContext.BuildContext.Git != GitCheckoutInformation.Unavailable
- && fileContext.BuildContext.DocumentationCheckoutDirectory is { } checkoutDirectory)
+ && fileContext.BuildContext.Git != GitCheckoutInformation.Unavailable)
{
+ var checkoutDirectory = fileContext.BuildContext.DocumentationCheckoutDirectory;
var relativeSourcePath = Path.GetRelativePath(
checkoutDirectory.FullName,
fileContext.BuildContext.DocumentationSourceDirectory.FullName);
diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs
index fb779c815f..f3a7c1b07b 100644
--- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs
+++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs
@@ -164,7 +164,7 @@ BuildContext build
private static IFileInfo GetRuleSourcePath(IFileInfo rulePath, BuildContext build)
{
- var checkoutDir = build.DocumentationCheckoutDirectory ?? build.DocumentationSourceDirectory.Parent!;
+ var checkoutDir = build.DocumentationCheckoutDirectory;
var relative = Path.GetRelativePath(checkoutDir.FullName, rulePath.FullName);
var newPath = Path.Join(build.DocumentationSourceDirectory.FullName, relative);
var md = Path.ChangeExtension(newPath, ".md");
diff --git a/src/Elastic.Markdown/HtmlWriter.cs b/src/Elastic.Markdown/HtmlWriter.cs
index c587fd11d7..72135f1680 100644
--- a/src/Elastic.Markdown/HtmlWriter.cs
+++ b/src/Elastic.Markdown/HtmlWriter.cs
@@ -94,8 +94,9 @@ private async Task RenderLayout(MarkdownFile markdown, MarkdownDoc
var gitHubRepo = DocumentationSet.Context.Git.GitHubRepository;
var branch = DocumentationSet.Context.Git.Branch;
string? editUrl = null;
- if (gitHubRepo is not null && DocumentationSet.Context.Git != GitCheckoutInformation.Unavailable && DocumentationSet.Context.DocumentationCheckoutDirectory is { } checkoutDirectory)
+ if (gitHubRepo is not null && DocumentationSet.Context.Git != GitCheckoutInformation.Unavailable)
{
+ var checkoutDirectory = DocumentationSet.Context.DocumentationCheckoutDirectory;
var relativeSourcePath = Path.GetRelativePath(checkoutDirectory.FullName, DocumentationSet.Context.DocumentationSourceDirectory.FullName);
var path = UrlPath.Join(relativeSourcePath, markdown.RelativePath);
editUrl = $"https://github.com/{gitHubRepo}/edit/{branch}/{path}";
@@ -158,9 +159,9 @@ private async Task RenderLayout(MarkdownFile markdown, MarkdownDoc
var gitRef = DocumentationSet.Context.Git.Ref;
string? gitHubDocsUrl = null;
if (gitHubRepo is not null
- && !string.IsNullOrEmpty(gitBranch) && gitBranch != "unavailable"
- && DocumentationSet.Context.DocumentationCheckoutDirectory is { } docsCheckoutDir)
+ && !string.IsNullOrEmpty(gitBranch) && gitBranch != "unavailable")
{
+ var docsCheckoutDir = DocumentationSet.Context.DocumentationCheckoutDirectory;
var relativeDocsPath = Path.GetRelativePath(docsCheckoutDir.FullName, DocumentationSet.Context.DocumentationSourceDirectory.FullName)
.Replace(Path.DirectorySeparatorChar, '/');
gitHubDocsUrl = $"https://github.com/{gitHubRepo}/tree/{gitBranch}/{relativeDocsPath}";
diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs
index 2e6fc5d937..ac670be56b 100644
--- a/src/Elastic.Markdown/IO/DocumentationSet.cs
+++ b/src/Elastic.Markdown/IO/DocumentationSet.cs
@@ -91,7 +91,9 @@ public DocumentationSet(
Name = Context.Git != GitCheckoutInformation.Unavailable
? Context.Git.RepositoryName
- : Context.DocumentationCheckoutDirectory?.Name ?? $"unknown-{Context.DocumentationSourceDirectory.Name}";
+ : Context.DocumentationCheckoutDirectory.Name
+ ?? Context.DocumentationSourceDirectory.Parent?.Name
+ ?? Context.DocumentationSourceDirectory.Name;
OutputStateFile = OutputDirectory.FileSystem.FileInfo.New(Path.Join(OutputDirectory.FullName, ".doc.state"));
LinkReferenceFile = OutputDirectory.FileSystem.FileInfo.New(Path.Join(OutputDirectory.FullName, "links.json"));
diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs
index 97f2794f90..9f054ea8a4 100644
--- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs
+++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs
@@ -404,7 +404,7 @@ private void LoadConfiguration() =>
/// against this same root.
///
private IDirectoryInfo ConfigTrustRoot =>
- Build.DocumentationCheckoutDirectory ?? Build.DocumentationSourceDirectory;
+ Build.DocumentationCheckoutDirectory;
private string? ResolveConfigPath()
{
diff --git a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvReader.cs b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvReader.cs
index 12f6b1d68f..771a533a39 100644
--- a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvReader.cs
+++ b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvReader.cs
@@ -5,17 +5,12 @@
using System.IO.Abstractions;
using Elastic.Documentation.Configuration;
using nietras.SeparatedValues;
-using Nullean.ScopedFileSystem;
namespace Elastic.Markdown.Myst.Directives.CsvInclude;
public static class CsvReader
{
- public static IEnumerable ReadCsvFile(string filePath, string separator, ScopedFileSystem? fileSystem = null)
- {
- var fs = fileSystem ?? FileSystemFactory.RealRead;
- return ReadWithSep(filePath, separator, fs);
- }
+ public static IEnumerable ReadCsvFile(string filePath, string separator, IFileSystem fileSystem) => ReadWithSep(filePath, separator, fileSystem);
private static IEnumerable ReadWithSep(string filePath, string separator, IFileSystem fileSystem)
{
diff --git a/src/authoring/Elastic.Documentation.Refactor/FormatService.cs b/src/authoring/Elastic.Documentation.Refactor/FormatService.cs
index 591a8c8da9..ef8d6d4e84 100644
--- a/src/authoring/Elastic.Documentation.Refactor/FormatService.cs
+++ b/src/authoring/Elastic.Documentation.Refactor/FormatService.cs
@@ -5,6 +5,7 @@
using System.IO.Abstractions;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Links.CrossLinks;
using Elastic.Documentation.Refactor.Formatters;
using Elastic.Documentation.Services;
@@ -40,7 +41,8 @@ Cancel ctx
)
{
// Create BuildContext to load the documentation set
- var context = new BuildContext(collector, fs, fs, configurationContext, ExportOptions.MetadataOnly, path, null);
+ var docFs = DocumentationFileSystem.Resolve(path);
+ var context = new BuildContext(collector, docFs, configurationContext) { AvailableExporters = ExportOptions.MetadataOnly };
var set = new DocumentationSet(context, logFactory, NoopCrossLinkResolver.Instance);
var mode = checkOnly ? "Checking" : "Formatting";
diff --git a/src/authoring/Elastic.Documentation.Refactor/MoveFileService.cs b/src/authoring/Elastic.Documentation.Refactor/MoveFileService.cs
index 91fbaedd56..2c037d0104 100644
--- a/src/authoring/Elastic.Documentation.Refactor/MoveFileService.cs
+++ b/src/authoring/Elastic.Documentation.Refactor/MoveFileService.cs
@@ -5,6 +5,7 @@
using System.IO.Abstractions;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Links.CrossLinks;
using Elastic.Documentation.Services;
using Elastic.Markdown.IO;
@@ -28,7 +29,8 @@ public async Task Move(
Cancel ctx
)
{
- var context = new BuildContext(collector, fs, fs, configurationContext, ExportOptions.MetadataOnly, path, null);
+ var docFs = DocumentationFileSystem.Resolve(path);
+ var context = new BuildContext(collector, docFs, configurationContext) { AvailableExporters = ExportOptions.MetadataOnly };
var set = new DocumentationSet(context, logFactory, NoopCrossLinkResolver.Instance);
diff --git a/src/authoring/Elastic.Documentation.Refactor/Tracking/LocalChangesService.cs b/src/authoring/Elastic.Documentation.Refactor/Tracking/LocalChangesService.cs
index c81f182790..1882bf3f14 100644
--- a/src/authoring/Elastic.Documentation.Refactor/Tracking/LocalChangesService.cs
+++ b/src/authoring/Elastic.Documentation.Refactor/Tracking/LocalChangesService.cs
@@ -7,6 +7,7 @@
using Elastic.Documentation.Configuration.Builder;
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Extensions;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using Nullean.ScopedFileSystem;
@@ -24,7 +25,8 @@ public Task ValidateRedirects(IDiagnosticsCollector collector, string? pat
{
var runningOnCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"));
- var buildContext = new BuildContext(collector, fs, fs, configurationContext, ExportOptions.MetadataOnly, path, null);
+ var docFs = DocumentationFileSystem.Resolve(path);
+ var buildContext = new BuildContext(collector, docFs, configurationContext) { AvailableExporters = ExportOptions.MetadataOnly };
var redirectFile = new RedirectFile(buildContext);
if (!redirectFile.Source.Exists)
{
diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs
index 4ea6feaa51..6cdd7f54ff 100644
--- a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs
+++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs
@@ -13,10 +13,10 @@
using Elastic.Documentation.Configuration.ReleaseNotes;
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Extensions;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Bundling;
@@ -56,7 +56,7 @@ public record AmendBundleArguments
///
public partial class ChangelogBundleAmendService(
ILoggerFactory logFactory,
- ScopedFileSystem? fileSystem = null,
+ IChangelogFileSystem fileSystem,
IConfigurationContext? configurationContext = null) : IService
{
///
@@ -65,9 +65,9 @@ public partial class ChangelogBundleAmendService(
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
+ private readonly IChangelogFileSystem _fileSystem = fileSystem;
private readonly ChangelogConfigurationLoader? _configLoader = configurationContext != null
- ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead)
+ ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem)
: null;
[GeneratedRegex(@"\.amend-(\d+)\.ya?ml$", RegexOptions.IgnoreCase)]
diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs
index e48b560106..c9b9e65bda 100644
--- a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs
+++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs
@@ -15,10 +15,10 @@
using Elastic.Documentation.Configuration.ReleaseNotes;
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Extensions;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Bundling;
@@ -165,8 +165,8 @@ public record BundlePlanResult
///
public partial class ChangelogBundlingService(
ILoggerFactory logFactory,
+ IChangelogFileSystem fileSystem,
IConfigurationContext? configurationContext = null,
- ScopedFileSystem? fileSystem = null,
IGitHubReleaseService? releaseService = null,
CdnChangelogEntryFetcher? entryFetcher = null,
IGitHubPrService? prService = null,
@@ -174,13 +174,13 @@ public partial class ChangelogBundlingService(
: IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly ScopedFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
+ private readonly IChangelogFileSystem _fileSystem = fileSystem;
private readonly IGitHubReleaseService _releaseService = releaseService ?? new GitHubReleaseService(logFactory);
private readonly CdnChangelogEntryFetcher _entryFetcher = entryFetcher ?? new CdnChangelogEntryFetcher(logFactory);
private readonly IGitHubPrService _prService = prService ?? new GitHubPrService(logFactory);
private readonly IGitHubCommitRangeService _commitRangeService = commitRangeService ?? new GitHubCommitRangeService(logFactory);
private readonly ChangelogConfigurationLoader? _configLoader = configurationContext != null
- ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead)
+ ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem)
: null;
// Defaults applied when sourcing CDN entries and the org/branch are not otherwise resolvable.
diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs
index d73b0089d2..a39a9c4a8b 100644
--- a/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs
+++ b/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs
@@ -8,9 +8,9 @@
using Elastic.Documentation.Configuration.Changelog;
using Elastic.Documentation.Configuration.ReleaseNotes;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Bundling;
@@ -60,16 +60,16 @@ public record ChangelogRemoveArguments
///
public class ChangelogRemoveService(
ILoggerFactory logFactory,
+ IChangelogFileSystem fileSystem,
IConfigurationContext? configurationContext = null,
- ScopedFileSystem? fileSystem = null,
IGitHubReleaseService? releaseService = null)
: IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly ScopedFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
+ private readonly IChangelogFileSystem _fileSystem = fileSystem;
private readonly IGitHubReleaseService _releaseService = releaseService ?? new GitHubReleaseService(logFactory);
private readonly ChangelogConfigurationLoader? _configLoader = configurationContext != null
- ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead)
+ ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem)
: null;
public async Task RemoveChangelogs(IDiagnosticsCollector collector, ChangelogRemoveArguments input, Cancel ctx)
diff --git a/src/services/Elastic.Changelog/Bundling/ProfileFilterResolver.cs b/src/services/Elastic.Changelog/Bundling/ProfileFilterResolver.cs
index ccedc482d4..004fb1b175 100644
--- a/src/services/Elastic.Changelog/Bundling/ProfileFilterResolver.cs
+++ b/src/services/Elastic.Changelog/Bundling/ProfileFilterResolver.cs
@@ -8,10 +8,10 @@
using Elastic.Changelog.GitHub;
using Elastic.Documentation.Configuration.Changelog;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Bundling;
@@ -83,7 +83,7 @@ public static partial class ProfileFilterResolver
string profileName,
string? profileArgument,
ChangelogConfiguration? config,
- ScopedFileSystem fileSystem,
+ IChangelogFileSystem fileSystem,
ILogger? logger,
Cancel ctx,
string? profileReport = null,
@@ -201,7 +201,7 @@ public static partial class ProfileFilterResolver
string profileArgument,
string profileReport,
BundleProfile profile,
- ScopedFileSystem fileSystem,
+ IChangelogFileSystem fileSystem,
ILogger? logger,
Cancel ctx)
{
@@ -282,7 +282,7 @@ public static partial class ProfileFilterResolver
internal static async Task ResolveListFileAsync(
IDiagnosticsCollector collector,
string filePath,
- ScopedFileSystem fileSystem,
+ IChangelogFileSystem fileSystem,
Cancel ctx)
{
var content = await fileSystem.File.ReadAllTextAsync(filePath, ctx);
@@ -356,7 +356,7 @@ public static partial class ProfileFilterResolver
: new ListFileResult(null, lines, null);
}
- private static ProfileArgumentType DetectLocalFileType(ScopedFileSystem fileSystem, string path) =>
+ private static ProfileArgumentType DetectLocalFileType(IChangelogFileSystem fileSystem, string path) =>
fileSystem.Path.GetExtension(path).ToLowerInvariant() is ".html" or ".htm"
? ProfileArgumentType.PromotionReportFile
: ProfileArgumentType.UrlListFile;
diff --git a/src/services/Elastic.Changelog/Bundling/PromotionReportParser.cs b/src/services/Elastic.Changelog/Bundling/PromotionReportParser.cs
index 12e9091ac6..9a2c03e1c0 100644
--- a/src/services/Elastic.Changelog/Bundling/PromotionReportParser.cs
+++ b/src/services/Elastic.Changelog/Bundling/PromotionReportParser.cs
@@ -7,18 +7,18 @@
using System.Text.RegularExpressions;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Bundling;
///
/// Parser for promotion report HTML files to extract PR lists
///
-public partial class PromotionReportParser(ILoggerFactory logFactory, ScopedFileSystem? fileSystem = null)
+public partial class PromotionReportParser(ILoggerFactory logFactory, IChangelogFileSystem fileSystem)
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
+ private readonly IChangelogFileSystem _fileSystem = fileSystem;
private static readonly string[] AllowedHosts = ["github.com", "buildkite.com"];
diff --git a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs
index a0397fca24..7a435eb8d6 100644
--- a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs
+++ b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs
@@ -9,9 +9,9 @@
using Elastic.Documentation.Configuration.Changelog;
using Elastic.Documentation.Configuration.Inference;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Creation;
@@ -71,17 +71,17 @@ public record CreateChangelogArguments
public class ChangelogCreationService(
ILoggerFactory logFactory,
IConfigurationContext configurationContext,
+IChangelogFileSystem fileSystem,
IGitHubPrService? githubPrService = null,
-ScopedFileSystem? fileSystem = null,
IEnvironmentVariables? env = null
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead);
+ private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem);
private readonly CreateChangelogArgumentsValidator _validator = new(configurationContext);
private readonly PrInfoProcessor _prProcessor = new(githubPrService, logFactory.CreateLogger());
private readonly IssueInfoProcessor _issueProcessor = new(githubPrService, logFactory.CreateLogger());
- private readonly ChangelogFileWriter _fileWriter = new(fileSystem ?? FileSystemFactory.RealWrite, logFactory.CreateLogger());
+ private readonly ChangelogFileWriter _fileWriter = new(fileSystem, logFactory.CreateLogger());
private readonly ProductInferService _productInferService = new(
configurationContext.ProductsConfiguration);
diff --git a/src/services/Elastic.Changelog/Evaluation/ChangelogArtifactEvaluationService.cs b/src/services/Elastic.Changelog/Evaluation/ChangelogArtifactEvaluationService.cs
index 012fee7fd8..7ea408b3ca 100644
--- a/src/services/Elastic.Changelog/Evaluation/ChangelogArtifactEvaluationService.cs
+++ b/src/services/Elastic.Changelog/Evaluation/ChangelogArtifactEvaluationService.cs
@@ -10,6 +10,7 @@
using Elastic.Changelog.GitHub;
using Elastic.Changelog.Utilities;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
@@ -20,11 +21,11 @@ public class ChangelogArtifactEvaluationService(
ILoggerFactory logFactory,
IGitHubPrService gitHubPrService,
ICoreService coreService,
- IFileSystem? fileSystem = null
+ IRunnerTempFileSystem fileSystem
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystem();
+ private readonly IRunnerTempFileSystem _fileSystem = fileSystem;
public async Task EvaluateArtifact(IDiagnosticsCollector collector, EvaluateArtifactArguments input, Cancel ctx)
{
diff --git a/src/services/Elastic.Changelog/Evaluation/ChangelogPrBodyReader.cs b/src/services/Elastic.Changelog/Evaluation/ChangelogPrBodyReader.cs
index e0177b142b..5a33e224fb 100644
--- a/src/services/Elastic.Changelog/Evaluation/ChangelogPrBodyReader.cs
+++ b/src/services/Elastic.Changelog/Evaluation/ChangelogPrBodyReader.cs
@@ -7,6 +7,7 @@
using System.Security;
using System.Text;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Changelog.Evaluation;
@@ -22,7 +23,7 @@ public static class ChangelogPrBodyReader
public static async Task ReadAsync(
string? prBodyFile,
IDiagnosticsCollector collector,
- IFileSystem fileSystem,
+ IRunnerTempFileSystem fileSystem,
CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(prBodyFile))
diff --git a/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs b/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs
index 2b1c78eda7..b572df850a 100644
--- a/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs
+++ b/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs
@@ -11,10 +11,10 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Changelog;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Evaluation;
@@ -24,12 +24,12 @@ public class ChangelogPrEvaluationService(
IConfigurationContext configurationContext,
IGitHubPrService gitHubPrService,
ICoreService coreService,
- ScopedFileSystem? fileSystem = null
+ IRunnerTempFileSystem fileSystem
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
- private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead);
+ private readonly IRunnerTempFileSystem _fileSystem = fileSystem;
+ private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem);
public async Task EvaluatePr(IDiagnosticsCollector collector, EvaluatePrArguments input, Cancel ctx)
{
diff --git a/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs b/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs
index 9fbd1c8cff..0773c20ce6 100644
--- a/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs
+++ b/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs
@@ -10,6 +10,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Changelog;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
@@ -20,7 +21,7 @@ public class ChangelogPrepareArtifactService(
ILoggerFactory logFactory,
IConfigurationContext configurationContext,
ICoreService coreService,
- IFileSystem? fileSystem = null
+ IRunnerTempFileSystem fileSystem
) : IService
{
///
@@ -29,8 +30,8 @@ public class ChangelogPrepareArtifactService(
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystem();
- private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem ?? new FileSystem());
+ private readonly IRunnerTempFileSystem _fileSystem = fileSystem;
+ private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem);
public async Task PrepareArtifact(IDiagnosticsCollector collector, PrepareArtifactArguments input, Cancel ctx)
{
diff --git a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs
index 1784f98dcb..99cb0750ba 100644
--- a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs
+++ b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs
@@ -13,10 +13,10 @@
using Elastic.Documentation.Configuration.Changelog;
using Elastic.Documentation.Configuration.ReleaseNotes;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.GithubRelease;
@@ -80,9 +80,9 @@ public record CreateChangelogsFromReleaseArguments
public class GitHubReleaseChangelogService(
ILoggerFactory logFactory,
IConfigurationContext configurationContext,
+ IChangelogFileSystem fileSystem,
IGitHubReleaseService? releaseService = null,
IGitHubPrService? prService = null,
- ScopedFileSystem? fileSystem = null,
ChangelogBundlingService? bundlingService = null
) : IService
{
@@ -92,11 +92,11 @@ public class GitHubReleaseChangelogService(
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
- private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead);
+ private readonly IChangelogFileSystem _fileSystem = fileSystem;
+ private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem);
private readonly IGitHubReleaseService _releaseService = releaseService ?? new GitHubReleaseService(logFactory);
private readonly IGitHubPrService _prService = prService ?? new GitHubPrService(logFactory);
- private readonly ChangelogBundlingService _bundlingService = bundlingService ?? new ChangelogBundlingService(logFactory, configurationContext, fileSystem);
+ private readonly ChangelogBundlingService _bundlingService = bundlingService ?? new ChangelogBundlingService(logFactory, fileSystem, configurationContext);
public async Task CreateChangelogsFromRelease(
IDiagnosticsCollector collector,
diff --git a/src/services/Elastic.Changelog/Migration/WebMigrationService.cs b/src/services/Elastic.Changelog/Migration/WebMigrationService.cs
index 5b9aae036e..5f25cdf725 100644
--- a/src/services/Elastic.Changelog/Migration/WebMigrationService.cs
+++ b/src/services/Elastic.Changelog/Migration/WebMigrationService.cs
@@ -11,7 +11,6 @@
using Amazon.S3;
using Amazon.S3.Model;
using Elastic.Documentation;
-using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.ReleaseNotes;
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Services;
@@ -48,7 +47,7 @@ public sealed record MigrationKeyResult(string Key, string Outcome, string? ETag
///
public class WebMigrationService(
ILoggerFactory logFactory,
- ScopedFileSystem? fileSystem = null,
+ ScopedFileSystem fileSystem,
IAmazonS3? s3Client = null,
HttpMessageHandler? httpMessageHandler = null
) : IService
@@ -59,7 +58,7 @@ public class WebMigrationService(
private const string OutcomeFailed = "failed";
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealWrite;
+ private readonly IFileSystem _fileSystem = fileSystem;
/// Per-key results of the most recent run; exposed for tests.
internal IReadOnlyList LastResults { get; private set; } = [];
diff --git a/src/services/Elastic.Changelog/Rendering/ChangelogRenderer.cs b/src/services/Elastic.Changelog/Rendering/ChangelogRenderer.cs
index 5e609cd243..64281dedba 100644
--- a/src/services/Elastic.Changelog/Rendering/ChangelogRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/ChangelogRenderer.cs
@@ -5,15 +5,15 @@
using System.IO.Abstractions;
using Elastic.Changelog.Rendering.Asciidoc;
using Elastic.Changelog.Rendering.Markdown;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Rendering;
///
/// Coordinates rendering of changelog output to different formats.
///
-public class ChangelogRenderer(ScopedFileSystem fileSystem, ILogger logger)
+public class ChangelogRenderer(IChangelogFileSystem fileSystem, ILogger logger)
{
///
/// Renders changelog output based on the specified file type.
diff --git a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs
index cf83389001..b647abe7e4 100644
--- a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs
+++ b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs
@@ -9,12 +9,12 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Changelog;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
using Elastic.Documentation.Services;
using Elastic.Documentation.Versions;
using Microsoft.Extensions.Logging;
using NetEscapades.EnumGenerators;
-using Nullean.ScopedFileSystem;
using YamlDotNet.Core;
namespace Elastic.Changelog.Rendering;
@@ -70,12 +70,12 @@ public enum ChangelogFileType
///
public class ChangelogRenderingService(
ILoggerFactory logFactory,
- IConfigurationContext? configurationContext = null,
- ScopedFileSystem? fileSystem = null
+ IChangelogFileSystem fileSystem,
+ IConfigurationContext? configurationContext = null
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly ScopedFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealWrite;
+ private readonly IChangelogFileSystem _fileSystem = fileSystem;
public async Task RenderChangelogs(
IDiagnosticsCollector collector,
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs
index 1eae9745d4..46a3a235ff 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs
@@ -5,8 +5,8 @@
using System.IO.Abstractions;
using System.Text;
using Elastic.Documentation;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
-using Nullean.ScopedFileSystem;
using static System.Globalization.CultureInfo;
using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType;
@@ -15,7 +15,7 @@ namespace Elastic.Changelog.Rendering.Markdown;
///
/// Renderer for the breaking-changes.md changelog file
///
-public class BreakingChangesMarkdownRenderer(ScopedFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
+public class BreakingChangesMarkdownRenderer(IChangelogFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
{
///
public override string OutputFileName => "breaking-changes.md";
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs
index 9d0e40cfdd..a518d4d092 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs
@@ -5,8 +5,8 @@
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Text;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
-using Nullean.ScopedFileSystem;
using static System.Globalization.CultureInfo;
using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType;
@@ -15,7 +15,7 @@ namespace Elastic.Changelog.Rendering.Markdown;
///
/// Renderer for generating clean GitHub Flavored Markdown in a single changelog.md file
///
-public class ChangelogGfmRenderer(ScopedFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
+public class ChangelogGfmRenderer(IChangelogFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
{
///
public override string OutputFileName => "changelog.md";
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogMarkdownRenderer.cs
index c57d7b1162..5b6e54c3fa 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogMarkdownRenderer.cs
@@ -4,14 +4,14 @@
using System.Collections.Generic;
using System.IO.Abstractions;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Changelog.Rendering.Markdown;
///
/// Coordinates rendering of all markdown changelog files.
///
-public class ChangelogMarkdownRenderer(ScopedFileSystem fileSystem)
+public class ChangelogMarkdownRenderer(IChangelogFileSystem fileSystem)
{
///
/// Renders all markdown changelog files (index, breaking changes, deprecations, known issues, highlights).
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs
index 43843594d8..55c98cfe92 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs
@@ -4,8 +4,8 @@
using System.IO.Abstractions;
using System.Text;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
-using Nullean.ScopedFileSystem;
using static System.Globalization.CultureInfo;
using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType;
@@ -14,7 +14,7 @@ namespace Elastic.Changelog.Rendering.Markdown;
///
/// Renderer for the deprecations.md changelog file
///
-public class DeprecationsMarkdownRenderer(ScopedFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
+public class DeprecationsMarkdownRenderer(IChangelogFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
{
///
public override string OutputFileName => "deprecations.md";
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs
index 22d39044b9..404ffd3864 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs
@@ -4,8 +4,8 @@
using System.IO.Abstractions;
using System.Text;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
-using Nullean.ScopedFileSystem;
using static System.Globalization.CultureInfo;
namespace Elastic.Changelog.Rendering.Markdown;
@@ -13,7 +13,7 @@ namespace Elastic.Changelog.Rendering.Markdown;
///
/// Renderer for the highlights.md changelog file
///
-public class HighlightsMarkdownRenderer(ScopedFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
+public class HighlightsMarkdownRenderer(IChangelogFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
{
///
public override string OutputFileName => "highlights.md";
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs
index 318e36e884..dea2a32d79 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs
@@ -5,8 +5,8 @@
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Text;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
-using Nullean.ScopedFileSystem;
using static System.Globalization.CultureInfo;
using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType;
@@ -15,7 +15,7 @@ namespace Elastic.Changelog.Rendering.Markdown;
///
/// Renderer for the index.md changelog file containing features, enhancements, fixes, docs, regressions, and other changes
///
-public class IndexMarkdownRenderer(ScopedFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
+public class IndexMarkdownRenderer(IChangelogFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
{
///
public override string OutputFileName => "index.md";
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs
index b84e4945e7..1872a78c6a 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs
@@ -4,8 +4,8 @@
using System.IO.Abstractions;
using System.Text;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
-using Nullean.ScopedFileSystem;
using static System.Globalization.CultureInfo;
using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType;
@@ -14,7 +14,7 @@ namespace Elastic.Changelog.Rendering.Markdown;
///
/// Renderer for the known-issues.md changelog file
///
-public class KnownIssuesMarkdownRenderer(ScopedFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
+public class KnownIssuesMarkdownRenderer(IChangelogFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
{
///
public override string OutputFileName => "known-issues.md";
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs b/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs
index d545d6e502..c9b9529330 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs
@@ -5,8 +5,8 @@
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Text;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Rendering.Markdown;
@@ -24,9 +24,9 @@ public record PrIssueLinkOptions(
///
/// Abstract base class for changelog markdown renderers
///
-public abstract class MarkdownRendererBase(ScopedFileSystem fileSystem) : IChangelogMarkdownRenderer
+public abstract class MarkdownRendererBase(IChangelogFileSystem fileSystem) : IChangelogMarkdownRenderer
{
- protected ScopedFileSystem FileSystem { get; } = fileSystem;
+ protected IChangelogFileSystem FileSystem { get; } = fileSystem;
///
public abstract string OutputFileName { get; }
diff --git a/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs b/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs
index b859ebb156..33b3f149b8 100644
--- a/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs
+++ b/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs
@@ -9,10 +9,10 @@
using Elastic.Documentation.Configuration.Changelog;
using Elastic.Documentation.Configuration.ReleaseNotes;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Integrations.S3;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Uploading;
@@ -61,15 +61,15 @@ public record ChangelogUploadArguments
public class ChangelogUploadService(
ILoggerFactory logFactory,
+ IChangelogFileSystem fileSystem,
IConfigurationContext? configurationContext = null,
- ScopedFileSystem? fileSystem = null,
IAmazonS3? s3Client = null
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
+ private readonly IChangelogFileSystem _fileSystem = fileSystem;
private readonly ChangelogConfigurationLoader? _configLoader = configurationContext != null
- ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead)
+ ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem)
: null;
public async Task Upload(IDiagnosticsCollector collector, ChangelogUploadArguments args, Cancel ctx)
diff --git a/src/services/Elastic.Documentation.Assembler/AssembleContext.cs b/src/services/Elastic.Documentation.Assembler/AssembleContext.cs
index 2970240090..b28196b311 100644
--- a/src/services/Elastic.Documentation.Assembler/AssembleContext.cs
+++ b/src/services/Elastic.Documentation.Assembler/AssembleContext.cs
@@ -11,14 +11,14 @@
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Deploying.Synchronization;
using Elastic.Documentation.Diagnostics;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Documentation.Assembler;
public class AssembleContext : IDocumentationConfigurationContext, IDocsSyncContext
{
- public ScopedFileSystem ReadFileSystem { get; }
- public ScopedFileSystem WriteFileSystem { get; }
+ public CheckoutsFileSystem ReadFileSystem { get; }
+ public DocumentationWriteFileSystem WriteFileSystem { get; }
public IDiagnosticsCollector Collector { get; }
@@ -66,15 +66,14 @@ public AssembleContext(
IConfigurationContext configurationContext,
string environment,
IDiagnosticsCollector collector,
- ScopedFileSystem readFileSystem,
- ScopedFileSystem writeFileSystem,
- string? checkoutDirectory,
- string? output
+ CheckoutsFileSystem fileSystem,
+ string? checkoutDirectory = null,
+ string? output = null
)
{
Collector = collector;
- ReadFileSystem = readFileSystem;
- WriteFileSystem = writeFileSystem;
+ ReadFileSystem = fileSystem;
+ WriteFileSystem = fileSystem.Write;
Configuration = configuration;
ConfigurationFileProvider = configurationContext.ConfigurationFileProvider;
@@ -95,8 +94,8 @@ public AssembleContext(
var contentSource = Environment.ContentSource.ToStringFast(true);
var defaultCheckoutDirectory = Path.Join(Paths.ApplicationData.FullName, "checkouts", contentSource);
CheckoutDirectory = checkoutDirectory is null
- ? FileSystemFactory.AppData.DirectoryInfo.New(defaultCheckoutDirectory)
- : ReadFileSystem.DirectoryInfo.New(checkoutDirectory);
+ ? fileSystem.DirectoryInfo.New(defaultCheckoutDirectory)
+ : fileSystem.DirectoryInfo.New(checkoutDirectory);
var defaultOutputDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly");
OutputDirectory = WriteFileSystem.DirectoryInfo.New(output ?? defaultOutputDirectory);
diff --git a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs
index 5534fd7c98..610d47272b 100644
--- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs
@@ -12,11 +12,11 @@
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.LegacyDocs;
using Elastic.Documentation.Navigation.Assembler;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.Building;
@@ -34,8 +34,7 @@ IEnvironmentVariables environmentVariables
public async Task BuildAll(
IDiagnosticsCollector collector,
AssemblerBuildOptions options,
- ScopedFileSystem readFs,
- ScopedFileSystem writeFs,
+ CheckoutsFileSystem fileSystem,
Cancel ctx
)
{
@@ -62,7 +61,7 @@ Cancel ctx
_logger.LogInformation("Creating assemble context");
- var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, readFs, writeFs, null, null);
+ var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, fileSystem);
// --assume-build is not allowed on CI: it could serve stale content from a previous/cached build
// CI builds must always produce fresh, reproducible output
@@ -73,7 +72,7 @@ Cancel ctx
if (assumeBuild.GetValueOrDefault(false))
{
var indexHtmlPath = Path.Join(assembleContext.OutputDirectory.FullName, "docs", "index.html");
- if (assembleContext.OutputDirectory.Exists && readFs.File.Exists(indexHtmlPath))
+ if (assembleContext.OutputDirectory.Exists && fileSystem.File.Exists(indexHtmlPath))
{
_logger.LogInformation("Assuming build already exists (--assume-build). Found index.html at {Path}. Skipping build.", indexHtmlPath);
return true;
@@ -106,7 +105,7 @@ Cancel ctx
var assembleSources = await AssembleSources.AssembleAsync(logFactory, assembleContext, checkouts, configurationContext, exporters, ctx);
var navigationFileInfo = configurationContext.ConfigurationFileProvider.NavigationFile;
- var siteNavigationFile = SiteNavigationFile.Deserialize(await readFs.File.ReadAllTextAsync(navigationFileInfo.FullName, ctx));
+ var siteNavigationFile = SiteNavigationFile.Deserialize(await fileSystem.File.ReadAllTextAsync(navigationFileInfo.FullName, ctx));
var documentationSets = assembleSources.AssembleSets.Values.Select(s => s.DocumentationSet.Navigation).ToArray();
var navigation = new SiteNavigation(siteNavigationFile, assembleContext, documentationSets, assembleContext.Environment.PathPrefix);
@@ -128,7 +127,7 @@ Cancel ctx
await cloner.WriteLinkRegistrySnapshot(checkoutResult.LinkRegistrySnapshot, ctx);
var redirectsPath = Path.Join(assembleContext.OutputDirectory.FullName, "redirects.json");
- if (writeFs.File.Exists(redirectsPath))
+ if (assembleContext.WriteFileSystem.File.Exists(redirectsPath))
await githubActionsService.SetOutputAsync("redirects-artifact-path", redirectsPath);
if (exporters.Contains(Exporter.Html))
diff --git a/src/services/Elastic.Documentation.Assembler/Building/AssemblerSitemapService.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerSitemapService.cs
index 3e532d189f..7542c7bfd5 100644
--- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerSitemapService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerSitemapService.cs
@@ -7,12 +7,12 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Search;
using Elastic.Documentation.Search.Contract;
using Elastic.Documentation.Services;
using Elastic.Markdown.Exporters.Elasticsearch;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.Building;
@@ -27,7 +27,7 @@ ICoreService githubActionsService
public async Task GenerateSitemapAsync(
IDiagnosticsCollector collector,
- ScopedFileSystem fileSystem,
+ CheckoutsFileSystem fileSystem,
ElasticsearchIndexOptions es,
string? environment = null,
Cancel ctx = default
@@ -38,10 +38,7 @@ public async Task GenerateSitemapAsync(
_logger.LogInformation("Generating sitemap from ES index for environment {Environment}", environment);
- var assembleContext = new AssembleContext(
- assemblyConfiguration, configurationContext, environment, collector,
- fileSystem, fileSystem, null, null
- );
+ var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, fileSystem);
var cfg = configurationContext.Endpoints.Elasticsearch;
await ElasticsearchEndpointConfigurator.ApplyAsync(cfg, es, collector, fileSystem, ctx);
diff --git a/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryBuildMatchingService.cs b/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryBuildMatchingService.cs
index 7de85a5a8d..300e51a061 100644
--- a/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryBuildMatchingService.cs
+++ b/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryBuildMatchingService.cs
@@ -7,11 +7,11 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.LinkIndex;
using Elastic.Documentation.Links;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.ContentSources;
@@ -20,7 +20,7 @@ public class RepositoryBuildMatchingService(
AssemblyConfiguration configuration,
IConfigurationContext configurationContext,
ICoreService githubActionsService,
- ScopedFileSystem fileSystem
+ CheckoutsFileSystem fileSystem
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
@@ -71,7 +71,7 @@ public async Task ShouldBuild(IDiagnosticsCollector collector, string? rep
var linkRegistry = await GetRegistryWithRetry(linkIndexProvider, ctx);
var alreadyPublishing = linkRegistry.Repositories.ContainsKey(repositoryName);
_logger.LogInformation("'{Repository}' (registry key: '{RepositoryName}') publishing to link registry: {PublishState} ", repo, repositoryName, alreadyPublishing);
- var assembleContext = new AssembleContext(configuration, configurationContext, "dev", collector, fileSystem, fileSystem, null, null);
+ var assembleContext = new AssembleContext(configuration, configurationContext, "dev", collector, fileSystem);
var product = assembleContext.ProductsConfiguration.GetProductByRepositoryName(repo);
var matches = assembleContext.Configuration.Match(logFactory, repo, refName, product, alreadyPublishing);
if (matches is { Current: null, Next: null, Edge: null, Speculative: false })
diff --git a/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryPublishValidationService.cs b/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryPublishValidationService.cs
index 8f11b65b68..45dc7bb2b7 100644
--- a/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryPublishValidationService.cs
+++ b/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryPublishValidationService.cs
@@ -7,10 +7,10 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.LinkIndex;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.ContentSources;
@@ -18,7 +18,7 @@ public class RepositoryPublishValidationService(
ILoggerFactory logFactory,
AssemblyConfiguration configuration,
IConfigurationContext configurationContext,
- ScopedFileSystem fileSystem
+ CheckoutsFileSystem fileSystem
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
@@ -27,7 +27,7 @@ ScopedFileSystem fileSystem
public async Task ValidatePublishStatus(IDiagnosticsCollector collector, Cancel ctx)
{
// environment does not matter to check the configuration, defaulting to dev
- var context = new AssembleContext(configuration, configurationContext, "dev", collector, fileSystem, fileSystem, null, null);
+ var context = new AssembleContext(configuration, configurationContext, "dev", collector, fileSystem);
ILinkIndexReader linkIndexReader = Aws3LinkIndexReader.CreateAnonymous();
var fetcher = new AssemblerCrossLinkFetcher(logFactory, context.Configuration, context.Environment, linkIndexReader);
var links = await fetcher.FetchLinkRegistry(ctx);
diff --git a/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerAiEnrichService.cs b/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerAiEnrichService.cs
index a7c4395cef..36de6f119c 100644
--- a/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerAiEnrichService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerAiEnrichService.cs
@@ -6,12 +6,12 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Indexing;
using Elastic.Documentation.Services;
using Elastic.Ingest.Elasticsearch.Enrichment;
using Elastic.Markdown.Exporters.Elasticsearch;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.Indexing;
@@ -34,8 +34,7 @@ ICoreService githubActionsService
///
public async Task AiEnrich(
IDiagnosticsCollector collector,
- ScopedFileSystem readFs,
- ScopedFileSystem writeFs,
+ CheckoutsFileSystem fileSystem,
ElasticsearchIndexOptions es,
string? environment,
bool bootstrapOnly,
@@ -43,12 +42,12 @@ Cancel ctx
)
{
var cfg = configurationContext.Endpoints.Elasticsearch;
- await ElasticsearchEndpointConfigurator.ApplyAsync(cfg, es, collector, readFs, ctx);
+ await ElasticsearchEndpointConfigurator.ApplyAsync(cfg, es, collector, fileSystem, ctx);
var githubEnvironmentInput = githubActionsService.GetInput("environment");
environment ??= !string.IsNullOrEmpty(githubEnvironmentInput) ? githubEnvironmentInput : "dev";
- var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, readFs, writeFs, null, null);
+ var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, fileSystem);
using var exporter = new ElasticsearchMarkdownExporter(logFactory, collector, assembleContext.Endpoints, assembleContext);
if (!exporter.AiEnrichmentEnabled)
diff --git a/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerIndexService.cs b/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerIndexService.cs
index 02af5d5e7d..a77c7347a2 100644
--- a/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerIndexService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerIndexService.cs
@@ -8,8 +8,8 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
using static Elastic.Documentation.Exporter;
namespace Elastic.Documentation.Assembler.Indexing;
@@ -27,15 +27,14 @@ IEnvironmentVariables environmentVariables
/// Index assembled documentation to Elasticsearch.
public async Task Index(
IDiagnosticsCollector collector,
- ScopedFileSystem readFs,
- ScopedFileSystem writeFs,
+ CheckoutsFileSystem fileSystem,
ElasticsearchIndexOptions es,
string? environment = null,
Cancel ctx = default
)
{
var cfg = _configurationContext.Endpoints.Elasticsearch;
- await ElasticsearchEndpointConfigurator.ApplyAsync(cfg, es, collector, readFs, ctx);
+ await ElasticsearchEndpointConfigurator.ApplyAsync(cfg, es, collector, fileSystem, ctx);
return await BuildAll(collector, new AssemblerBuildOptions
{
@@ -45,6 +44,6 @@ public async Task Index(
ShowHints = false,
Exporters = new HashSet { Elasticsearch },
AssumeBuild = false
- }, readFs, writeFs, ctx);
+ }, fileSystem, ctx);
}
}
diff --git a/src/services/Elastic.Documentation.Assembler/Navigation/AssemblerDocumentationSet.cs b/src/services/Elastic.Documentation.Assembler/Navigation/AssemblerDocumentationSet.cs
index 4ddc90eeba..31f1eda6a2 100644
--- a/src/services/Elastic.Documentation.Assembler/Navigation/AssemblerDocumentationSet.cs
+++ b/src/services/Elastic.Documentation.Assembler/Navigation/AssemblerDocumentationSet.cs
@@ -2,11 +2,13 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
+using System.IO.Abstractions;
using Elastic.Documentation;
using Elastic.Documentation.Assembler.Sourcing;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Configuration.ReleaseNotes;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Links.CrossLinks;
using Elastic.Markdown.IO;
using Microsoft.Extensions.Logging;
@@ -51,17 +53,14 @@ IReadOnlySet availableExporters
Branch = checkout.Repository.GetBranch(env.ContentSource)
};
- var buildContext = new BuildContext(
- context.Collector,
- context.ReadFileSystem,
- context.WriteFileSystem,
- configurationContext,
- availableExporters,
- path,
- output,
- gitConfiguration
- )
+ var docFs = DocumentationFileSystem.Resolve(path, new DocumentationScopeOptions
{
+ Output = output,
+ Git = gitConfiguration,
+ });
+ var buildContext = new BuildContext(context.Collector, docFs, configurationContext)
+ {
+ AvailableExporters = availableExporters,
UrlPathPrefix = env.PathPrefix,
Force = true,
AllowIndexing = env.AllowIndexing,
diff --git a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs
index 18c621c8ad..42a9df636e 100644
--- a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs
@@ -7,9 +7,9 @@
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.Navigation;
@@ -17,12 +17,12 @@ public class GlobalNavigationService(
ILoggerFactory logFactory,
AssemblyConfiguration configuration,
IConfigurationContext configurationContext,
- ScopedFileSystem fileSystem
+ CheckoutsFileSystem fileSystem
) : IService
{
public async Task Validate(IDiagnosticsCollector collector, Cancel ctx)
{
- var assembleContext = new AssembleContext(configuration, configurationContext, "dev", collector, fileSystem, fileSystem, null, null);
+ var assembleContext = new AssembleContext(configuration, configurationContext, "dev", collector, fileSystem);
var namespaceChecker = new NavigationPrefixChecker(logFactory, assembleContext);
var navigationFileInfo = assembleContext.ConfigurationFileProvider.NavigationFile;
@@ -40,7 +40,7 @@ public async Task Validate(IDiagnosticsCollector collector, Cancel ctx)
public async Task ValidateLocalLinkReference(IDiagnosticsCollector collector, string? file, Cancel ctx)
{
file ??= ".artifacts/docs/html/links.json";
- var assembleContext = new AssembleContext(configuration, configurationContext, "dev", collector, fileSystem, fileSystem, null, null);
+ var assembleContext = new AssembleContext(configuration, configurationContext, "dev", collector, fileSystem);
var root = fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
var repository = GitCheckoutInformationFactory.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName
diff --git a/src/services/Elastic.Documentation.Assembler/Sourcing/AssemblerCloneService.cs b/src/services/Elastic.Documentation.Assembler/Sourcing/AssemblerCloneService.cs
index f3522d25c1..4927842002 100644
--- a/src/services/Elastic.Documentation.Assembler/Sourcing/AssemblerCloneService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Sourcing/AssemblerCloneService.cs
@@ -7,6 +7,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
@@ -25,8 +26,8 @@ public async Task CloneAll(IDiagnosticsCollector collector, AssemblerClone
var githubEnvironmentInput = githubActionsService.GetInput("environment");
var environment = options.Environment ?? (!string.IsNullOrEmpty(githubEnvironmentInput) ? githubEnvironmentInput : "dev");
- var fs = FileSystemFactory.RealRead;
- var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, fs, fs, null, null);
+ var cfs = CheckoutsFileSystem.FromWorkingDirectory();
+ var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, cfs);
var cloner = new AssemblerRepositorySourcer(logFactory, assembleContext);
_ = await cloner.CloneAll(options.FetchLatest ?? false, options.AssumeCloned ?? false, ctx);
diff --git a/src/services/Elastic.Documentation.Deploying/Synchronization/IDocsSyncContext.cs b/src/services/Elastic.Documentation.Deploying/Synchronization/IDocsSyncContext.cs
index c93e39c568..7ef483a422 100644
--- a/src/services/Elastic.Documentation.Deploying/Synchronization/IDocsSyncContext.cs
+++ b/src/services/Elastic.Documentation.Deploying/Synchronization/IDocsSyncContext.cs
@@ -4,7 +4,7 @@
using System.IO.Abstractions;
using Elastic.Documentation.Diagnostics;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Documentation.Deploying.Synchronization;
@@ -14,8 +14,8 @@ namespace Elastic.Documentation.Deploying.Synchronization;
///
public interface IDocsSyncContext
{
- ScopedFileSystem ReadFileSystem { get; }
- ScopedFileSystem WriteFileSystem { get; }
+ CheckoutsFileSystem ReadFileSystem { get; }
+ DocumentationWriteFileSystem WriteFileSystem { get; }
IDirectoryInfo OutputDirectory { get; }
IDiagnosticsCollector Collector { get; }
diff --git a/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs b/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs
index f2422aa2b4..67951bf718 100644
--- a/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs
+++ b/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs
@@ -11,6 +11,7 @@
using Elastic.Documentation.Configuration.Inference;
using Elastic.Documentation.Configuration.ReleaseNotes;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.LinkIndex;
using Elastic.Documentation.Links;
using Elastic.Documentation.Links.CrossLinks;
@@ -23,7 +24,6 @@
using Elastic.Markdown.Page;
using Microsoft.Extensions.Logging;
using Nullean.ScopedFileSystem;
-using static System.StringComparison;
namespace Elastic.Documentation.Isolated;
@@ -46,9 +46,8 @@ public bool IsStrict(bool? strict)
public async Task Build(
IDiagnosticsCollector collector,
- ScopedFileSystem fileSystem,
IsolatedBuildOptions options,
- ScopedFileSystem? writeFileSystem = null,
+ IFileSystem? writeFileSystem = null,
Cancel ctx = default
)
{
@@ -86,8 +85,14 @@ public async Task Build(
try
{
- context = new BuildContext(collector, fileSystem, writeFileSystem ?? fileSystem, configurationContext, exporters, path, output)
+ var docFs = DocumentationFileSystem.Resolve(path, new DocumentationScopeOptions
{
+ Output = options.Output?.FullName,
+ InnerWrite = writeFileSystem
+ });
+ context = new BuildContext(collector, docFs, configurationContext)
+ {
+ AvailableExporters = exporters,
UrlPathPrefix = pathPrefix,
Force = force ?? false,
AllowIndexing = allowIndexing ?? false,
@@ -97,19 +102,21 @@ public async Task Build(
// On CI, we are running on a merge commit which may have changes against an older
// docs folder (this can happen on out-of-date PR's).
// At some point in the future we can remove this try catch
- catch (Exception e) when (runningOnCi && e.Message.StartsWith("Can not locate docset.yml file in", OrdinalIgnoreCase))
+ catch (DocumentationPathException e) when (runningOnCi)
{
// Derive the default output from `path` so it stays within the write FS scope.
// Using Paths.WorkingDirectoryRoot would be wrong when --path points to a different repo.
var rootFolder = !string.IsNullOrWhiteSpace(path) ? path : Paths.WorkingDirectoryRoot.FullName;
- var writeFs = writeFileSystem ?? fileSystem;
- var outputDirectory = !string.IsNullOrWhiteSpace(output)
- ? writeFs.DirectoryInfo.New(output)
- : writeFs.DirectoryInfo.New(Path.Join(rootFolder, ".artifacts/docs/html"));
+ var fallbackFs = writeFileSystem ?? new FileSystem();
+ var outputDirectory = fallbackFs.DirectoryInfo.New(output ?? Path.Join(rootFolder, ".artifacts/docs/html"));
// we temporarily do not error when pointed to a non-documentation folder.
- _ = writeFs.Directory.CreateDirectory(outputDirectory.FullName);
+ _ = fallbackFs.Directory.CreateDirectory(outputDirectory.FullName);
- _logger.LogInformation("Skipping build as we are running on a merge commit and the docs folder is out of date and has no docset.yml. {Message}",
+ // Surfaced as a warning (not swallowed at Information level) so that when the underlying
+ // cause is a real bug — not the stale-merge-commit case this catch was written for —
+ // the --git-dir remedy in e.Message actually reaches whoever is reading the failed run,
+ // rather than being buried above a later, unrelated artifact-upload failure.
+ _logger.LogWarning("Skipping build on CI: {Message} If the docs folder is not actually out of date on a stale merge commit, this indicates a real path-resolution issue.",
e.Message);
await githubActionsService.SetOutputAsync("skip", "true");
@@ -128,7 +135,7 @@ public async Task Build(
else
{
using var codexReader = context.Configuration.Registry != DocSetRegistry.Public
- ? new GitLinkIndexReader(context.Configuration.Registry.ToStringFast(true), FileSystemFactory.AppData)
+ ? new GitLinkIndexReader(context.Configuration.Registry.ToStringFast(true), new ApplicationDataFileSystem())
: null;
var crossLinkFetcher = new DocSetConfigurationCrossLinkFetcher(
diff --git a/src/services/Elastic.Documentation.Isolated/IsolatedIndexService.cs b/src/services/Elastic.Documentation.Isolated/IsolatedIndexService.cs
index 8f615612d2..2771b8f9ba 100644
--- a/src/services/Elastic.Documentation.Isolated/IsolatedIndexService.cs
+++ b/src/services/Elastic.Documentation.Isolated/IsolatedIndexService.cs
@@ -33,7 +33,7 @@ public async Task Index(
var cfg = _configurationContext.Endpoints.Elasticsearch;
await ElasticsearchEndpointConfigurator.ApplyAsync(cfg, es, collector, fileSystem, ctx);
- return await Build(collector, fileSystem, new IsolatedBuildOptions
+ return await Build(collector, new IsolatedBuildOptions
{
Path = path != null ? new DirectoryInfo(path) : null,
MetadataOnly = true,
diff --git a/src/tooling/docs-builder/Commands/Assembler/AssemblerAiEnrichCommand.cs b/src/tooling/docs-builder/Commands/Assembler/AssemblerAiEnrichCommand.cs
index b93e9d3d67..09094129f4 100644
--- a/src/tooling/docs-builder/Commands/Assembler/AssemblerAiEnrichCommand.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/AssemblerAiEnrichCommand.cs
@@ -8,6 +8,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using Nullean.Argh;
@@ -43,11 +44,10 @@ public async Task AiEnrich(
)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var readFs = FileSystemFactory.RealRead;
- var writeFs = FileSystemFactory.RealWrite;
+ var fs = CheckoutsFileSystem.FromWorkingDirectory();
var service = new AssemblerAiEnrichService(logFactory, configuration, configurationContext, githubActionsService);
serviceInvoker.AddCommand(service,
- async (s, col, ctx) => await s.AiEnrich(col, readFs, writeFs, es, environment, bootstrapOnly, ctx)
+ async (s, col, ctx) => await s.AiEnrich(col, fs, es, environment, bootstrapOnly, ctx)
);
return await serviceInvoker.InvokeAsync(ct);
}
diff --git a/src/tooling/docs-builder/Commands/Assembler/AssemblerCommands.cs b/src/tooling/docs-builder/Commands/Assembler/AssemblerCommands.cs
index f89094b7f8..63927b844c 100644
--- a/src/tooling/docs-builder/Commands/Assembler/AssemblerCommands.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/AssemblerCommands.cs
@@ -11,6 +11,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using Nullean.Argh;
@@ -62,11 +63,10 @@ public async Task Assemble(
static async (s, col, opts, ctx) => await s.CloneAll(col, opts, ctx)
);
- var readFs = FileSystemFactory.RealRead;
- var writeFs = FileSystemFactory.RealWrite;
+ var fs = CheckoutsFileSystem.FromWorkingDirectory();
var buildService = new AssemblerBuildService(logFactory, assemblyConfiguration, configurationContext, githubActionsService, environmentVariables);
- serviceInvoker.AddCommand(buildService, (buildOptions, readFs, writeFs), buildOptions.Strict ?? false,
- static async (s, col, state, ctx) => await s.BuildAll(col, state.buildOptions, state.readFs, state.writeFs, ctx)
+ serviceInvoker.AddCommand(buildService, (buildOptions, fs), buildOptions.Strict ?? false,
+ static async (s, col, state, ctx) => await s.BuildAll(col, state.buildOptions, state.fs, ctx)
);
var result = await serviceInvoker.InvokeAsync(ct);
@@ -146,11 +146,10 @@ public async Task Build(
)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var readFs = FileSystemFactory.RealRead;
- var writeFs = FileSystemFactory.RealWrite;
+ var fs = CheckoutsFileSystem.FromWorkingDirectory();
var service = new AssemblerBuildService(logFactory, assemblyConfiguration, configurationContext, githubActionsService, environmentVariables);
- serviceInvoker.AddCommand(service, (options, readFs, writeFs), options.Strict ?? false,
- static async (s, col, state, ctx) => await s.BuildAll(col, state.options, state.readFs, state.writeFs, ctx)
+ serviceInvoker.AddCommand(service, (options, fs), options.Strict ?? false,
+ static async (s, col, state, ctx) => await s.BuildAll(col, state.options, state.fs, ctx)
);
return await serviceInvoker.InvokeAsync(ct);
}
diff --git a/src/tooling/docs-builder/Commands/Assembler/AssemblerIndexCommand.cs b/src/tooling/docs-builder/Commands/Assembler/AssemblerIndexCommand.cs
index 26412a93cc..c384c647be 100644
--- a/src/tooling/docs-builder/Commands/Assembler/AssemblerIndexCommand.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/AssemblerIndexCommand.cs
@@ -9,6 +9,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using Nullean.Argh;
@@ -42,11 +43,10 @@ public async Task Index(
)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var readFs = FileSystemFactory.RealRead;
- var writeFs = FileSystemFactory.RealWrite;
+ var fs = CheckoutsFileSystem.FromWorkingDirectory();
var service = new AssemblerIndexService(logFactory, configuration, configurationContext, githubActionsService, environmentVariables);
serviceInvoker.AddCommand(service,
- async (s, col, ctx) => await s.Index(col, readFs, writeFs, es, environment, ctx)
+ async (s, col, ctx) => await s.Index(col, fs, es, environment, ctx)
);
return await serviceInvoker.InvokeAsync(ct);
}
diff --git a/src/tooling/docs-builder/Commands/Assembler/AssemblerSitemapCommand.cs b/src/tooling/docs-builder/Commands/Assembler/AssemblerSitemapCommand.cs
index d0e2bbc825..0f0407d18f 100644
--- a/src/tooling/docs-builder/Commands/Assembler/AssemblerSitemapCommand.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/AssemblerSitemapCommand.cs
@@ -9,6 +9,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using Nullean.Argh;
@@ -41,7 +42,7 @@ public async Task Sitemap(
)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = FileSystemFactory.RealWrite;
+ var fs = CheckoutsFileSystem.FromWorkingDirectory();
var service = new AssemblerSitemapService(logFactory, configuration, configurationContext, githubActionsService);
serviceInvoker.AddCommand(service,
async (s, col, ctx) => await s.GenerateSitemapAsync(col, fs, es, environment, ctx)
diff --git a/src/tooling/docs-builder/Commands/Assembler/ConfigurationCommands.cs b/src/tooling/docs-builder/Commands/Assembler/ConfigurationCommands.cs
index e617afd6d5..88491515c4 100644
--- a/src/tooling/docs-builder/Commands/Assembler/ConfigurationCommands.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/ConfigurationCommands.cs
@@ -2,12 +2,12 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
-using System.IO.Abstractions;
using Elastic.Documentation;
using Elastic.Documentation.Assembler.Configuration;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using Nullean.Argh;
@@ -34,8 +34,7 @@ public async Task Init(string? gitRef = null, bool local = false, Cancellat
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = FileSystemFactory.RealRead;
- var service = new ConfigurationCloneService(logFactory, assemblyConfiguration, fs);
+ var service = new ConfigurationCloneService(logFactory, assemblyConfiguration, CheckoutsFileSystem.FromWorkingDirectory());
serviceInvoker.AddCommand(service, (gitRef, local), static async (s, collector, state, ctx) =>
await s.InitConfigurationToApplicationData(collector, state.gitRef, state.local, ctx));
return await serviceInvoker.InvokeAsync(ct);
diff --git a/src/tooling/docs-builder/Commands/Assembler/ContentSourceCommands.cs b/src/tooling/docs-builder/Commands/Assembler/ContentSourceCommands.cs
index 9cfe6be0e3..c19dbdd7d2 100644
--- a/src/tooling/docs-builder/Commands/Assembler/ContentSourceCommands.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/ContentSourceCommands.cs
@@ -9,6 +9,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using Nullean.Argh;
@@ -30,7 +31,7 @@ public async Task Validate(CancellationToken ct = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = FileSystemFactory.RealRead;
+ var fs = CheckoutsFileSystem.FromWorkingDirectory();
var service = new RepositoryPublishValidationService(logFactory, configuration, configurationContext, fs);
serviceInvoker.AddCommand(service, static async (s, collector, ctx) => await s.ValidatePublishStatus(collector, ctx));
@@ -46,7 +47,7 @@ public async Task Match([Argument] string? repository = null, [Argument] st
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = FileSystemFactory.RealRead;
+ var fs = CheckoutsFileSystem.FromWorkingDirectory();
var service = new RepositoryBuildMatchingService(logFactory, configuration, configurationContext, githubActionsService, fs);
serviceInvoker.AddCommand(service, (repository, branchOrTag),
static async (s, collector, state, ctx) =>
diff --git a/src/tooling/docs-builder/Commands/Assembler/DeployCommands.cs b/src/tooling/docs-builder/Commands/Assembler/DeployCommands.cs
index ed1b452197..30d3f661a0 100644
--- a/src/tooling/docs-builder/Commands/Assembler/DeployCommands.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/DeployCommands.cs
@@ -11,6 +11,7 @@
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Deploying;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using Nullean.Argh;
@@ -43,7 +44,8 @@ public async Task Plan(string environment, string s3BucketName, [ExpandUser
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var context = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, FileSystemFactory.RealRead, FileSystemFactory.RealWrite, null, null);
+ var fs = CheckoutsFileSystem.FromWorkingDirectory();
+ var context = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, fs);
var service = new IncrementalDeployService(logFactory, githubActionsService);
serviceInvoker.AddCommand(service, (context, s3BucketName, @out, deleteThreshold),
static async (s, collector, state, ctx) => await s.Plan(collector, state.context, state.s3BucketName, state.@out?.FullName ?? "", state.deleteThreshold, [], ctx)
@@ -64,7 +66,8 @@ public async Task Apply(string environment, string s3BucketName, [Existing,
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var context = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, FileSystemFactory.RealRead, FileSystemFactory.RealWrite, null, null);
+ var fs = CheckoutsFileSystem.FromWorkingDirectory();
+ var context = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, fs);
var service = new IncrementalDeployService(logFactory, githubActionsService);
serviceInvoker.AddCommand(service, (context, s3BucketName, planFile),
static async (s, collector, state, ctx) => await s.Apply(collector, state.context, state.s3BucketName, state.planFile.FullName, ctx)
@@ -81,8 +84,7 @@ public async Task UpdateRedirects(string environment, [Existing, ExpandUser
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = FileSystemFactory.RealRead;
- var service = new DeployUpdateRedirectsService(logFactory, fs);
+ var service = new DeployUpdateRedirectsService(logFactory, CheckoutsFileSystem.FromWorkingDirectory());
serviceInvoker.AddCommand(service, (environment, redirectsFile),
static async (s, collector, state, ctx) => await s.UpdateRedirects(collector, state.environment, state.redirectsFile?.FullName, ctx: ctx)
);
diff --git a/src/tooling/docs-builder/Commands/Assembler/NavigationCommands.cs b/src/tooling/docs-builder/Commands/Assembler/NavigationCommands.cs
index 7f1f03820e..9a3e877e26 100644
--- a/src/tooling/docs-builder/Commands/Assembler/NavigationCommands.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/NavigationCommands.cs
@@ -9,6 +9,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using Nullean.Argh;
@@ -28,7 +29,7 @@ IConfigurationContext configurationContext
public async Task Validate(CancellationToken ct = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var service = new GlobalNavigationService(logFactory, configuration, configurationContext, FileSystemFactory.RealRead);
+ var service = new GlobalNavigationService(logFactory, configuration, configurationContext, CheckoutsFileSystem.FromWorkingDirectory());
serviceInvoker.AddCommand(service, static async (s, collector, ctx) => await s.Validate(collector, ctx));
return await serviceInvoker.InvokeAsync(ct);
}
@@ -39,7 +40,7 @@ public async Task Validate(CancellationToken ct = default)
public async Task ValidateLinkReference([Argument, Existing, ExpandUserProfile, RejectSymbolicLinks, FileExtensions(Extensions = "json")] FileInfo? file = null, CancellationToken ct = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var service = new GlobalNavigationService(logFactory, configuration, configurationContext, FileSystemFactory.RealRead);
+ var service = new GlobalNavigationService(logFactory, configuration, configurationContext, CheckoutsFileSystem.FromWorkingDirectory());
serviceInvoker.AddCommand(service, file, static async (s, collector, file, ctx) => await s.ValidateLocalLinkReference(collector, file?.FullName, ctx));
return await serviceInvoker.InvokeAsync(ct);
}
diff --git a/src/tooling/docs-builder/Commands/ChangelogCommand.cs b/src/tooling/docs-builder/Commands/ChangelogCommand.cs
index 409057eb2b..1d4cd6df3d 100644
--- a/src/tooling/docs-builder/Commands/ChangelogCommand.cs
+++ b/src/tooling/docs-builder/Commands/ChangelogCommand.cs
@@ -25,6 +25,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Changelog;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ReleaseNotes;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
@@ -48,7 +49,7 @@ IEnvironmentVariables environmentVariables
[GeneratedRegex(@"^( *output_directory:\s*).+$", RegexOptions.Multiline)]
private static partial Regex BundleOutputDirectoryRegex();
- private readonly IFileSystem _fileSystem = FileSystemFactory.RealRead;
+ private readonly ChangelogFileSystem _fileSystem = ChangelogFileSystem.FromWorkingDirectory();
private readonly ILogger _logger = logFactory.CreateLogger();
/// Create changelog.yml and the changelog/releases directory structure.
///
@@ -357,7 +358,7 @@ public async Task Add(
var repoArg = resolvedRepo.Contains('/') ? resolvedRepo : $"{resolvedOwner}/{resolvedRepo}";
IGitHubReleaseService releaseService = new GitHubReleaseService(logFactory);
IGitHubPrService prService = new GitHubPrService(logFactory);
- var releaseChangelogService = new GitHubReleaseChangelogService(logFactory, configurationContext, releaseService, prService);
+ var releaseChangelogService = new GitHubReleaseChangelogService(logFactory, configurationContext, _fileSystem, releaseService, prService);
var releaseInput = new CreateChangelogsFromReleaseArguments
{
@@ -377,7 +378,7 @@ async static (s, collector, state, ctx) => await s.CreateChangelogsFromRelease(c
}
IGitHubPrService githubPrService = new GitHubPrService(logFactory);
- var service = new ChangelogCreationService(logFactory, configurationContext, githubPrService, env: SystemEnvironmentVariables.Instance);
+ var service = new ChangelogCreationService(logFactory, configurationContext, _fileSystem, githubPrService, env: SystemEnvironmentVariables.Instance);
// Parse PRs: promotion report (--report), or comma-separated values and file paths (--prs)
string[]? parsedPrs = null;
@@ -388,7 +389,7 @@ async static (s, collector, state, ctx) => await s.CreateChangelogsFromRelease(c
!reportSource.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
reportSource = NormalizePath(reportSource);
- var reportParser = new PromotionReportParser(logFactory, null);
+ var reportParser = new PromotionReportParser(logFactory, _fileSystem);
parsedPrs = await reportParser.ParseReportToPrUrlsAsync(collector, reportSource, ctx);
if (parsedPrs == null)
{
@@ -615,7 +616,7 @@ public async Task Bundle(
var ctx = ct;
await using var serviceInvoker = new ServiceInvoker(collector);
- var service = new ChangelogBundlingService(logFactory, configurationContext);
+ var service = new ChangelogBundlingService(logFactory, _fileSystem, configurationContext);
var isProfileMode = !string.IsNullOrWhiteSpace(profile);
var isGitRefMode = !string.IsNullOrWhiteSpace(startGitRef) || !string.IsNullOrWhiteSpace(endGitRef);
@@ -1008,7 +1009,7 @@ public async Task Remove(
var ctx = ct;
await using var serviceInvoker = new ServiceInvoker(collector);
- var service = new ChangelogRemoveService(logFactory, configurationContext);
+ var service = new ChangelogRemoveService(logFactory, _fileSystem, configurationContext);
var isProfileMode = !string.IsNullOrWhiteSpace(profile);
@@ -1224,7 +1225,7 @@ public async Task Render(
var ctx = ct;
await using var serviceInvoker = new ServiceInvoker(collector);
- var service = new ChangelogRenderingService(logFactory, configurationContext);
+ var service = new ChangelogRenderingService(logFactory, _fileSystem, configurationContext);
var allFeatureIds = ExpandCommaSeparated(hideFeatures);
@@ -1297,7 +1298,7 @@ public async Task GhRelease(
IGitHubReleaseService releaseService = new GitHubReleaseService(logFactory);
IGitHubPrService prService = new GitHubPrService(logFactory);
- var service = new GitHubReleaseChangelogService(logFactory, configurationContext, releaseService, prService);
+ var service = new GitHubReleaseChangelogService(logFactory, configurationContext, _fileSystem, releaseService, prService);
// Validate release date format if provided
if (!string.IsNullOrWhiteSpace(releaseDate) && !DateOnly.TryParseExact(releaseDate, "yyyy-MM-dd", out _))
@@ -1348,7 +1349,7 @@ public async Task BundleAmend(
var ctx = ct;
await using var serviceInvoker = new ServiceInvoker(collector);
- var service = new ChangelogBundleAmendService(logFactory, configurationContext: configurationContext);
+ var service = new ChangelogBundleAmendService(logFactory, _fileSystem, configurationContext: configurationContext);
var normalizedAddFiles = add != null
? ExpandCommaSeparated(add).Select(NormalizePath).ToList()
@@ -1425,7 +1426,7 @@ public async Task EvaluatePr(
var ctx = ct;
await using var serviceInvoker = new ServiceInvoker(collector);
- var fileSystem = FileSystemFactory.RealReadForRunnerTemp(environmentVariables);
+ var fileSystem = RunnerTempFileSystem.ForEvaluatePr(environmentVariables);
IGitHubPrService prService = new GitHubPrService(logFactory);
var service = new ChangelogPrEvaluationService(logFactory, configurationContext, prService, githubActionsService, fileSystem);
@@ -1515,7 +1516,7 @@ public async Task PrepareArtifact(
var ctx = ct;
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = FileSystemFactory.RealGitRootForPathWrite(null, outputDir);
+ var fs = RunnerTempFileSystem.ForPrepareArtifact(stagingDir, outputDir);
var service = new ChangelogPrepareArtifactService(logFactory, configurationContext, githubActionsService, fs);
var args = new PrepareArtifactArguments
@@ -1564,7 +1565,7 @@ public async Task EvaluateArtifact(
var ctx = ct;
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = FileSystemFactory.RealGitRootForPathWrite(null, metadata);
+ var fs = RunnerTempFileSystem.ForEvaluateArtifact(metadata);
IGitHubPrService prService = new GitHubPrService(logFactory);
var service = new ChangelogArtifactEvaluationService(logFactory, prService, githubActionsService, fs);
@@ -1691,7 +1692,7 @@ public async Task Upload(
var (resolvedRepo, resolvedOwner, resolvedBranch) = await ResolveUploadRepoOwnerBranch(repo, owner, branch, resolvedConfig, resolvedDirectory, ctx);
await using var serviceInvoker = new ServiceInvoker(collector);
- var service = new ChangelogUploadService(logFactory, configurationContext);
+ var service = new ChangelogUploadService(logFactory, _fileSystem, configurationContext);
var args = new ChangelogUploadArguments
{
ArtifactType = parsedArtifactType,
@@ -1797,7 +1798,7 @@ public async Task MigrateFromWeb(
return 1;
}
- var service = new WebMigrationService(logFactory, FileSystemFactory.RealWrite);
+ var service = new WebMigrationService(logFactory, CheckoutsFileSystem.FromWorkingDirectory().Write);
var args = new MigrateFromWebArguments
{
Products = ExpandCommaSeparated(products),
diff --git a/src/tooling/docs-builder/Commands/Codex/CodexCommands.cs b/src/tooling/docs-builder/Commands/Codex/CodexCommands.cs
index cb11145b51..1bc343111f 100644
--- a/src/tooling/docs-builder/Commands/Codex/CodexCommands.cs
+++ b/src/tooling/docs-builder/Commands/Codex/CodexCommands.cs
@@ -12,6 +12,7 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Isolated;
using Elastic.Documentation.LinkIndex;
using Elastic.Documentation.Services;
@@ -58,14 +59,11 @@ public async Task CloneAndBuild(
CancellationToken ct = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var readFs = FileSystemFactory.ScopeCurrentWorkingDirectory(new FileSystem(), [Paths.FindGitRoot(config.FullName)]);
- var writeFs = FileSystemFactory.RealGitRootForPathWrite(null, output?.FullName);
-
- var configFile = readFs.FileInfo.New(config.FullName);
- if (!CodexConfigurationLoader.TryLoad(configFile, config.FullName, collector, out var codexConfig, out var environment))
+ var fs = new CodexFileSystem(config.FullName, output?.FullName);
+ if (!CodexConfigurationLoader.TryLoad(fs.ConfigurationFile, config.FullName, collector, out var codexConfig, out var environment))
return 1;
- var codexContext = new CodexContext(codexConfig, configFile, collector, readFs, writeFs, null, output?.FullName);
+ var codexContext = new CodexContext(codexConfig, fs.ConfigurationFile, collector, fs, null, output?.FullName);
using var linkIndexReader = new GitLinkIndexReader(environment);
var cloneService = new CodexCloneService(logFactory, linkIndexReader);
@@ -80,12 +78,12 @@ public async Task CloneAndBuild(
var isolatedBuildService = new IsolatedBuildService(logFactory, configurationContext, githubActionsService, environmentVariables);
var buildService = new CodexBuildService(logFactory, configurationContext, isolatedBuildService);
- serviceInvoker.AddCommand(buildService, (codexContext, cloneResult, readFs), strict,
+ serviceInvoker.AddCommand(buildService, (codexContext, cloneResult, fs), strict,
async (s, col, state, c) =>
{
if (state.cloneResult == null)
return false;
- var result = await s.BuildAll(state.codexContext, state.cloneResult, state.readFs, c);
+ var result = await s.BuildAll(state.codexContext, state.cloneResult, state.fs, c);
return result.DocumentationSets.Count > 0;
});
@@ -116,13 +114,11 @@ public async Task Clone(
CancellationToken ct = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var readFs = FileSystemFactory.ScopeCurrentWorkingDirectory(new FileSystem(), [Paths.FindGitRoot(config.FullName)]);
-
- var configFile = readFs.FileInfo.New(config.FullName);
- if (!CodexConfigurationLoader.TryLoad(configFile, config.FullName, collector, out var codexConfig, out var environment))
+ var fs = new CodexFileSystem(config.FullName);
+ if (!CodexConfigurationLoader.TryLoad(fs.ConfigurationFile, config.FullName, collector, out var codexConfig, out var environment))
return 1;
- var codexContext = new CodexContext(codexConfig, configFile, collector, readFs, FileSystemFactory.RealWrite, null, null);
+ var codexContext = new CodexContext(codexConfig, fs.ConfigurationFile, collector, fs);
using var linkIndexReader = new GitLinkIndexReader(environment);
var cloneService = new CodexCloneService(logFactory, linkIndexReader);
@@ -150,14 +146,11 @@ public async Task Build(
CancellationToken ct = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var readFs = FileSystemFactory.ScopeCurrentWorkingDirectory(new FileSystem(), [Paths.FindGitRoot(config.FullName)]);
- var writeFs = FileSystemFactory.RealGitRootForPathWrite(null, output?.FullName);
-
- var configFile = readFs.FileInfo.New(config.FullName);
- if (!CodexConfigurationLoader.TryLoad(configFile, config.FullName, collector, out var codexConfig, out _))
+ var fs = new CodexFileSystem(config.FullName, output?.FullName);
+ if (!CodexConfigurationLoader.TryLoad(fs.ConfigurationFile, config.FullName, collector, out var codexConfig, out _))
return 1;
- var codexContext = new CodexContext(codexConfig, configFile, collector, readFs, writeFs, null, output?.FullName);
+ var codexContext = new CodexContext(codexConfig, fs.ConfigurationFile, collector, fs, null, output?.FullName);
var cloneResult = await CodexCloneService.DiscoverCheckouts(codexContext, logFactory, ct);
if (cloneResult == null || cloneResult.Checkouts.Count == 0)
@@ -168,10 +161,10 @@ public async Task Build(
var isolatedBuildService = new IsolatedBuildService(logFactory, configurationContext, githubActionsService, environmentVariables);
var buildService = new CodexBuildService(logFactory, configurationContext, isolatedBuildService);
- serviceInvoker.AddCommand(buildService, (codexContext, cloneResult, readFs), strict,
+ serviceInvoker.AddCommand(buildService, (codexContext, cloneResult, fs), strict,
async (s, col, state, c) =>
{
- var result = await s.BuildAll(state.codexContext, state.cloneResult, state.readFs, c);
+ var result = await s.BuildAll(state.codexContext, state.cloneResult, state.fs, c);
return result.DocumentationSets.Count > 0;
});
@@ -186,8 +179,7 @@ public async Task Build(
[NoOptionsInjection]
public async Task Serve(int port = 4000, [Existing, ExpandUserProfile, RejectSymbolicLinks] DirectoryInfo? path = null, CancellationToken ct = default)
{
- var fs = FileSystemFactory.RealRead;
- var servePath = path?.FullName ?? fs.Path.Join(Environment.CurrentDirectory, ".artifacts", "codex", "docs");
+ var servePath = path?.FullName ?? Path.Join(Environment.CurrentDirectory, ".artifacts", "codex", "docs");
var host = new StaticWebHost(port, servePath);
await host.RunAsync(ct);
diff --git a/src/tooling/docs-builder/Commands/Codex/CodexIndexCommand.cs b/src/tooling/docs-builder/Commands/Codex/CodexIndexCommand.cs
index 72f770940a..95cb649d94 100644
--- a/src/tooling/docs-builder/Commands/Codex/CodexIndexCommand.cs
+++ b/src/tooling/docs-builder/Commands/Codex/CodexIndexCommand.cs
@@ -5,13 +5,13 @@
using System.ComponentModel.DataAnnotations;
using System.IO.Abstractions;
using Actions.Core.Services;
-
using Elastic.Codex;
using Elastic.Codex.Indexing;
using Elastic.Codex.Sourcing;
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Isolated;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
@@ -44,12 +44,11 @@ public async Task Index(
)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var readFs = FileSystemFactory.ScopeCurrentWorkingDirectory(new FileSystem(), [Paths.FindGitRoot(config.FullName)]);
- var configFile = readFs.FileInfo.New(config.FullName);
- if (!CodexConfigurationLoader.TryLoad(configFile, config.FullName, collector, out var codexConfig, out var environment))
+ var fs = new CodexFileSystem(config.FullName);
+ if (!CodexConfigurationLoader.TryLoad(fs.ConfigurationFile, config.FullName, collector, out var codexConfig, out var environment))
return 1;
- var codexContext = new CodexContext(codexConfig, configFile, collector, readFs, FileSystemFactory.RealWrite, null, null);
+ var codexContext = new CodexContext(codexConfig, fs.ConfigurationFile, collector, fs);
var cloneResult = await CodexCloneService.DiscoverCheckouts(codexContext, logFactory, ct);
@@ -61,9 +60,9 @@ public async Task Index(
var isolatedBuildService = new IsolatedBuildService(logFactory, configurationContext, githubActionsService, environmentVariables);
var service = new CodexIndexService(logFactory, configurationContext, isolatedBuildService);
- serviceInvoker.AddCommand(service, (codexContext, cloneResult, readFs, es),
+ serviceInvoker.AddCommand(service, (codexContext, cloneResult, fs, es),
static async (s, col, state, c) =>
- await s.Index(state.codexContext, state.cloneResult, state.readFs, state.es, c)
+ await s.Index(state.codexContext, state.cloneResult, state.fs, state.es, c)
);
return await serviceInvoker.InvokeAsync(ct);
diff --git a/src/tooling/docs-builder/Commands/Codex/CodexSyncCommand.cs b/src/tooling/docs-builder/Commands/Codex/CodexSyncCommand.cs
index cbef92df6d..ad319adf44 100644
--- a/src/tooling/docs-builder/Commands/Codex/CodexSyncCommand.cs
+++ b/src/tooling/docs-builder/Commands/Codex/CodexSyncCommand.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information
using System.ComponentModel.DataAnnotations;
+using System.IO.Abstractions;
using Actions.Core.Services;
using Elastic.Codex;
using Elastic.Documentation;
@@ -10,6 +11,7 @@
using Elastic.Documentation.Configuration.Codex;
using Elastic.Documentation.Deploying;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using Nullean.Argh;
@@ -85,10 +87,9 @@ static async (s, collector, state, ctx) => await s.Apply(collector, state.contex
private (CodexContext context, IncrementalDeployService service) LoadContext(FileInfo config)
{
- var fs = FileSystemFactory.RealRead;
- var configFile = fs.FileInfo.New(config.FullName);
- var codexConfig = CodexConfiguration.Load(configFile);
- return (new CodexContext(codexConfig, configFile, collector, fs, FileSystemFactory.RealWrite, null, null),
+ var fs = new CodexFileSystem(config.FullName);
+ var codexConfig = CodexConfiguration.Load(fs.ConfigurationFile);
+ return (new CodexContext(codexConfig, fs.ConfigurationFile, collector, fs),
new IncrementalDeployService(logFactory, githubActionsService));
}
}
diff --git a/src/tooling/docs-builder/Commands/Codex/CodexUpdateRedirectsCommand.cs b/src/tooling/docs-builder/Commands/Codex/CodexUpdateRedirectsCommand.cs
index b5027a11c5..048dd20595 100644
--- a/src/tooling/docs-builder/Commands/Codex/CodexUpdateRedirectsCommand.cs
+++ b/src/tooling/docs-builder/Commands/Codex/CodexUpdateRedirectsCommand.cs
@@ -9,6 +9,7 @@
using Elastic.Documentation.Assembler.Deploying;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using Nullean.Argh;
@@ -35,9 +36,8 @@ public async Task UpdateRedirects(
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var readFs = FileSystemFactory.ScopeCurrentWorkingDirectory(new FileSystem(), [Paths.FindGitRoot(config.FullName)]);
- var configFile = readFs.FileInfo.New(config.FullName);
- if (!CodexConfigurationLoader.TryLoad(configFile, config.FullName, collector, out var codexConfig))
+ var fs = new CodexFileSystem(config.FullName);
+ if (!CodexConfigurationLoader.TryLoad(fs.ConfigurationFile, config.FullName, collector, out var codexConfig))
return 1;
var resolvedEnvironment = environment
@@ -45,7 +45,7 @@ public async Task UpdateRedirects(
?? Environment.GetEnvironmentVariable("ENVIRONMENT")
?? "internal";
- var service = new DeployUpdateRedirectsService(logFactory, readFs);
+ var service = new DeployUpdateRedirectsService(logFactory, fs);
serviceInvoker.AddCommand(service, (environment: resolvedEnvironment, redirectsFile, kvsNamePrefix: "codex", defaultRedirectsFile: ".artifacts/codex/docs/redirects.json"),
static async (s, col, state, c) => await s.UpdateRedirects(col, state.environment, state.redirectsFile?.FullName, state.kvsNamePrefix, state.defaultRedirectsFile, c)
);
diff --git a/src/tooling/docs-builder/Commands/DiffCommands.cs b/src/tooling/docs-builder/Commands/DiffCommands.cs
index e6196a3ca4..6a7f0d216f 100644
--- a/src/tooling/docs-builder/Commands/DiffCommands.cs
+++ b/src/tooling/docs-builder/Commands/DiffCommands.cs
@@ -6,6 +6,7 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Refactor.Tracking;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
@@ -32,7 +33,7 @@ public async Task Validate(string? path = null, CancellationToken ct = defa
await using var serviceInvoker = new ServiceInvoker(collector);
var service = new LocalChangeTrackingService(logFactory, configurationContext);
- var fs = FileSystemFactory.RealGitRootForPath(path);
+ var fs = DocumentationFileSystem.Resolve(path ?? Paths.WorkingDirectoryRoot.FullName);
serviceInvoker.AddCommand(service, (path, fs),
async static (s, collector, state, _) => await s.ValidateRedirects(collector, state.path, state.fs)
diff --git a/src/tooling/docs-builder/Commands/InboundLinkCommands.cs b/src/tooling/docs-builder/Commands/InboundLinkCommands.cs
index e208eea64f..0429771102 100644
--- a/src/tooling/docs-builder/Commands/InboundLinkCommands.cs
+++ b/src/tooling/docs-builder/Commands/InboundLinkCommands.cs
@@ -7,6 +7,7 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Links.InboundLinks;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
@@ -24,7 +25,7 @@ namespace Documentation.Builder.Commands;
///
internal sealed class InboundLinkCommands(ILoggerFactory logFactory, IDiagnosticsCollector collector)
{
- private readonly LinkIndexService _linkIndexService = new(logFactory, FileSystemFactory.RealRead);
+ private readonly LinkIndexService _linkIndexService = new(logFactory, CheckoutsFileSystem.FromWorkingDirectory());
/// Validate all cross-links across every published links.json in the registry.
[NoOptionsInjection]
diff --git a/src/tooling/docs-builder/Commands/IndexCommand.cs b/src/tooling/docs-builder/Commands/IndexCommand.cs
index 34e1489d10..35bfad9f4d 100644
--- a/src/tooling/docs-builder/Commands/IndexCommand.cs
+++ b/src/tooling/docs-builder/Commands/IndexCommand.cs
@@ -6,6 +6,7 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Isolated;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
@@ -40,7 +41,7 @@ public async Task Index(
)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = FileSystemFactory.RealGitRootForPath(path);
+ var fs = DocumentationFileSystem.Resolve(path ?? Paths.WorkingDirectoryRoot.FullName);
var service = new IsolatedIndexService(logFactory, configurationContext, githubActionsService, environmentVariables);
serviceInvoker.AddCommand(service,
async (s, col, ctx) => await s.Index(col, fs, es, path, ctx)
diff --git a/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs b/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs
index cc9d478428..32c61c2227 100644
--- a/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs
+++ b/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information
using System.IO.Abstractions;
+using System.IO.Abstractions.TestingHelpers;
using Actions.Core.Services;
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
@@ -42,12 +43,11 @@ public async Task Build(
await using var serviceInvoker = new ServiceInvoker(collector);
var service = new IsolatedBuildService(logFactory, configurationContext, githubActionsService, environmentVariables);
- var readFs = inMemory ? FileSystemFactory.InMemory() : FileSystemFactory.RealGitRootForPath(options.Path?.FullName);
- var writeFs = inMemory ? null : FileSystemFactory.RealGitRootForPathWrite(options.Path?.FullName, options.Output?.FullName);
+ IFileSystem? writeFs = inMemory ? new MockFileSystem() : null;
var strictCommand = service.IsStrict(options.Strict);
- serviceInvoker.AddCommand(service, (options, readFs, writeFs), strictCommand,
- static async (s, col, state, ctx) => await s.Build(col, state.readFs, state.options, state.writeFs, ctx)
+ serviceInvoker.AddCommand(service, (options, writeFs), strictCommand,
+ static async (s, col, state, ctx) => await s.Build(col, state.options, state.writeFs, ctx)
);
return await serviceInvoker.InvokeAsync(ct);
}
diff --git a/src/tooling/docs-builder/Commands/MoveCommand.cs b/src/tooling/docs-builder/Commands/MoveCommand.cs
index 2f5ac7a901..a347fc2f27 100644
--- a/src/tooling/docs-builder/Commands/MoveCommand.cs
+++ b/src/tooling/docs-builder/Commands/MoveCommand.cs
@@ -6,6 +6,7 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Refactor;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
@@ -40,7 +41,7 @@ public async Task Move(
await using var serviceInvoker = new ServiceInvoker(collector);
var service = new MoveFileService(logFactory, configurationContext);
- var fs = FileSystemFactory.RealGitRootForPath(path);
+ var fs = DocumentationFileSystem.Resolve(path ?? Paths.WorkingDirectoryRoot.FullName);
serviceInvoker.AddCommand(service, (source, target, dryRun, path, fs),
async static (s, collector, state, ctx) => await s.Move(collector, state.source, state.target, state.dryRun, state.path, state.fs, ctx)
@@ -73,7 +74,7 @@ public async Task Format(
await using var serviceInvoker = new ServiceInvoker(collector);
var service = new FormatService(logFactory, configurationContext);
- var fs = FileSystemFactory.RealGitRootForPath(path);
+ var fs = DocumentationFileSystem.Resolve(path ?? Paths.WorkingDirectoryRoot.FullName);
serviceInvoker.AddCommand(service, (path, check, fs),
async static (s, collector, state, ctx) => await s.Format(collector, state.path, state.check, state.fs, ctx)
diff --git a/src/tooling/docs-builder/Commands/ServeCommand.cs b/src/tooling/docs-builder/Commands/ServeCommand.cs
index 0d2ff3afa3..9c4b759957 100644
--- a/src/tooling/docs-builder/Commands/ServeCommand.cs
+++ b/src/tooling/docs-builder/Commands/ServeCommand.cs
@@ -25,7 +25,7 @@ internal sealed class ServeCommand(ILoggerFactory logFactory, IConfigurationCont
[CommandName("serve")]
public async Task Serve(GlobalCliOptions _, [Existing, ExpandUserProfile, RejectSymbolicLinks] DirectoryInfo? path = null, int port = 3000, bool watch = false, CancellationToken ct = default)
{
- var host = new DocumentationWebHost(logFactory, path?.FullName, port, FileSystemFactory.RealGitRootForPath(path?.FullName), FileSystemFactory.InMemoryForPath(path?.FullName), configurationContext, watch);
+ var host = new DocumentationWebHost(logFactory, path?.FullName, port, configurationContext, watch);
await host.RunAsync(ct);
_logger.LogInformation("Find your documentation at http://localhost:{Port}/{Path}", port,
host.GeneratorState.Generator.DocumentationSet.FirstInterestingUrl.TrimStart('/')
diff --git a/src/tooling/docs-builder/Http/DocumentationWebHost.cs b/src/tooling/docs-builder/Http/DocumentationWebHost.cs
index 57d80c0baa..2dee21bcc7 100644
--- a/src/tooling/docs-builder/Http/DocumentationWebHost.cs
+++ b/src/tooling/docs-builder/Http/DocumentationWebHost.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information
using System.IO.Abstractions;
+using System.IO.Abstractions.TestingHelpers;
using System.Net;
using Nullean.ScopedFileSystem;
using System.Runtime.InteropServices;
@@ -15,6 +16,7 @@
using Elastic.Documentation.Api;
#endif
using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.ServiceDefaults;
using Elastic.Documentation.Site.FileProviders;
using Elastic.Markdown.IO;
@@ -44,13 +46,10 @@ public class DocumentationWebHost
public DocumentationWebHost(ILoggerFactory logFactory,
string? path,
int port,
- ScopedFileSystem readFs,
- ScopedFileSystem writeFs,
IConfigurationContext configurationContext,
bool isWatchBuild
)
{
- _writeFileSystem = writeFs;
var builder = WebApplication.CreateSlimBuilder();
_ = builder.AddDocumentationServiceDefaults();
@@ -71,7 +70,9 @@ bool isWatchBuild
var hostUrl = $"http://localhost:{port}";
_hostedService = collector;
- Context = new BuildContext(collector, readFs, writeFs, configurationContext, ExportOptions.Default, path, null)
+ var docFs = DocumentationFileSystem.Resolve(path, new DocumentationScopeOptions { InnerWrite = new MockFileSystem() });
+ _writeFileSystem = docFs.Write;
+ Context = new BuildContext(collector, docFs, configurationContext)
{
CanonicalBaseUrl = new Uri(hostUrl),
};
diff --git a/src/tooling/docs-builder/Http/InMemoryBuildState.cs b/src/tooling/docs-builder/Http/InMemoryBuildState.cs
index d882cd195e..c1d2463259 100644
--- a/src/tooling/docs-builder/Http/InMemoryBuildState.cs
+++ b/src/tooling/docs-builder/Http/InMemoryBuildState.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information
using System.IO.Abstractions;
+using System.IO.Abstractions.TestingHelpers;
using System.Text.Json.Serialization;
using System.Threading.Channels;
using Actions.Core;
@@ -14,7 +15,6 @@
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Isolated;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Documentation.Builder.Http;
@@ -62,7 +62,7 @@ public class InMemoryBuildState(ILoggerFactory loggerFactory, IConfigurationCont
// Initialized lazily on first ExecuteBuildAsync so we can scope it to the source path.
// Exposed so the serve host can read pagefind index files written by the background build.
private string? _writeFsPath;
- public ScopedFileSystem? WriteFileSystem { get; private set; }
+ public IFileSystem? WriteFileSystem { get; private set; }
// Broadcast: maintain list of connected client channels
private readonly Lock _clientsLock = new();
@@ -164,10 +164,9 @@ private async Task ExecuteBuildAsync(string sourcePath, Cancel ct)
// Create a diagnostics collector that streams to our channel
var streamingCollector = new StreamingDiagnosticsCollector(_loggerFactory, this);
- var readFs = FileSystemFactory.RealGitRootForPath(sourcePath);
if (WriteFileSystem is null || _writeFsPath != sourcePath)
{
- WriteFileSystem = FileSystemFactory.InMemoryForPath(sourcePath);
+ WriteFileSystem = new MockFileSystem();
_writeFsPath = sourcePath;
}
var service = new IsolatedBuildService(_loggerFactory, _configurationContext, new NullCoreService(), SystemEnvironmentVariables.Instance);
@@ -176,7 +175,6 @@ private async Task ExecuteBuildAsync(string sourcePath, Cancel ct)
_ = await service.Build(
streamingCollector,
- readFs,
new IsolatedBuildOptions
{
Path = new DirectoryInfo(sourcePath),
diff --git a/src/tooling/docs-builder/Http/ReloadGeneratorService.cs b/src/tooling/docs-builder/Http/ReloadGeneratorService.cs
index 43776a17e4..1097b5723e 100644
--- a/src/tooling/docs-builder/Http/ReloadGeneratorService.cs
+++ b/src/tooling/docs-builder/Http/ReloadGeneratorService.cs
@@ -49,8 +49,7 @@ public async Task StartAsync(Cancel cancellationToken)
_serviceCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Await the live-reload generator so the server can serve pages immediately.
- var sourcePath = ReloadableGenerator.Generator.Context.DocumentationCheckoutDirectory?.FullName
- ?? ReloadableGenerator.Generator.Context.DocumentationSourceDirectory.FullName;
+ var sourcePath = ReloadableGenerator.Generator.Context.DocumentationCheckoutDirectory.FullName;
await ReloadableGenerator.ReloadAsync(cancellationToken);
// Start the build loop; only shutdownCt (Ctrl+C / app exit) can cancel a running build.
@@ -58,13 +57,7 @@ public async Task StartAsync(Cancel cancellationToken)
_backgroundBuildTask = InMemoryBuildState.RunAsync(_serviceCts.Token);
InMemoryBuildState.ScheduleBuild(sourcePath);
- // ReSharper disable once RedundantAssignment
- var directory = ReloadableGenerator.Generator.DocumentationSet.SourceDirectory.FullName;
-#if DEBUG
- // Fall back to source directory when there is no separate checkout directory (e.g. when serving the project's own docs from a worktree)
- directory = ReloadableGenerator.Generator.Context.DocumentationCheckoutDirectory?.FullName
- ?? ReloadableGenerator.Generator.DocumentationSet.SourceDirectory.FullName;
-#endif
+ var directory = ReloadableGenerator.Generator.Context.DocumentationCheckoutDirectory.FullName;
Logger.LogInformation("Start file watch on: {Directory}", directory);
var watcher = new FileSystemWatcher(directory)
{
@@ -108,8 +101,7 @@ private void Reload(bool reloadConfiguration = false)
// Schedule a validation build after every reload — both content edits and structural changes.
// The build loop coalesces rapid triggers: a new request while a build runs queues one more.
- var sourcePath = ReloadableGenerator.Generator.Context.DocumentationCheckoutDirectory?.FullName
- ?? ReloadableGenerator.Generator.Context.DocumentationSourceDirectory.FullName;
+ var sourcePath = ReloadableGenerator.Generator.Context.DocumentationCheckoutDirectory.FullName;
InMemoryBuildState.ScheduleBuild(sourcePath);
}, token);
}
diff --git a/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs b/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs
index 6ed55dd6bb..7e667d6600 100644
--- a/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs
+++ b/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs
@@ -7,6 +7,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Builder;
using Elastic.Documentation.Configuration.ReleaseNotes;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.LinkIndex;
using Elastic.Documentation.Links.CrossLinks;
using Elastic.Markdown;
@@ -47,7 +48,7 @@ bool isWatchBuild
ApiPath = context.WriteFileSystem.DirectoryInfo.New(Path.Join(outputPath.FullName, "api"));
if (context.Configuration.Registry != DocSetRegistry.Public)
- _codexReader = new GitLinkIndexReader(context.Configuration.Registry.ToStringFast(true), FileSystemFactory.AppData);
+ _codexReader = new GitLinkIndexReader(context.Configuration.Registry.ToStringFast(true), new ApplicationDataFileSystem());
_crossLinkFetcher = new DocSetConfigurationCrossLinkFetcher(logFactory, _context.Configuration, codexLinkIndexReader: _codexReader);
// we pass NoopCrossLinkResolver.Instance here because `ReloadAsync` will always be called when the is started.
@@ -80,7 +81,7 @@ public async Task ReloadAsync(Cancel ctx, bool reloadConfiguration = true)
_context.ReloadConfiguration();
(_codexReader as IDisposable)?.Dispose();
_codexReader = _context.Configuration.Registry != DocSetRegistry.Public
- ? new GitLinkIndexReader(_context.Configuration.Registry.ToStringFast(true), FileSystemFactory.AppData)
+ ? new GitLinkIndexReader(_context.Configuration.Registry.ToStringFast(true), new ApplicationDataFileSystem())
: null;
_crossLinkFetcher = new DocSetConfigurationCrossLinkFetcher(_logFactory, _context.Configuration, codexLinkIndexReader: _codexReader);
}
diff --git a/src/tooling/docs-builder/Http/StaticWebHost.cs b/src/tooling/docs-builder/Http/StaticWebHost.cs
index a0d99a67b8..9b2638242e 100644
--- a/src/tooling/docs-builder/Http/StaticWebHost.cs
+++ b/src/tooling/docs-builder/Http/StaticWebHost.cs
@@ -7,6 +7,7 @@
using Elastic.Documentation.Api;
#endif
using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Extensions;
using Elastic.Documentation.ServiceDefaults;
using Microsoft.AspNetCore.Builder;
@@ -25,7 +26,7 @@ public class StaticWebHost
public StaticWebHost(int port, string? path)
{
_contentRoot = path ?? Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly");
- var fs = FileSystemFactory.RealGitRootForPath(_contentRoot);
+ var fs = CheckoutsFileSystem.FromWorkingDirectory();
var dir = fs.DirectoryInfo.New(_contentRoot);
if (!dir.Exists)
throw new Exception($"Can not serve empty directory: {_contentRoot}");
diff --git a/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs b/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs
index 35670eadd2..cdf10b745b 100644
--- a/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs
+++ b/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs
@@ -6,6 +6,7 @@
using System.Reflection;
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Versions;
using Microsoft.Extensions.Logging;
using Nullean.Argh.Middleware;
@@ -15,7 +16,7 @@ namespace Documentation.Builder.Middleware;
internal sealed class CheckForUpdatesMiddleware(ILogger logger) : ICommandMiddleware
{
// Only accesses ApplicationData — no workspace access needed
- private static readonly IFileSystem Fs = FileSystemFactory.AppData;
+ private static readonly ApplicationDataFileSystem Fs = new();
private readonly IFileInfo _stateFile = Fs.FileInfo.New(Path.Join(Paths.ApplicationData.FullName, "docs-build-check.state"));
private readonly ILogger _logger = logger;
diff --git a/tests-integration/Elastic.Documentation.IntegrationTests/AssemblerConfigurationTests.cs b/tests-integration/Elastic.Documentation.IntegrationTests/AssemblerConfigurationTests.cs
index 6008c930fa..0c7ba73650 100644
--- a/tests-integration/Elastic.Documentation.IntegrationTests/AssemblerConfigurationTests.cs
+++ b/tests-integration/Elastic.Documentation.IntegrationTests/AssemblerConfigurationTests.cs
@@ -8,8 +8,8 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.Logging.Abstractions;
-using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.IntegrationTests;
@@ -26,11 +26,11 @@ public PublicOnlyAssemblerConfigurationTests()
FileSystem.Path.Join(Paths.GetSolutionDirectory()!.FullName, ".artifacts", "checkouts")
);
Collector = new DiagnosticsCollector([]);
- var configurationFileProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, FileSystem, skipPrivateRepositories: true);
+ var configurationFileProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, new ConfigurationFileSystem(FileSystem), skipPrivateRepositories: true);
var configurationContext = TestHelpers.CreateConfigurationContext(FileSystem, configurationFileProvider: configurationFileProvider);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem);
- Context = new AssembleContext(config, configurationContext, "dev", Collector, scopedFs, scopedFs, CheckoutDirectory.FullName, null);
+ var assembleFs = CheckoutsFileSystem.FromWorkingDirectory(FileSystem);
+ Context = new AssembleContext(config, configurationContext, "dev", Collector, assembleFs, CheckoutDirectory.FullName, null);
}
[Fact]
@@ -66,8 +66,8 @@ public AssemblerConfigurationTests(DocumentationFixture fixture, ITestOutputHelp
Collector = new DiagnosticsCollector([]);
var configurationContext = TestHelpers.CreateConfigurationContext(FileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem);
- Context = new AssembleContext(config, configurationContext, "dev", Collector, scopedFs, scopedFs, CheckoutDirectory.FullName, null);
+ var assembleFs2 = CheckoutsFileSystem.FromWorkingDirectory(FileSystem);
+ Context = new AssembleContext(config, configurationContext, "dev", Collector, assembleFs2, CheckoutDirectory.FullName, null);
}
[Fact]
diff --git a/tests-integration/Elastic.Documentation.IntegrationTests/DocsSyncTests.cs b/tests-integration/Elastic.Documentation.IntegrationTests/DocsSyncTests.cs
index 5f90a5d00d..096528a084 100644
--- a/tests-integration/Elastic.Documentation.IntegrationTests/DocsSyncTests.cs
+++ b/tests-integration/Elastic.Documentation.IntegrationTests/DocsSyncTests.cs
@@ -13,11 +13,11 @@
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Deploying.Synchronization;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Integrations.S3;
using Elastic.Documentation.ServiceDefaults.Telemetry;
using FakeItEasy;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
using OpenTelemetry;
using OpenTelemetry.Trace;
@@ -46,9 +46,8 @@ public async Task TestPlan()
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem);
- var scopedWriteFs = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fileSystem);
- var context = new AssembleContext(config, configurationContext, "dev", collector, scopedFs, scopedWriteFs, null, Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"));
+ var assembleFs = CheckoutsFileSystem.FromWorkingDirectory(fileSystem);
+ var context = new AssembleContext(config, configurationContext, "dev", collector, assembleFs, null, Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"));
A.CallTo(() => mockS3Client.ListObjectsV2Async(A._, A._))
.Returns(new ListObjectsV2Response
{
@@ -189,9 +188,8 @@ bool valid
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- var scopedFs2 = FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem);
- var scopedWriteFs2 = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fileSystem);
- var context = new AssembleContext(config, configurationContext, "dev", collector, scopedFs2, scopedWriteFs2, null, Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"));
+ var assembleFs2 = CheckoutsFileSystem.FromWorkingDirectory(fileSystem);
+ var context = new AssembleContext(config, configurationContext, "dev", collector, assembleFs2, null, Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"));
var s3Objects = new List();
foreach (var i in Enumerable.Range(0, remoteFiles))
@@ -241,9 +239,8 @@ public async Task TestApply()
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
var checkoutDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly");
- var scopedFs3 = FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem);
- var scopedWriteFs3 = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fileSystem);
- var context = new AssembleContext(config, configurationContext, "dev", collector, scopedFs3, scopedWriteFs3, null, checkoutDirectory);
+ var assembleFs3 = CheckoutsFileSystem.FromWorkingDirectory(fileSystem);
+ var context = new AssembleContext(config, configurationContext, "dev", collector, assembleFs3);
var plan = new SyncPlan
{
RemoteListingCompleted = true,
diff --git a/tests-integration/Elastic.Documentation.IntegrationTests/IncrementalDeployRoundTripTests.cs b/tests-integration/Elastic.Documentation.IntegrationTests/IncrementalDeployRoundTripTests.cs
index 5facecbe6a..b48d0f139c 100644
--- a/tests-integration/Elastic.Documentation.IntegrationTests/IncrementalDeployRoundTripTests.cs
+++ b/tests-integration/Elastic.Documentation.IntegrationTests/IncrementalDeployRoundTripTests.cs
@@ -16,10 +16,10 @@
using Elastic.Documentation.Deploying;
using Elastic.Documentation.Deploying.Synchronization;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Integrations.S3;
using FakeItEasy;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.IntegrationTests;
@@ -47,9 +47,8 @@ public async Task AssemblerRoundTrip()
var configurationContext = TestHelpers.CreateConfigurationContext(fs);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
var collector = new DiagnosticsCollector([]);
- var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
- var scopedWriteFs = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fs);
- var context = new AssembleContext(config, configurationContext, "dev", collector, scopedFs, scopedWriteFs, null, outputDir);
+ var assembleFs = CheckoutsFileSystem.FromWorkingDirectory(fs);
+ var context = new AssembleContext(config, configurationContext, "dev", collector, assembleFs, null, outputDir);
await RunRoundTrip(fs, s3, xfer, gh, svc, context, outputDir);
}
@@ -60,13 +59,12 @@ public async Task CodexRoundTrip()
var outputDir = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "codex", "docs");
var (fs, s3, xfer, gh, svc) = Arrange(outputDir);
var collector = new DiagnosticsCollector([]);
- var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
- var scopedWriteFs = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fs);
+ var codexFs = CheckoutsFileSystem.FromWorkingDirectory(fs);
// CodexContext only stores configurationPath — it never reads from it —
// so we can point to any path without adding it to the mock FS.
var codexConfig = new CodexConfiguration { Environment = "dev" };
var configFile = fs.FileInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName, "codex.yml"));
- var context = new CodexContext(codexConfig, configFile, collector, scopedFs, scopedWriteFs, null, outputDir);
+ var context = new CodexContext(codexConfig, configFile, collector, codexFs, null, outputDir);
await RunRoundTrip(fs, s3, xfer, gh, svc, context, outputDir);
}
@@ -227,11 +225,10 @@ public async Task ExcludedRemoteObjectsAreNotDeleted()
var svc = new IncrementalDeployService(new LoggerFactory(), gh, s3, xfer, etagCalculator);
var collector = new DiagnosticsCollector([]);
- var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
- var scopedWriteFs = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fs);
+ var codexFs2 = CheckoutsFileSystem.FromWorkingDirectory(fs);
var codexConfig = new CodexConfiguration { Environment = "dev" };
var configFile = fs.FileInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName, "codex.yml"));
- var context = new CodexContext(codexConfig, configFile, collector, scopedFs, scopedWriteFs, null, outputDir);
+ var context = new CodexContext(codexConfig, configFile, collector, codexFs2, null, outputDir);
var planPath = Path.Join(outputDir, "sync-plan.json");
var planOk = await svc.Plan(
diff --git a/tests-integration/Elastic.Documentation.IntegrationTests/NavigationBuildingTests.cs b/tests-integration/Elastic.Documentation.IntegrationTests/NavigationBuildingTests.cs
index 5f34d33929..6b0050e00b 100644
--- a/tests-integration/Elastic.Documentation.IntegrationTests/NavigationBuildingTests.cs
+++ b/tests-integration/Elastic.Documentation.IntegrationTests/NavigationBuildingTests.cs
@@ -13,6 +13,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Configuration.Toc;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation;
using Elastic.Documentation.Navigation.Assembler;
using Elastic.Documentation.Navigation.Isolated.Leaf;
@@ -21,7 +22,6 @@
using Elastic.Documentation.Site.Navigation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
-using Nullean.ScopedFileSystem;
using RazorSlices;
namespace Elastic.Documentation.IntegrationTests;
@@ -46,7 +46,9 @@ public async Task AssertRealNavigation()
var assemblyConfiguration = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
var collector = new TestDiagnosticsCollector(TestContext.Current.TestOutputHelper);
var fs = new FileSystem();
- var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, "dev", collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fs), FileSystemFactory.ScopeCurrentWorkingDirectory(new MockFileSystem()), null, null);
+ var assembleFs = CheckoutsFileSystem.FromWorkingDirectory(fs);
+ var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, "dev", collector,
+ assembleFs);
var logFactory = new TestLoggerFactory(TestContext.Current.TestOutputHelper);
var cloner = new AssemblerRepositorySourcer(logFactory, assembleContext);
var checkoutResult = cloner.GetAll();
diff --git a/tests-integration/Elastic.Documentation.IntegrationTests/NavigationRootTests.cs b/tests-integration/Elastic.Documentation.IntegrationTests/NavigationRootTests.cs
index 00e9624f6a..61f5c36780 100644
--- a/tests-integration/Elastic.Documentation.IntegrationTests/NavigationRootTests.cs
+++ b/tests-integration/Elastic.Documentation.IntegrationTests/NavigationRootTests.cs
@@ -13,6 +13,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Configuration.Toc;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation;
using Elastic.Documentation.Navigation.Assembler;
using Elastic.Documentation.Navigation.Isolated;
@@ -21,7 +22,6 @@
using Elastic.Documentation.Site.Navigation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
-using Nullean.ScopedFileSystem;
using RazorSlices;
namespace Elastic.Documentation.IntegrationTests;
@@ -46,7 +46,9 @@ public async Task AssertRealNavigation()
var assemblyConfiguration = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
var collector = new TestDiagnosticsCollector(TestContext.Current.TestOutputHelper);
var fs = new FileSystem();
- var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, "dev", collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fs), FileSystemFactory.ScopeCurrentWorkingDirectory(new MockFileSystem()), null, null);
+ var assembleFs = CheckoutsFileSystem.FromWorkingDirectory(fs);
+ var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, "dev", collector,
+ assembleFs);
var logFactory = new TestLoggerFactory(TestContext.Current.TestOutputHelper);
var cloner = new AssemblerRepositorySourcer(logFactory, assembleContext);
var checkoutResult = cloner.GetAll();
diff --git a/tests-integration/Elastic.Documentation.IntegrationTests/SiteNavigationTests.cs b/tests-integration/Elastic.Documentation.IntegrationTests/SiteNavigationTests.cs
index f3cb9a098c..3afa2bfdc1 100644
--- a/tests-integration/Elastic.Documentation.IntegrationTests/SiteNavigationTests.cs
+++ b/tests-integration/Elastic.Documentation.IntegrationTests/SiteNavigationTests.cs
@@ -11,11 +11,11 @@
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation;
using Elastic.Documentation.Navigation.Assembler;
using Elastic.Markdown.IO;
using Microsoft.Extensions.Logging.Abstractions;
-using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.IntegrationTests;
@@ -44,8 +44,8 @@ public SiteNavigationTests(DocumentationFixture fixture, ITestOutputHelper outpu
Collector = new DiagnosticsCollector([]);
var configurationContext = TestHelpers.CreateConfigurationContext(FileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem);
- Context = new AssembleContext(config, configurationContext, "dev", Collector, scopedFs, scopedFs, CheckoutDirectory.FullName, null);
+ var assembleFs = CheckoutsFileSystem.FromWorkingDirectory(FileSystem);
+ Context = new AssembleContext(config, configurationContext, "dev", Collector, assembleFs, CheckoutDirectory.FullName, null);
}
private Checkout CreateCheckout(IFileSystem fs, Repository repository)
@@ -98,8 +98,8 @@ public async Task ReadAllPathPrefixes()
var fileSystem = new FileSystem();
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- var scopedFileSystem = FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem);
- var context = new AssembleContext(config, configurationContext, "dev", collector, scopedFileSystem, scopedFileSystem, null, null);
+ var assembleFs2 = CheckoutsFileSystem.FromWorkingDirectory(fileSystem);
+ var context = new AssembleContext(config, configurationContext, "dev", collector, assembleFs2);
var navigationFileInfo = configurationContext.ConfigurationFileProvider.NavigationFile;
var siteNavigationFile = SiteNavigationFile.Deserialize(await FileSystem.File.ReadAllTextAsync(navigationFileInfo.FullName, TestContext.Current.CancellationToken));
@@ -191,8 +191,8 @@ public async Task UriResolving()
var fs = new FileSystem();
var configurationContext = TestHelpers.CreateConfigurationContext(fs);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
- var assembleContext = new AssembleContext(config, configurationContext, "prod", collector, scopedFs, scopedFs, null, null);
+ var assembleFs3 = CheckoutsFileSystem.FromWorkingDirectory(fs);
+ var assembleContext = new AssembleContext(config, configurationContext, "prod", collector, assembleFs3);
var repos = assembleContext.Configuration.AvailableRepositories
.Where(kv => !kv.Value.Skip)
.Select(kv => kv.Value)
diff --git a/tests-integration/Elastic.Documentation.IntegrationTests/TestHelpers.cs b/tests-integration/Elastic.Documentation.IntegrationTests/TestHelpers.cs
index 7098a012de..26952c33a3 100644
--- a/tests-integration/Elastic.Documentation.IntegrationTests/TestHelpers.cs
+++ b/tests-integration/Elastic.Documentation.IntegrationTests/TestHelpers.cs
@@ -10,6 +10,7 @@
using Elastic.Documentation.Configuration.Products;
using Elastic.Documentation.Configuration.Search;
using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Versions;
using Microsoft.Extensions.Logging.Abstractions;
@@ -24,7 +25,7 @@ public static IConfigurationContext CreateConfigurationContext(
ProductsConfiguration? productsConfiguration = null
)
{
- configurationFileProvider ??= new ConfigurationFileProvider(NullLoggerFactory.Instance, fileSystem, skipPrivateRepositories: true);
+ configurationFileProvider ??= new ConfigurationFileProvider(NullLoggerFactory.Instance, new ConfigurationFileSystem(fileSystem), skipPrivateRepositories: true);
versionsConfiguration ??= new VersionsConfiguration
{
VersioningSystems = new Dictionary
diff --git a/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs b/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs
index e0152cfc1a..d448f4559e 100644
--- a/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs
+++ b/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs
@@ -8,6 +8,7 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Search;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Search;
using Elastic.Documentation.Search.Common;
using Elastic.Documentation.Search.Contract;
@@ -245,7 +246,7 @@ public async Task ExplainTopResultAndExpectedAsyncReturnsDetailedScoring()
private static (NavigationSearchService Gateway, ElasticsearchClientAccessor ClientAccessor) CreateFindPageGateway()
{
var endpoints = ElasticsearchEndpointFactory.Create(buildType: "assembler");
- var configProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, new FileSystem(), configurationSource: ConfigurationSource.Embedded);
+ var configProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, new ConfigurationFileSystem(), configurationSource: ConfigurationSource.Embedded);
var searchConfig = configProvider.CreateSearchConfiguration();
var clientAccessor = new ElasticsearchClientAccessor(endpoints, searchConfig);
diff --git a/tests/Elastic.ApiExplorer.Tests/ApiExplorerFixture.cs b/tests/Elastic.ApiExplorer.Tests/ApiExplorerFixture.cs
index d683d47382..3bbca387f2 100644
--- a/tests/Elastic.ApiExplorer.Tests/ApiExplorerFixture.cs
+++ b/tests/Elastic.ApiExplorer.Tests/ApiExplorerFixture.cs
@@ -8,10 +8,10 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.OpenApi;
-using Nullean.ScopedFileSystem;
namespace Elastic.ApiExplorer.Tests;
@@ -40,10 +40,10 @@ public sealed class ApiExplorerFixture : IAsyncLifetime
public async ValueTask InitializeAsync()
{
- var configurationContext = TestHelpers.CreateConfigurationContext(new FileSystem());
- // RealGitRootForPath(null) rather than RealRead: it adds the main repo's .git dir as a scope
- // root when the checkout is a git worktree, which BuildContext needs to read git information.
- Context = new BuildContext(new DiagnosticsCollector([]), FileSystemFactory.RealGitRootForPath(null), configurationContext);
+ var realFs = new FileSystem();
+ var configurationContext = TestHelpers.CreateConfigurationContext(realFs);
+ var invocation = realFs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
+ Context = new BuildContext(new DiagnosticsCollector([]), DocumentationFileSystem.Resolve(invocation), configurationContext);
var fs = new FileSystem();
var path = fs.Path.Combine(AppContext.BaseDirectory, "TestData", "api-explorer-fixture.json");
diff --git a/tests/Elastic.ApiExplorer.Tests/ApiMarkdownIntraApiLinkTests.cs b/tests/Elastic.ApiExplorer.Tests/ApiMarkdownIntraApiLinkTests.cs
index fbe83eb50d..a85fb3ade7 100644
--- a/tests/Elastic.ApiExplorer.Tests/ApiMarkdownIntraApiLinkTests.cs
+++ b/tests/Elastic.ApiExplorer.Tests/ApiMarkdownIntraApiLinkTests.cs
@@ -11,9 +11,9 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Site.FileProviders;
using Microsoft.OpenApi;
-using Nullean.ScopedFileSystem;
namespace Elastic.ApiExplorer.Tests;
@@ -37,7 +37,7 @@ public void Render_RewritesGroupAndOperationLinksAgainstCurrentApiBase()
var renderer = new CapturingRenderer();
var collector = new DiagnosticsCollector([]);
var fs = new FileSystem();
- var context = new BuildContext(collector, FileSystemFactory.RealGitRootForPath(null), TestHelpers.CreateConfigurationContext(fs));
+ var context = new BuildContext(collector, DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName), TestHelpers.CreateConfigurationContext(fs));
var renderContext = new ApiRenderContext(context, new OpenApiDocument(), new StaticFileContentHashProvider(new EmbeddedOrPhysicalFileProvider(context)))
{
NavigationHtml = string.Empty,
diff --git a/tests/Elastic.ApiExplorer.Tests/ApiPagesNavRenderingTests.cs b/tests/Elastic.ApiExplorer.Tests/ApiPagesNavRenderingTests.cs
index b7d7c416b1..e10b977137 100644
--- a/tests/Elastic.ApiExplorer.Tests/ApiPagesNavRenderingTests.cs
+++ b/tests/Elastic.ApiExplorer.Tests/ApiPagesNavRenderingTests.cs
@@ -13,9 +13,9 @@
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Configuration.Builder;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Site;
using Elastic.Documentation.Site.FileProviders;
-using Nullean.ScopedFileSystem;
using RazorSlices;
namespace Elastic.ApiExplorer.Tests;
@@ -28,7 +28,7 @@ public async Task Render_MarksOnlyCurrentVersionSelected()
var fs = new FileSystem();
var context = new BuildContext(
new DiagnosticsCollector([]),
- FileSystemFactory.RealGitRootForPath(null),
+ DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName),
TestHelpers.CreateConfigurationContext(fs));
var navigationItem = new LandingNavigationItem("/api/doc/elasticsearch/v9/").Index;
var model = new ApiLayoutViewModel
diff --git a/tests/Elastic.ApiExplorer.Tests/DashboardOpenApiNavigationTests.cs b/tests/Elastic.ApiExplorer.Tests/DashboardOpenApiNavigationTests.cs
index fd3a8cb620..2a11d229a1 100644
--- a/tests/Elastic.ApiExplorer.Tests/DashboardOpenApiNavigationTests.cs
+++ b/tests/Elastic.ApiExplorer.Tests/DashboardOpenApiNavigationTests.cs
@@ -10,8 +10,8 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.Logging.Abstractions;
-using Nullean.ScopedFileSystem;
namespace Elastic.ApiExplorer.Tests;
@@ -25,7 +25,7 @@ public class DashboardOpenApiNavigationTests
public async Task CreateNavigation_SingleTagOpenApiSpec_HasSidebarItems()
{
var configurationContext = TestHelpers.CreateConfigurationContext(new FileSystem());
- var context = new BuildContext(new DiagnosticsCollector([]), FileSystemFactory.RealGitRootForPath(null), configurationContext);
+ var context = new BuildContext(new DiagnosticsCollector([]), DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName), configurationContext);
var fs = new FileSystem();
var path = fs.Path.Combine(Paths.WorkingDirectoryRoot.FullName, "docs", "dashboard-openapi.json");
var fi = fs.FileInfo.New(path);
diff --git a/tests/Elastic.ApiExplorer.Tests/KibanaApiMarkdownNavigationTests.cs b/tests/Elastic.ApiExplorer.Tests/KibanaApiMarkdownNavigationTests.cs
index 6f41961b19..8fbbce8e50 100644
--- a/tests/Elastic.ApiExplorer.Tests/KibanaApiMarkdownNavigationTests.cs
+++ b/tests/Elastic.ApiExplorer.Tests/KibanaApiMarkdownNavigationTests.cs
@@ -14,11 +14,11 @@
using Elastic.Documentation.Configuration.Products;
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation;
using Elastic.Documentation.Site.FileProviders;
using Elastic.Documentation.Site.Navigation;
using Microsoft.Extensions.Logging.Abstractions;
-using Nullean.ScopedFileSystem;
namespace Elastic.ApiExplorer.Tests;
@@ -54,7 +54,7 @@ private static (LandingNavigationItem navigation, SimpleMarkdownNavigationItem i
var collector = new DiagnosticsCollector([]);
var configurationContext = TestHelpers.CreateConfigurationContext(fs);
- var context = new BuildContext(collector, FileSystemFactory.RealGitRootForPath(null), configurationContext);
+ var context = new BuildContext(collector, DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName), configurationContext);
var doc = OpenApiReader.Instance.ReadAsync(specFile).GetAwaiter().GetResult();
doc.Should().NotBeNull("OpenAPI document should load successfully");
var generator = new OpenApiGenerator(NullLoggerFactory.Instance, context, NoopMarkdownStringRenderer.Instance);
diff --git a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCurrentSpecResolutionTests.cs b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCurrentSpecResolutionTests.cs
index 63cf07b041..6bad9da9f8 100644
--- a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCurrentSpecResolutionTests.cs
+++ b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCurrentSpecResolutionTests.cs
@@ -13,10 +13,10 @@
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using FakeItEasy;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.OpenApi;
-using Nullean.ScopedFileSystem;
namespace Elastic.ApiExplorer.Tests;
@@ -30,10 +30,9 @@ private static BuildContext CreateContext(
ProductsConfiguration? productsConfiguration = null,
GitCheckoutInformation? git = null)
{
- var fs = FileSystemFactory.RealGitRootForPath(null);
- return new BuildContext(collector, fs, fs,
- TestHelpers.CreateConfigurationContext(new FileSystem(), versionsConfiguration, productsConfiguration),
- ExportOptions.Default, null, null, gitCheckoutInformation: git);
+ return new BuildContext(collector,
+ DocumentationFileSystem.Resolve(new FileSystem().DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), new DocumentationScopeOptions { Git = git }),
+ TestHelpers.CreateConfigurationContext(new FileSystem(), versionsConfiguration, productsConfiguration));
}
private static ResolvedApiConfiguration ApiConfig(Product product, IFileInfo? localSpecFile = null) => new()
diff --git a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs
index 61fce88b2f..7eff1c5823 100644
--- a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs
+++ b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs
@@ -17,10 +17,10 @@
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using FakeItEasy;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.OpenApi;
-using Nullean.ScopedFileSystem;
namespace Elastic.ApiExplorer.Tests;
@@ -34,9 +34,9 @@ private static BuildContext CreateContext(
ProductsConfiguration? productsConfiguration = null,
GitCheckoutInformation? git = null)
{
- var fs = FileSystemFactory.RealGitRootForPath(null);
- return new BuildContext(collector, fs, fs, TestHelpers.CreateConfigurationContext(new FileSystem(), versionsConfiguration, productsConfiguration),
- ExportOptions.Default, null, null, gitCheckoutInformation: git);
+ return new BuildContext(collector,
+ DocumentationFileSystem.Resolve(new FileSystem().DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), new DocumentationScopeOptions { Git = git }),
+ TestHelpers.CreateConfigurationContext(new FileSystem(), versionsConfiguration, productsConfiguration));
}
private static ResolvedApiConfiguration ApiConfig(
@@ -217,13 +217,17 @@ private static BuildContext CreateGenerateContext(
var fs = new MockFileSystem(new MockFileSystemOptions { CurrentDirectory = Paths.WorkingDirectoryRoot.FullName });
fs.AddDirectory(Path.Join(repoRoot, ".git"));
fs.AddFile(configPath, new MockFileData(docsetYaml));
- var readFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
- var writeFs = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fs);
var configurationContext = TestHelpers.CreateConfigurationContext(fs, versionsConfiguration, productsConfiguration);
- return new BuildContext(collector, readFs, writeFs, configurationContext,
- ExportOptions.Default, source: repoRoot, output: outputRoot, gitCheckoutInformation: git,
- configurationFile: readFs.FileInfo.New(configPath));
+ return new BuildContext(collector,
+ DocumentationFileSystem.Resolve(repoRoot, new DocumentationScopeOptions
+ {
+ ConfigurationFile = configPath,
+ Output = outputRoot,
+ Git = git,
+ Inner = fs
+ }),
+ configurationContext);
}
private static OpenApiGenerator CreateGenerator(BuildContext context, VersionIndexClient versionIndexClient, IOpenApiSpecificationReader reader) =>
diff --git a/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs b/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs
index 1445f941bc..88672a7060 100644
--- a/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs
+++ b/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs
@@ -11,8 +11,8 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.Logging.Abstractions;
-using Nullean.ScopedFileSystem;
namespace Elastic.ApiExplorer.Tests;
@@ -52,7 +52,7 @@ public async Task Navigation()
{
var collector = new DiagnosticsCollector([]);
var configurationContext = TestHelpers.CreateConfigurationContext(new FileSystem());
- var context = new BuildContext(collector, FileSystemFactory.RealGitRootForPath(null), configurationContext);
+ var context = new BuildContext(collector, DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName), configurationContext);
var generator = new OpenApiGenerator(NullLoggerFactory.Instance, context, NoopMarkdownStringRenderer.Instance);
var openApiDocument = await OpenApiReader.Instance.ReadAsync(LocalSpecFile());
diff --git a/tests/Elastic.ApiExplorer.Tests/TagMetadataTests.cs b/tests/Elastic.ApiExplorer.Tests/TagMetadataTests.cs
index f04ea80d2f..a8016491c6 100644
--- a/tests/Elastic.ApiExplorer.Tests/TagMetadataTests.cs
+++ b/tests/Elastic.ApiExplorer.Tests/TagMetadataTests.cs
@@ -12,10 +12,10 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Reader;
-using Nullean.ScopedFileSystem;
namespace Elastic.ApiExplorer.Tests;
@@ -285,7 +285,7 @@ public async Task ApiTag_StableNavigationIds_UsesCanonicalTagName()
{
var collector = new DiagnosticsCollector([]);
var configurationContext = TestHelpers.CreateConfigurationContext(new FileSystem());
- var context = new BuildContext(collector, FileSystemFactory.RealGitRootForPath(null), configurationContext);
+ var context = new BuildContext(collector, DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName), configurationContext);
var generator = new OpenApiGenerator(NullLoggerFactory.Instance, context, NoopMarkdownStringRenderer.Instance);
diff --git a/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs b/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs
index 450c7b2a22..3b03b19924 100644
--- a/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs
+++ b/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs
@@ -14,6 +14,7 @@
using Elastic.Documentation.Configuration.Products;
using Elastic.Documentation.Configuration.Search;
using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Versions;
using Microsoft.Extensions.Logging.Abstractions;
@@ -51,7 +52,7 @@ public static IConfigurationContext CreateConfigurationContext(IFileSystem fileS
{
Elasticsearch = ElasticsearchEndpoint.Default,
},
- ConfigurationFileProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, fileSystem),
+ ConfigurationFileProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, new ConfigurationFileSystem(fileSystem)),
VersionsConfiguration = versionsConfiguration,
ProductsConfiguration = productsConfiguration,
LegacyUrlMappings = new LegacyUrlMappingConfiguration { Mappings = [] },
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs
index 14f8c4ec8c..0faa715f48 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs
@@ -20,7 +20,7 @@ public class BundleAmendTests : ChangelogTestBase
public BundleAmendTests(ITestOutputHelper output) : base(output)
{
Service = new(LoggerFactory, FileSystem);
- BundleService = new(LoggerFactory, null, FileSystem);
+ BundleService = new(LoggerFactory, FileSystem);
_changelogDir = CreateChangelogDir();
}
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs
index 49cacb9969..63b770eae0 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs
@@ -74,7 +74,7 @@ public async Task OptionMode_RepoResolvable_SourcesAllEntriesFromRepoPoolOnCdn()
// Under the artifact-root layout the CDN entry pool is keyed by the authoring repo, not the
// target product. A resolvable repo (here via --repo) is what enables CDN sourcing.
var handler = RegistryHandler();
- var service = new ChangelogBundlingService(LoggerFactory, null, FileSystem, null, Fetcher(Output, handler));
+ var service = new ChangelogBundlingService(LoggerFactory, FileSystem, null, null, Fetcher(Output, handler));
var output = OutputPath();
var input = new BundleChangelogsArguments
@@ -103,7 +103,7 @@ public async Task OptionMode_OwnerAndBranchOverride_SourcesFromThatPoolOnCdn()
{
// Explicit owner/branch select a specific pool; the branch is stored verbatim (dots kept).
var handler = RegistryHandler();
- var service = new ChangelogBundlingService(LoggerFactory, null, FileSystem, null, Fetcher(Output, handler));
+ var service = new ChangelogBundlingService(LoggerFactory, FileSystem, null, null, Fetcher(Output, handler));
var output = OutputPath();
var input = new BundleChangelogsArguments
@@ -128,7 +128,7 @@ public async Task OptionMode_OwnerFromCombinedRepo_SourcesFromThatPool()
// When --repo is given in owner/repo form and no explicit owner is set, the owner segment must be
// taken from the repo prefix (not defaulted to elastic), so the CDN pool path stays correct.
var handler = RegistryHandler();
- var service = new ChangelogBundlingService(LoggerFactory, null, FileSystem, null, Fetcher(Output, handler));
+ var service = new ChangelogBundlingService(LoggerFactory, FileSystem, null, null, Fetcher(Output, handler));
var output = OutputPath();
var input = new BundleChangelogsArguments
@@ -166,7 +166,7 @@ await FileSystem.File.WriteAllTextAsync(
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
var handler = RegistryHandler();
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystem, null, Fetcher(Output, handler));
+ var service = new ChangelogBundlingService(LoggerFactory, FileSystem, ConfigurationContext, null, Fetcher(Output, handler));
var output = OutputPath();
var input = new BundleChangelogsArguments { Config = configPath, Output = output, All = true };
@@ -202,7 +202,7 @@ await FileSystem.File.WriteAllTextAsync(
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
var handler = RegistryHandler();
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystem, null, Fetcher(Output, handler));
+ var service = new ChangelogBundlingService(LoggerFactory, FileSystem, ConfigurationContext, null, Fetcher(Output, handler));
var output = OutputPath();
var input = new BundleChangelogsArguments
@@ -226,7 +226,7 @@ public async Task RegistryFailure_FailsBundle()
{
var fetcher = new CdnChangelogEntryFetcher(new TestLoggerFactory(Output),
new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.NotFound)), sleep: (_, _) => Task.CompletedTask);
- var service = new ChangelogBundlingService(LoggerFactory, null, FileSystem, null, fetcher);
+ var service = new ChangelogBundlingService(LoggerFactory, FileSystem, null, null, fetcher);
var input = new BundleChangelogsArguments
{
@@ -256,7 +256,7 @@ public async Task EntryMissingAfterRetries_FailsBundle()
return new HttpResponseMessage(HttpStatusCode.NotFound); // 2-bravo.yaml never propagates
});
var fetcher = new CdnChangelogEntryFetcher(new TestLoggerFactory(Output), handler, maxAttempts: 2, sleep: (_, _) => Task.CompletedTask);
- var service = new ChangelogBundlingService(LoggerFactory, null, FileSystem, null, fetcher);
+ var service = new ChangelogBundlingService(LoggerFactory, FileSystem, null, null, fetcher);
var input = new BundleChangelogsArguments
{
@@ -301,7 +301,7 @@ public async Task ProfileGitHubRelease_ScopesByOutputProductsAndFiltersByRelease
A.CallTo(() => releaseService.FetchReleaseAsync("elastic", "elasticsearch", "9.3.0", TestContext.Current.CancellationToken))
.Returns(new GitHubReleaseInfo { TagName = "v9.3.0", Name = "9.3.0", Body = releaseBody });
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystem, releaseService, Fetcher());
+ var service = new ChangelogBundlingService(LoggerFactory, FileSystem, ConfigurationContext, releaseService, Fetcher());
var input = new BundleChangelogsArguments
{
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs
index 66e03bd316..8f195f8c52 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs
@@ -8,6 +8,7 @@
using Elastic.Changelog.Utilities;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.Logging.Abstractions;
namespace Elastic.Changelog.Tests.Changelogs;
@@ -20,8 +21,8 @@ public class BundleChangelogsTests : ChangelogTestBase
public BundleChangelogsTests(ITestOutputHelper output) : base(output)
{
- Service = new(LoggerFactory, null, FileSystem);
- ServiceWithConfig = new(LoggerFactory, ConfigurationContext, FileSystem);
+ Service = new(LoggerFactory, FileSystem);
+ ServiceWithConfig = new(LoggerFactory, FileSystem, ConfigurationContext);
_changelogDir = CreateChangelogDir();
}
@@ -3262,7 +3263,7 @@ public async Task BundleChangelogs_WithProfileMode_MissingConfig_ReturnsErrorWit
currentDirectory: "/empty-project"
);
cwdFs.Directory.CreateDirectory("/empty-project");
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystemFactory.ScopeCurrentWorkingDirectory(cwdFs));
+ var service = new ChangelogBundlingService(LoggerFactory, ChangelogFileSystem.FromWorkingDirectory(cwdFs), ConfigurationContext);
var input = new BundleChangelogsArguments
{
@@ -3323,7 +3324,7 @@ public async Task BundleChangelogs_WithProfileMode_ConfigAtCurrentDir_LoadsSucce
""";
await cwdFs.File.WriteAllTextAsync(Path.Join(root, "changelogs/1755268130-feature.yaml"), changelogContent, TestContext.Current.CancellationToken);
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystemFactory.ScopeCurrentWorkingDirectory(cwdFs));
+ var service = new ChangelogBundlingService(LoggerFactory, ChangelogFileSystem.FromWorkingDirectory(cwdFs), ConfigurationContext);
var input = new BundleChangelogsArguments
{
@@ -3384,7 +3385,7 @@ public async Task BundleChangelogs_WithProfileMode_ConfigAtDocsSubdir_LoadsSucce
""";
await cwdFs.File.WriteAllTextAsync(Path.Join(root, "changelogs/1755268130-feature.yaml"), changelogContent, TestContext.Current.CancellationToken);
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystemFactory.ScopeCurrentWorkingDirectory(cwdFs));
+ var service = new ChangelogBundlingService(LoggerFactory, ChangelogFileSystem.FromWorkingDirectory(cwdFs), ConfigurationContext);
var input = new BundleChangelogsArguments
{
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs
index 75f72a0b64..7ba1ab2e2e 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs
@@ -55,7 +55,7 @@ public class BundleFilesFilterTests : ChangelogTestBase
public BundleFilesFilterTests(ITestOutputHelper output) : base(output)
{
- ServiceWithConfig = new(LoggerFactory, ConfigurationContext, FileSystem);
+ ServiceWithConfig = new(LoggerFactory, FileSystem, ConfigurationContext);
_changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(_changelogDir);
}
@@ -281,7 +281,7 @@ public async Task Bundle_WithFilesAndForceLocal_SourcesLocalEvenWhenRepoResolves
var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.NotFound));
var fetcher = new CdnChangelogEntryFetcher(LoggerFactory, handler, sleep: (_, _) => Task.CompletedTask);
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystem, null, fetcher);
+ var service = new ChangelogBundlingService(LoggerFactory, FileSystem, ConfigurationContext, null, fetcher);
var output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml");
var input = new BundleChangelogsArguments
@@ -415,7 +415,7 @@ public async Task Bundle_WithProfile_PathListFile_RepoResolves_SourcesFromCdn()
private ChangelogBundlingService ServiceWithCdn(StubHandler handler)
{
var fetcher = new CdnChangelogEntryFetcher(LoggerFactory, handler, sleep: (_, _) => Task.CompletedTask);
- return new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystem, null, fetcher);
+ return new ChangelogBundlingService(LoggerFactory, FileSystem, ConfigurationContext, null, fetcher);
}
private async Task WriteRepoOnlyConfigAsync()
@@ -449,7 +449,7 @@ public async Task Bundle_WithForceLocal_SourcesLocalDespiteResolvableRepo()
var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.NotFound));
var fetcher = new CdnChangelogEntryFetcher(LoggerFactory, handler, sleep: (_, _) => Task.CompletedTask);
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystem, null, fetcher);
+ var service = new ChangelogBundlingService(LoggerFactory, FileSystem, ConfigurationContext, null, fetcher);
var output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml");
var input = new BundleChangelogsArguments
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs
index eae12dd31b..a3c2373a27 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs
@@ -116,7 +116,7 @@ private ChangelogBundlingService Service(
StubHandler handler,
IGitHubCommitRangeService rangeService,
IGitHubPrService? prService = null) =>
- new(LoggerFactory, ConfigurationContext, FileSystem, null, Fetcher(handler),
+ new(LoggerFactory, FileSystem, ConfigurationContext, null, Fetcher(handler),
prService ?? A.Fake(), rangeService);
[Fact]
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundlePlanTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundlePlanTests.cs
index 75070688ab..bab8772ab3 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/BundlePlanTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/BundlePlanTests.cs
@@ -15,7 +15,7 @@ public class BundlePlanTests : ChangelogTestBase
private ChangelogBundlingService Service { get; }
public BundlePlanTests(ITestOutputHelper output) : base(output) =>
- Service = new(LoggerFactory, ConfigurationContext, FileSystem);
+ Service = new(LoggerFactory, FileSystem, ConfigurationContext);
private async Task CreateConfigAsync(string configContent)
{
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleProfileGitHubReleaseTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleProfileGitHubReleaseTests.cs
index ebf213c17e..72347cffa2 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/BundleProfileGitHubReleaseTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleProfileGitHubReleaseTests.cs
@@ -23,7 +23,7 @@ public class BundleProfileGitHubReleaseTests : ChangelogTestBase
public BundleProfileGitHubReleaseTests(ITestOutputHelper output) : base(output)
{
_mockReleaseService = A.Fake();
- _service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystem, _mockReleaseService);
+ _service = new ChangelogBundlingService(LoggerFactory, FileSystem, ConfigurationContext, _mockReleaseService);
_changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(_changelogDir);
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs
index e130c3cc5e..557b5423b2 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs
@@ -26,7 +26,7 @@ public class BundleReleaseVersionTests : ChangelogTestBase
public BundleReleaseVersionTests(ITestOutputHelper output) : base(output)
{
- _bundlingService = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystem);
+ _bundlingService = new ChangelogBundlingService(LoggerFactory, FileSystem, ConfigurationContext);
_changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(_changelogDir);
}
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogRemoveTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogRemoveTests.cs
index 9ea8290e30..27ebb30c73 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogRemoveTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogRemoveTests.cs
@@ -6,6 +6,7 @@
using Elastic.Changelog.Bundling;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Changelog.Tests.Changelogs;
@@ -71,8 +72,8 @@ public class ChangelogRemoveTests : ChangelogTestBase
public ChangelogRemoveTests(ITestOutputHelper output) : base(output)
{
- Service = new ChangelogRemoveService(LoggerFactory, null, FileSystem);
- ServiceWithConfig = new ChangelogRemoveService(LoggerFactory, ConfigurationContext, FileSystem);
+ Service = new ChangelogRemoveService(LoggerFactory, FileSystem);
+ ServiceWithConfig = new ChangelogRemoveService(LoggerFactory, FileSystem, ConfigurationContext);
_changelogDir = CreateChangelogDir();
}
@@ -457,7 +458,7 @@ public async Task Remove_WithProfileMode_MissingConfig_ReturnsErrorWithAdvice()
currentDirectory: "/empty-project"
);
cwdFs.Directory.CreateDirectory("/empty-project");
- var service = new ChangelogRemoveService(LoggerFactory, ConfigurationContext, FileSystemFactory.ScopeCurrentWorkingDirectory(cwdFs));
+ var service = new ChangelogRemoveService(LoggerFactory, ChangelogFileSystem.FromWorkingDirectory(cwdFs), ConfigurationContext);
var input = new ChangelogRemoveArguments
{
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs
index 4a3254c947..94d406e1e2 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs
@@ -11,16 +11,17 @@
using Elastic.Documentation.Configuration.Products;
using Elastic.Documentation.Configuration.Search;
using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Versions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Tests.Changelogs;
public abstract class ChangelogTestBase : IDisposable
{
- protected ScopedFileSystem FileSystem { get; }
+ protected ChangelogFileSystem FileSystem { get; }
+ protected RunnerTempFileSystem RunnerTempFileSystem { get; }
protected IConfigurationContext ConfigurationContext { get; }
protected TestDiagnosticsCollector Collector { get; }
protected ILoggerFactory LoggerFactory { get; }
@@ -30,7 +31,14 @@ protected ChangelogTestBase(ITestOutputHelper output)
{
Output = output;
var mockFileSystem = new MockFileSystem(new MockFileSystemOptions { CurrentDirectory = Paths.WorkingDirectoryRoot.FullName });
- FileSystem = FileSystemFactory.ScopeCurrentWorkingDirectory(mockFileSystem);
+ FileSystem = ChangelogFileSystem.FromWorkingDirectory(mockFileSystem);
+ RunnerTempFileSystem = new RunnerTempFileSystem(
+ mockFileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName),
+ inner: mockFileSystem);
+ // ConfigurationFileProvider writes to AppData/config-runtime, which is outside ChangelogFileSystem's
+ // git-root scope by design. Use a CheckoutsFileSystem (includes AppData) for the config provider only;
+ // it wraps the same mock so both filesystems share in-memory state.
+ var configFileSystem = CheckoutsFileSystem.FromWorkingDirectory(mockFileSystem);
Collector = new TestDiagnosticsCollector(output);
LoggerFactory = new TestLoggerFactory(output);
@@ -104,7 +112,7 @@ protected ChangelogTestBase(ITestOutputHelper output)
{
Elasticsearch = ElasticsearchEndpoint.Default,
},
- ConfigurationFileProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, FileSystem),
+ ConfigurationFileProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, configFileSystem),
VersionsConfiguration = versionsConfiguration,
ProductsConfiguration = productsConfiguration,
SearchConfiguration = new SearchConfiguration { Synonyms = [], Rules = [], DiminishTerms = [] },
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs
index a31bc4b37c..32294e20b7 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs
@@ -133,7 +133,7 @@ public async Task MonthlyProfile_SourcesFromRepoPool_ProducesSoundBundle()
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
var handler = RepoPoolHandler();
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystem, null, Fetcher(handler));
+ var service = new ChangelogBundlingService(LoggerFactory, FileSystem, ConfigurationContext, null, Fetcher(handler));
var input = new BundleChangelogsArguments
{
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs
index 1c485eedfb..b679f379df 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs
@@ -15,7 +15,7 @@ public abstract class CreateChangelogTestBase(ITestOutputHelper output) : Change
protected IGitHubPrService MockGitHubService { get; } = A.Fake();
protected ChangelogCreationService CreateService(IEnvironmentVariables? env = null) =>
- new(LoggerFactory, ConfigurationContext, MockGitHubService, FileSystem, env);
+ new(LoggerFactory, ConfigurationContext, FileSystem, MockGitHubService, env);
protected async Task CreateConfigDirectory(string configContent)
{
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseVersionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseVersionTests.cs
index aa50df84f7..6d144f0bf1 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseVersionTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseVersionTests.cs
@@ -21,7 +21,7 @@ public class ReleaseVersionTests(ITestOutputHelper output) : ChangelogTestBase(o
private readonly IGitHubPrService _mockPrService = A.Fake();
private GitHubReleaseChangelogService CreateService() =>
- new(LoggerFactory, ConfigurationContext, _mockReleaseService, _mockPrService, FileSystem);
+ new(LoggerFactory, ConfigurationContext, FileSystem, _mockReleaseService, _mockPrService);
private string CreateOutputDirectory() =>
FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/RemoveReleaseVersionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/RemoveReleaseVersionTests.cs
index f64aeebf19..f0dcdefd2d 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/RemoveReleaseVersionTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/RemoveReleaseVersionTests.cs
@@ -25,7 +25,7 @@ public class RemoveReleaseVersionTests : ChangelogTestBase
public RemoveReleaseVersionTests(ITestOutputHelper output) : base(output)
{
- _removeService = new ChangelogRemoveService(LoggerFactory, ConfigurationContext, FileSystem);
+ _removeService = new ChangelogRemoveService(LoggerFactory, FileSystem, ConfigurationContext);
_changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(_changelogDir);
}
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/RenderChangelogTestBase.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/RenderChangelogTestBase.cs
index 684b0d9a92..4d9b4a93b6 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/Render/RenderChangelogTestBase.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/RenderChangelogTestBase.cs
@@ -11,5 +11,5 @@ public abstract class RenderChangelogTestBase : ChangelogTestBase
protected ChangelogRenderingService Service { get; }
protected RenderChangelogTestBase(ITestOutputHelper output) : base(output) =>
- Service = new ChangelogRenderingService(LoggerFactory, ConfigurationContext, FileSystem);
+ Service = new ChangelogRenderingService(LoggerFactory, FileSystem, ConfigurationContext);
}
diff --git a/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs b/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs
index ab38e30eb0..e207d26e98 100644
--- a/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs
+++ b/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs
@@ -45,7 +45,7 @@ private static IEnvironmentVariables FakeLocalEnv()
}
private ChangelogCreationService CreateServiceWithEnv(IEnvironmentVariables env) =>
- new(LoggerFactory, ConfigurationContext, env: env);
+ new(LoggerFactory, ConfigurationContext, FileSystem, env: env);
[Fact]
public void EnrichFromCI_NotInCI_ReturnsUnchanged()
@@ -167,7 +167,7 @@ public void EnrichFromCI_InCI_TitleOnly_EnrichesWithoutPr()
[Fact]
public void EnrichFromCI_NullEnv_ReturnsUnchanged()
{
- var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext);
+ var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext, FileSystem);
var input = DefaultInput() with { Title = "original" };
var result = service.EnrichFromCI(input);
diff --git a/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs b/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs
index 60480bbc4f..043d536f0f 100644
--- a/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs
+++ b/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs
@@ -11,6 +11,7 @@
using Elastic.Changelog.Utilities;
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
using FakeItEasy;
namespace Elastic.Changelog.Tests.Creation;
@@ -83,7 +84,7 @@ public async Task CreateChangelog_CIWithProducts_SkipsPrFetchAndSucceeds()
products: "cloud-hosted, cloud-serverless"
);
- var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext, _mockGitHub, FileSystem, env);
+ var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext, FileSystem, _mockGitHub, env);
var input = new CreateChangelogArguments
{
Products = [],
@@ -126,7 +127,7 @@ public async Task CreateChangelog_CIWithoutProducts_FallsBackToPrFetchForProduct
repo: "cloud"
);
- var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext, _mockGitHub, FileSystem, env);
+ var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext, FileSystem, _mockGitHub, env);
var input = new CreateChangelogArguments
{
Products = [],
@@ -169,7 +170,7 @@ public async Task CreateChangelog_CIWithoutProducts_NoPrProductLabels_FailsWithP
repo: "cloud"
);
- var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext, _mockGitHub, FileSystem, env);
+ var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext, FileSystem, _mockGitHub, env);
var input = new CreateChangelogArguments
{
Products = [],
@@ -189,24 +190,16 @@ public async Task CreateChangelog_CIWithoutProducts_NoPrProductLabels_FailsWithP
}
///
- /// When --output points to a temp directory (e.g. /tmp/changelog-staging in CI),
- /// the service must use a write-scoped filesystem that allows temp paths.
- /// Regression test for ScopedFileSystemException on temp output.
+ /// When --output points to a subdirectory of the repo root, the service writes changelog files there.
///
[Fact]
- public async Task CreateChangelog_TempOutputDirectory_Succeeds()
+ public async Task CreateChangelog_OutputSubdirectory_Succeeds()
{
- var mockFs = new MockFileSystem(new MockFileSystemOptions { CurrentDirectory = Paths.WorkingDirectoryRoot.FullName });
- var writeFs = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(mockFs);
-
var configPath = Path.Join(Paths.WorkingDirectoryRoot.FullName, "config", "changelog.yml");
- writeFs.Directory.CreateDirectory(writeFs.Path.GetDirectoryName(configPath)!);
- await writeFs.File.WriteAllTextAsync(configPath, ConfigWithProductLabels, TestContext.Current.CancellationToken);
+ FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
+ await FileSystem.File.WriteAllTextAsync(configPath, ConfigWithProductLabels, TestContext.Current.CancellationToken);
- // Use the real system temp path so AllowedSpecialFolder.Temp matches cross-platform.
- // MockFileSystem's GetTempPath() returns a hardcoded "C:\temp" that diverges from the
- // real temp on Windows CI (D:\Temp), causing scope validation to fail.
- var tempOutput = Path.Join(Path.GetTempPath(), "changelog-staging");
+ var output = Path.Join(Paths.WorkingDirectoryRoot.FullName, "changelog-staging");
var env = FakeCIEnv(
prNumber: "1044",
@@ -217,12 +210,12 @@ public async Task CreateChangelog_TempOutputDirectory_Succeeds()
products: "elasticsearch"
);
- var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext, _mockGitHub, writeFs, env);
+ var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext, FileSystem, _mockGitHub, env);
var input = new CreateChangelogArguments
{
Products = [],
Config = configPath,
- Output = tempOutput,
+ Output = output,
Concise = true
};
@@ -230,8 +223,8 @@ public async Task CreateChangelog_TempOutputDirectory_Succeeds()
result.Should().BeTrue();
Collector.Errors.Should().Be(0);
- writeFs.Directory.Exists(tempOutput).Should().BeTrue();
- writeFs.Directory.GetFiles(tempOutput, "*.yaml").Should().NotBeEmpty();
+ FileSystem.Directory.Exists(output).Should().BeTrue();
+ FileSystem.Directory.GetFiles(output, "*.yaml").Should().NotBeEmpty();
}
[Fact]
@@ -241,7 +234,7 @@ public async Task CreateChangelog_OutputDoesNotContainBom()
var tempOutput = Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(tempOutput);
- var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext, _mockGitHub, FileSystem, null);
+ var service = new ChangelogCreationService(LoggerFactory, ConfigurationContext, FileSystem, _mockGitHub, null);
var input = new CreateChangelogArguments
{
Title = "Test BOM handling",
diff --git a/tests/Elastic.Changelog.Tests/Evaluation/ChangelogArtifactEvaluationServiceTests.cs b/tests/Elastic.Changelog.Tests/Evaluation/ChangelogArtifactEvaluationServiceTests.cs
index 7d662e4c8c..6148df8634 100644
--- a/tests/Elastic.Changelog.Tests/Evaluation/ChangelogArtifactEvaluationServiceTests.cs
+++ b/tests/Elastic.Changelog.Tests/Evaluation/ChangelogArtifactEvaluationServiceTests.cs
@@ -24,7 +24,7 @@ public class ChangelogArtifactEvaluationServiceTests(ITestOutputHelper output) :
private static readonly string MetadataFilePath = Path.Join(Root, "artifact/metadata.json");
private ChangelogArtifactEvaluationService CreateService() =>
- new(LoggerFactory, _mockGitHub, _mockCore, FileSystem);
+ new(LoggerFactory, _mockGitHub, _mockCore, RunnerTempFileSystem);
private static EvaluateArtifactArguments DefaultArgs() =>
new()
diff --git a/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrBodyReaderTests.cs b/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrBodyReaderTests.cs
index 3c8c29e9c9..32043275d0 100644
--- a/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrBodyReaderTests.cs
+++ b/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrBodyReaderTests.cs
@@ -7,6 +7,7 @@
using Elastic.Changelog.Evaluation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Changelog.Tests.Evaluation;
@@ -19,8 +20,9 @@ public async Task ReadAsync_PrBodyFileUnderWorkingDir_ReadsBody()
var mockFs = CreateMockFileSystem();
mockFs.AddFile(bodyPath, new MockFileData("Release Notes: adds billing metadata"));
var collector = new TestDiagnosticsCollector(output);
+ var fs = CreateRunnerTempFs(mockFs);
- var result = await ChangelogPrBodyReader.ReadAsync(bodyPath, collector, mockFs, TestContext.Current.CancellationToken);
+ var result = await ChangelogPrBodyReader.ReadAsync(bodyPath, collector, fs, TestContext.Current.CancellationToken);
result.Should().Be("Release Notes: adds billing metadata");
collector.Diagnostics.Should().BeEmpty();
@@ -30,7 +32,8 @@ public async Task ReadAsync_PrBodyFileUnderWorkingDir_ReadsBody()
public async Task ReadAsync_PrBodyFileMissing_EmitsWarning()
{
var bodyPath = Path.Join(Paths.WorkingDirectoryRoot.FullName, "missing-pr-body.md");
- var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(CreateMockFileSystem());
+ var mockFs = CreateMockFileSystem();
+ var scopedFs = CreateRunnerTempFs(mockFs);
var collector = new TestDiagnosticsCollector(output);
var result = await ChangelogPrBodyReader.ReadAsync(bodyPath, collector, scopedFs, TestContext.Current.CancellationToken);
@@ -48,7 +51,8 @@ public async Task ReadAsync_PrBodyFileOutsideScope_EmitsWarning()
var bodyPath = Path.Join(runnerTemp, "changelog-pr-body.md");
var mockFs = new MockFileSystem(new MockFileSystemOptions { CurrentDirectory = Paths.WorkingDirectoryRoot.FullName });
mockFs.AddFile(bodyPath, new MockFileData("Release Notes: something important"));
- var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(mockFs);
+ // Deliberately do NOT add runnerTemp to ciPaths — file should be outside scope
+ var scopedFs = CreateRunnerTempFs(mockFs);
var collector = new TestDiagnosticsCollector(output);
var result = await ChangelogPrBodyReader.ReadAsync(bodyPath, collector, scopedFs, TestContext.Current.CancellationToken);
@@ -67,8 +71,9 @@ public async Task ReadAsync_PrBodyFileExceedsMaxSize_TruncatesAndHints()
var mockFs = CreateMockFileSystem();
mockFs.AddFile(bodyPath, new MockFileData(largeContent));
var collector = new TestDiagnosticsCollector(output);
+ var fs = CreateRunnerTempFs(mockFs);
- var result = await ChangelogPrBodyReader.ReadAsync(bodyPath, collector, mockFs, TestContext.Current.CancellationToken);
+ var result = await ChangelogPrBodyReader.ReadAsync(bodyPath, collector, fs, TestContext.Current.CancellationToken);
result.Should().NotBeNull();
result.Length.Should().Be(ChangelogPrBodyReader.MaxPrBodyFileBytes);
@@ -77,4 +82,7 @@ public async Task ReadAsync_PrBodyFileExceedsMaxSize_TruncatesAndHints()
private static MockFileSystem CreateMockFileSystem() =>
new(new MockFileSystemOptions { CurrentDirectory = Paths.WorkingDirectoryRoot.FullName });
+
+ private static RunnerTempFileSystem CreateRunnerTempFs(MockFileSystem inner) =>
+ new(inner.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), inner: inner);
}
diff --git a/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrEvaluationServiceTests.cs b/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrEvaluationServiceTests.cs
index 1419ddf75e..da29e64f33 100644
--- a/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrEvaluationServiceTests.cs
+++ b/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrEvaluationServiceTests.cs
@@ -67,7 +67,7 @@ public ChangelogPrEvaluationServiceTests(ITestOutputHelper output) : base(output
}
private ChangelogPrEvaluationService CreateService() =>
- new(LoggerFactory, ConfigurationContext, _mockGitHub, _mockCore, FileSystem);
+ new(LoggerFactory, ConfigurationContext, _mockGitHub, _mockCore, RunnerTempFileSystem);
private EvaluatePrArguments DefaultArgs(
string eventAction = "opened",
diff --git a/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrepareArtifactServiceTests.cs b/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrepareArtifactServiceTests.cs
index 464bcaf16c..bff6350b26 100644
--- a/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrepareArtifactServiceTests.cs
+++ b/tests/Elastic.Changelog.Tests/Evaluation/ChangelogPrepareArtifactServiceTests.cs
@@ -43,7 +43,7 @@ public class ChangelogPrepareArtifactServiceTests(ITestOutputHelper output) : Ch
""";
private ChangelogPrepareArtifactService CreateService() =>
- new(LoggerFactory, ConfigurationContext, _mockCore, FileSystem);
+ new(LoggerFactory, ConfigurationContext, _mockCore, RunnerTempFileSystem);
private PrepareArtifactArguments DefaultArgs(
string evaluateStatus = "proceed",
diff --git a/tests/Elastic.Changelog.Tests/Migration/WebMigrationServiceTests.cs b/tests/Elastic.Changelog.Tests/Migration/WebMigrationServiceTests.cs
index b6cf414155..ffce519f42 100644
--- a/tests/Elastic.Changelog.Tests/Migration/WebMigrationServiceTests.cs
+++ b/tests/Elastic.Changelog.Tests/Migration/WebMigrationServiceTests.cs
@@ -11,6 +11,7 @@
using AwesomeAssertions;
using Elastic.Changelog.Migration;
using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
using FakeItEasy;
using Microsoft.Extensions.Logging.Abstractions;
using Nullean.ScopedFileSystem;
@@ -36,7 +37,7 @@ public WebMigrationServiceTests(ITestOutputHelper output)
{
CurrentDirectory = Paths.WorkingDirectoryRoot.FullName
});
- _fileSystem = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(_mockFileSystem);
+ _fileSystem = CheckoutsFileSystem.FromWorkingDirectory(_mockFileSystem).Write;
_collector = new TestDiagnosticsCollector(output);
_httpHandler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
{
diff --git a/tests/Elastic.Changelog.Tests/Uploading/ChangelogUploadServiceTests.cs b/tests/Elastic.Changelog.Tests/Uploading/ChangelogUploadServiceTests.cs
index a4e5c1cef3..975b0ba88f 100644
--- a/tests/Elastic.Changelog.Tests/Uploading/ChangelogUploadServiceTests.cs
+++ b/tests/Elastic.Changelog.Tests/Uploading/ChangelogUploadServiceTests.cs
@@ -11,9 +11,9 @@
using Elastic.Changelog.Tests.Changelogs;
using Elastic.Changelog.Uploading;
using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
using FakeItEasy;
using Microsoft.Extensions.Logging.Abstractions;
-using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Tests.Uploading;
@@ -21,7 +21,7 @@ namespace Elastic.Changelog.Tests.Uploading;
public class ChangelogUploadServiceTests
{
private readonly MockFileSystem _mockFileSystem;
- private readonly ScopedFileSystem _fileSystem;
+ private readonly ChangelogFileSystem _fileSystem;
private readonly IAmazonS3 _s3Client = A.Fake();
private readonly ChangelogUploadService _service;
private readonly TestDiagnosticsCollector _collector;
@@ -33,7 +33,7 @@ public ChangelogUploadServiceTests(ITestOutputHelper output)
{
CurrentDirectory = Paths.WorkingDirectoryRoot.FullName
});
- _fileSystem = FileSystemFactory.ScopeCurrentWorkingDirectory(_mockFileSystem);
+ _fileSystem = ChangelogFileSystem.FromWorkingDirectory(_mockFileSystem);
_service = new ChangelogUploadService(NullLoggerFactory.Instance, fileSystem: _fileSystem, s3Client: _s3Client);
_collector = new TestDiagnosticsCollector(output);
_changelogDir = _mockFileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog");
diff --git a/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs b/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs
index 804502ebca..b9a7f84465 100644
--- a/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs
+++ b/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs
@@ -15,6 +15,7 @@
using Elastic.Documentation.Configuration.Search;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Versions;
using FakeItEasy;
using Microsoft.Extensions.Logging;
@@ -71,7 +72,7 @@ public static IConfigurationContext CreateConfigurationContext(
{
Elasticsearch = ElasticsearchEndpoint.Default,
},
- ConfigurationFileProvider = new ConfigurationFileProvider(new TestLoggerFactory(null), fileSystem),
+ ConfigurationFileProvider = new ConfigurationFileProvider(new TestLoggerFactory(null), new ConfigurationFileSystem(fileSystem)),
VersionsConfiguration = versionsConfiguration,
ProductsConfiguration = productsConfiguration,
SearchConfiguration = search,
diff --git a/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs
index cb4f942077..14bb6a8923 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs
@@ -11,8 +11,8 @@
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.Logging.Abstractions;
-using Nullean.ScopedFileSystem;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
@@ -639,8 +639,10 @@ private sealed class MockDocumentationSetContext(
: IDocumentationSetContext
{
public IDiagnosticsCollector Collector => collector;
- public ScopedFileSystem ReadFileSystem => WriteFileSystem;
- public ScopedFileSystem WriteFileSystem { get; } = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fileSystem);
+ public IDocumentationFileSystem ReadFileSystem { get; } = DocumentationFileSystem.Resolve(
+ documentationSourceDirectory,
+ new DocumentationScopeOptions { Inner = fileSystem, ConfigurationFile = configurationPath.FullName });
+ public DocumentationWriteFileSystem WriteFileSystem { get; } = new(fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), inner: fileSystem);
public IDirectoryInfo OutputDirectory => fileSystem.DirectoryInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts"));
public IFileInfo ConfigurationPath => configurationPath;
public BuildType BuildType => BuildType.Isolated;
diff --git a/tests/Elastic.Documentation.Configuration.Tests/FileSystemFactoryTests.cs b/tests/Elastic.Documentation.Configuration.Tests/CheckoutsFileSystemTests.cs
similarity index 55%
rename from tests/Elastic.Documentation.Configuration.Tests/FileSystemFactoryTests.cs
rename to tests/Elastic.Documentation.Configuration.Tests/CheckoutsFileSystemTests.cs
index bcef22ddf7..a3da9685d6 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/FileSystemFactoryTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/CheckoutsFileSystemTests.cs
@@ -5,20 +5,14 @@
using System.IO.Abstractions.TestingHelpers;
using AwesomeAssertions;
using Elastic.Documentation;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Documentation.Configuration.Tests;
-sealed file class StubEnv(string? runnerTemp) : IEnvironmentVariables
-{
- public string? GetEnvironmentVariable(string name) =>
- name == "RUNNER_TEMP" ? runnerTemp : null;
- public bool IsRunningOnCI => runnerTemp is not null;
-}
-
-public class FileSystemFactoryTests
+public class CheckoutsFileSystemTests
{
[Fact]
- public void ScopeCurrentWorkingDirectory_NestedExtensionRoot_DoesNotThrow()
+ public void NestedExtensionRoot_DoesNotThrow()
{
var workingRoot = Paths.WorkingDirectoryRoot.FullName;
var nestedConfigDir = Path.Join(workingRoot, "environments", "internal");
@@ -28,16 +22,17 @@ public void ScopeCurrentWorkingDirectory_NestedExtensionRoot_DoesNotThrow()
{ configPath, new MockFileData("environment: internal") }
});
- var act = () => FileSystemFactory.ScopeCurrentWorkingDirectory(mockFs, [nestedConfigDir]);
+ var act = () => new CheckoutsFileSystem(mockFs.DirectoryInfo.New(workingRoot), inner: mockFs, extraRoots: [nestedConfigDir]);
act.Should().NotThrow();
- var scoped = FileSystemFactory.ScopeCurrentWorkingDirectory(mockFs, [nestedConfigDir]);
+ var scoped = act();
scoped.File.Exists(configPath).Should().BeTrue();
}
[Fact]
- public void ScopeCurrentWorkingDirectory_ExternalExtensionRoot_AllowsReadingExternalConfig()
+ public void ExternalExtensionRoot_AllowsReadingExternalConfig()
{
+ var workingRoot = Paths.WorkingDirectoryRoot.FullName;
var externalRoot = Path.Join(Path.GetTempPath(), $"external-codex-{Guid.NewGuid():N}");
var configPath = Path.Join(externalRoot, "codex.yml");
var mockFs = new MockFileSystem(new Dictionary
@@ -45,13 +40,13 @@ public void ScopeCurrentWorkingDirectory_ExternalExtensionRoot_AllowsReadingExte
{ configPath, new MockFileData("environment: internal") }
});
- var scoped = FileSystemFactory.ScopeCurrentWorkingDirectory(mockFs, [externalRoot]);
+ var scoped = new CheckoutsFileSystem(mockFs.DirectoryInfo.New(workingRoot), inner: mockFs, extraRoots: [externalRoot]);
scoped.File.Exists(configPath).Should().BeTrue();
}
[Fact]
- public void ScopeCurrentWorkingDirectory_AncestorExtensionRoot_DoesNotThrow()
+ public void AncestorExtensionRoot_DoesNotThrow()
{
// An ancestor of the working root would produce overlapping roots, the same class of
// crash fixed in a96ef869 / 3c5f9703 for Codex nested paths.
@@ -59,45 +54,30 @@ public void ScopeCurrentWorkingDirectory_AncestorExtensionRoot_DoesNotThrow()
var ancestor = Path.GetDirectoryName(workingRoot)!;
var mockFs = new MockFileSystem(new MockFileSystemOptions { CurrentDirectory = workingRoot });
- var act = () => FileSystemFactory.ScopeCurrentWorkingDirectory(mockFs, [ancestor]);
+ var act = () => new CheckoutsFileSystem(mockFs.DirectoryInfo.New(workingRoot), inner: mockFs, extraRoots: [ancestor]);
act.Should().NotThrow();
var scoped = act();
- // The scoped filesystem must still allow reads within the working root.
var fileInWorkingRoot = Path.Join(workingRoot, "some.yml");
mockFs.AddFile(fileInWorkingRoot, new MockFileData("test"));
scoped.File.Exists(fileInWorkingRoot).Should().BeTrue();
}
[Fact]
- public void RealReadForRunnerTemp_RunnerTempUnset_FallsBackToRealRead()
- {
- var env = new StubEnv(runnerTemp: null);
-
- var result = FileSystemFactory.RealReadForRunnerTemp(env);
-
- result.Should().BeSameAs(FileSystemFactory.RealRead);
- }
-
- [Fact]
- public void RealReadForRunnerTemp_RunnerTempSibling_AllowsReadingStagedFile()
+ public void ExtraRunnerTempRoot_AllowsReadingStagedFile()
{
- // Simulate the GitHub Actions hosted runner layout: RUNNER_TEMP and the
- // checkout root are sibling directories. A file staged by the action in
- // RUNNER_TEMP must be readable, which the plain RealRead scope denies.
+ // Simulate the GitHub Actions hosted runner layout: RUNNER_TEMP and the checkout root
+ // are sibling directories. A file staged in RUNNER_TEMP must be readable, which a
+ // plain working-dir scope denies.
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
- var env = new StubEnv(runnerTemp: tempDir);
var stagedFile = Path.Join(tempDir, "changelog-pr-body.md");
var mockFs = new MockFileSystem(new Dictionary
{
{ stagedFile, new MockFileData("Release Notes: fix memory leak") }
}, new MockFileSystemOptions { CurrentDirectory = Paths.WorkingDirectoryRoot.FullName });
- var scoped = FileSystemFactory.RealReadForRunnerTemp(env);
- // We call the overload that accepts inner FS, reusing the factory helper
- // that RealReadForRunnerTemp delegates to.
- var scopedMock = FileSystemFactory.ScopeCurrentWorkingDirectory(mockFs, [tempDir]);
+ var scoped = new CheckoutsFileSystem(mockFs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), inner: mockFs, extraRoots: [tempDir]);
- scopedMock.File.Exists(stagedFile).Should().BeTrue();
+ scoped.File.Exists(stagedFile).Should().BeTrue();
}
}
diff --git a/tests/Elastic.Documentation.Configuration.Tests/CiCheckoutLayoutTests.cs b/tests/Elastic.Documentation.Configuration.Tests/CiCheckoutLayoutTests.cs
new file mode 100644
index 0000000000..9acb0e0caf
--- /dev/null
+++ b/tests/Elastic.Documentation.Configuration.Tests/CiCheckoutLayoutTests.cs
@@ -0,0 +1,173 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions.TestingHelpers;
+using AwesomeAssertions;
+using Elastic.Documentation;
+using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
+
+namespace Elastic.Documentation.Configuration.Tests;
+
+///
+/// Regression tests for the GitHub Actions hosted-runner layout where each documentation
+/// set checkout lives inside the ApplicationData folder:
+/// ~/.local/share/elastic/docs-builder/checkouts/current/<repo>
+///
+/// Before the fix, all three filesystem types unconditionally added ApplicationData as a
+/// second scope root alongside the checkout. When checkout ⊂ AppData, the two roots form
+/// a parent–child pair and ValidateRootsAreDisjoint throws with "Scope roots must
+/// be disjoint". Now each type skips AppData when checkout is a sub-path of it (or vice
+/// versa).
+///
+///
+public class CiCheckoutLayoutTests
+{
+ ///
+ /// Constructs the simulated CI checkout path: a directory nested inside the real
+ /// ApplicationData folder, matching the layout the assembler uses on hosted runners.
+ ///
+ private static string CiCheckoutRoot =>
+ Path.Join(Paths.ApplicationData.FullName, "checkouts", "current", "apm-server");
+
+ // -----------------------------------------------------------------------
+ // CheckoutsFileSystem
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void CheckoutsFileSystem_CheckoutInsideAppData_DoesNotThrow()
+ {
+ var checkoutRoot = CiCheckoutRoot;
+ var mockFs = new MockFileSystem(new Dictionary
+ {
+ { Path.Join(checkoutRoot, "readme.md"), new MockFileData("hello") }
+ });
+
+ var act = () => new CheckoutsFileSystem(mockFs.DirectoryInfo.New(checkoutRoot), inner: mockFs);
+
+ act.Should().NotThrow();
+ }
+
+ [Fact]
+ public void CheckoutsFileSystem_CheckoutInsideAppData_ReadsFilesUnderCheckout()
+ {
+ var checkoutRoot = CiCheckoutRoot;
+ var filePath = Path.Join(checkoutRoot, "readme.md");
+ var mockFs = new MockFileSystem(new Dictionary
+ {
+ { filePath, new MockFileData("hello") }
+ });
+
+ var fs = new CheckoutsFileSystem(mockFs.DirectoryInfo.New(checkoutRoot), inner: mockFs);
+
+ fs.File.Exists(filePath).Should().BeTrue();
+ }
+
+ // -----------------------------------------------------------------------
+ // DocumentationWriteFileSystem
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void DocumentationWriteFileSystem_CheckoutInsideAppData_DoesNotThrow()
+ {
+ var checkoutRoot = CiCheckoutRoot;
+ var mockFs = new MockFileSystem();
+
+ var act = () => new DocumentationWriteFileSystem(
+ mockFs.DirectoryInfo.New(checkoutRoot),
+ inner: mockFs);
+
+ act.Should().NotThrow();
+ }
+
+ [Fact]
+ public void DocumentationWriteFileSystem_CheckoutInsideAppData_WritesFilesUnderCheckout()
+ {
+ var checkoutRoot = CiCheckoutRoot;
+ var outputPath = Path.Join(checkoutRoot, ".artifacts", "docs", "html");
+ var mockFs = new MockFileSystem();
+
+ var writeFs = new DocumentationWriteFileSystem(
+ mockFs.DirectoryInfo.New(checkoutRoot),
+ inner: mockFs);
+
+ var act = () => writeFs.Directory.CreateDirectory(outputPath);
+ act.Should().NotThrow();
+ }
+
+ // -----------------------------------------------------------------------
+ // DocumentationFileSystem (read + write via Resolve)
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void DocumentationFileSystem_Resolve_CheckoutInsideAppData_DoesNotThrow()
+ {
+ var checkoutRoot = CiCheckoutRoot;
+ var docsPath = Path.Join(checkoutRoot, "docs");
+ var mockFs = BuildDocsetFs(checkoutRoot, docsPath);
+
+ var act = () => DocumentationFileSystem.Resolve(
+ mockFs.DirectoryInfo.New(docsPath),
+ new DocumentationScopeOptions { Inner = mockFs });
+
+ act.Should().NotThrow();
+ }
+
+ [Fact]
+ public void DocumentationFileSystem_Resolve_CheckoutInsideAppData_CheckoutResolvedCorrectly()
+ {
+ var checkoutRoot = CiCheckoutRoot;
+ var docsPath = Path.Join(checkoutRoot, "docs");
+ var mockFs = BuildDocsetFs(checkoutRoot, docsPath);
+
+ var docFs = DocumentationFileSystem.Resolve(
+ mockFs.DirectoryInfo.New(docsPath),
+ new DocumentationScopeOptions { Inner = mockFs });
+
+ docFs.Paths.CheckoutDirectory.FullName.Should().Be(checkoutRoot);
+ }
+
+ [Fact]
+ public void DocumentationFileSystem_Resolve_CheckoutInsideAppData_WriteDoesNotThrow()
+ {
+ var checkoutRoot = CiCheckoutRoot;
+ var docsPath = Path.Join(checkoutRoot, "docs");
+ var mockFs = BuildDocsetFs(checkoutRoot, docsPath);
+
+ var act = () =>
+ {
+ var docFs = DocumentationFileSystem.Resolve(
+ mockFs.DirectoryInfo.New(docsPath),
+ new DocumentationScopeOptions { Inner = mockFs });
+ // accessing .Write must not throw either
+ _ = docFs.Write;
+ };
+
+ act.Should().NotThrow();
+ }
+
+ // -----------------------------------------------------------------------
+ // Helpers
+ // -----------------------------------------------------------------------
+
+ private static MockFileSystem BuildDocsetFs(string checkoutRoot, string docsPath)
+ {
+ var mockFs = new MockFileSystem();
+ mockFs.AddDirectory(Path.Join(checkoutRoot, ".git"));
+ mockFs.AddFile(Path.Join(checkoutRoot, ".git", "HEAD"),
+ new MockFileData("ref: refs/heads/main\n"));
+ mockFs.AddFile(Path.Join(checkoutRoot, ".git", "refs", "heads", "main"),
+ new MockFileData("abc1234\n"));
+ mockFs.AddFile(Path.Join(checkoutRoot, ".git", "config"),
+ new MockFileData("""
+ [remote "origin"]
+ url = https://github.com/elastic/apm-server.git
+ [branch "main"]
+ remote = origin
+ merge = refs/heads/main
+ """));
+ mockFs.AddFile(Path.Join(docsPath, "docset.yml"), new MockFileData("toc: []\n"));
+ return mockFs;
+ }
+}
diff --git a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs
index f3582b6e3e..1d92535ac1 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs
@@ -11,7 +11,7 @@
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Diagnostics;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Documentation.Configuration.Tests;
@@ -87,8 +87,9 @@ private sealed class MockDocumentationSetContext(
: IDocumentationSetContext
{
public IDiagnosticsCollector Collector => collector;
- public ScopedFileSystem ReadFileSystem => WriteFileSystem;
- public ScopedFileSystem WriteFileSystem { get; } = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fileSystem);
+ public IDocumentationFileSystem ReadFileSystem { get; } = DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName);
+ public DocumentationWriteFileSystem WriteFileSystem { get; } = new DocumentationWriteFileSystem(
+ fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), inner: fileSystem);
public IDirectoryInfo OutputDirectory => fileSystem.DirectoryInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts"));
public IFileInfo ConfigurationPath => configurationPath;
public BuildType BuildType => BuildType.Isolated;
diff --git a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileReleaseNotesTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileReleaseNotesTests.cs
index bb289aa76a..b541ccb920 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileReleaseNotesTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileReleaseNotesTests.cs
@@ -11,7 +11,7 @@
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Diagnostics;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Documentation.Configuration.Tests;
@@ -156,8 +156,9 @@ private sealed class MockDocumentationSetContext(
: IDocumentationSetContext
{
public IDiagnosticsCollector Collector => collector;
- public ScopedFileSystem ReadFileSystem => WriteFileSystem;
- public ScopedFileSystem WriteFileSystem { get; } = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fileSystem);
+ public IDocumentationFileSystem ReadFileSystem { get; } = DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName);
+ public DocumentationWriteFileSystem WriteFileSystem { get; } = new DocumentationWriteFileSystem(
+ fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), inner: fileSystem);
public IDirectoryInfo OutputDirectory => fileSystem.DirectoryInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts"));
public IFileInfo ConfigurationPath => configurationPath;
public BuildType BuildType => BuildType.Isolated;
diff --git a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileStorybookRegistryTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileStorybookRegistryTests.cs
index d86c7f1f90..af6bedef33 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileStorybookRegistryTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileStorybookRegistryTests.cs
@@ -11,7 +11,7 @@
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Diagnostics;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Documentation.Configuration.Tests;
@@ -105,8 +105,9 @@ private sealed class MockDocumentationSetContext(
: IDocumentationSetContext
{
public IDiagnosticsCollector Collector => collector;
- public ScopedFileSystem ReadFileSystem => WriteFileSystem;
- public ScopedFileSystem WriteFileSystem { get; } = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fileSystem);
+ public IDocumentationFileSystem ReadFileSystem { get; } = DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName);
+ public DocumentationWriteFileSystem WriteFileSystem { get; } = new DocumentationWriteFileSystem(
+ fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), inner: fileSystem);
public IDirectoryInfo OutputDirectory => fileSystem.DirectoryInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts"));
public IFileInfo ConfigurationPath => configurationPath;
public BuildType BuildType => BuildType.Isolated;
diff --git a/tests/Elastic.Documentation.Configuration.Tests/CreateNavigationFileTests.cs b/tests/Elastic.Documentation.Configuration.Tests/CreateNavigationFileTests.cs
index b54ec47e48..b2bf2f89e6 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/CreateNavigationFileTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/CreateNavigationFileTests.cs
@@ -5,6 +5,7 @@
using System.IO.Abstractions.TestingHelpers;
using AwesomeAssertions;
using Elastic.Documentation.Configuration.Assembler;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.Logging.Abstractions;
namespace Elastic.Documentation.Configuration.Tests;
@@ -12,7 +13,7 @@ namespace Elastic.Documentation.Configuration.Tests;
public class CreateNavigationFileTests
{
private static ConfigurationFileProvider CreateProvider(MockFileSystem fileSystem) =>
- new(NullLoggerFactory.Instance, fileSystem, skipPrivateRepositories: true, ConfigurationSource.Embedded);
+ new(NullLoggerFactory.Instance, new ConfigurationFileSystem(fileSystem), skipPrivateRepositories: true, ConfigurationSource.Embedded);
private static AssemblyConfiguration CreateConfig(params string[] privateRepoNames)
{
diff --git a/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs b/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs
index b259fdff5a..34bfb84791 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs
@@ -11,7 +11,7 @@
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Diagnostics;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Documentation.Configuration.Tests;
@@ -133,8 +133,9 @@ private sealed class MockDocumentationSetContext(
: IDocumentationSetContext
{
public IDiagnosticsCollector Collector => collector;
- public ScopedFileSystem ReadFileSystem => WriteFileSystem;
- public ScopedFileSystem WriteFileSystem { get; } = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fileSystem);
+ public IDocumentationFileSystem ReadFileSystem { get; } = DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName);
+ public DocumentationWriteFileSystem WriteFileSystem { get; } = new DocumentationWriteFileSystem(
+ fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), inner: fileSystem);
public IDirectoryInfo OutputDirectory => fileSystem.DirectoryInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts"));
public IFileInfo ConfigurationPath => configurationPath;
public BuildType BuildType => BuildType.Isolated;
diff --git a/tests/Elastic.Documentation.Configuration.Tests/DocumentationPathsResolverTests.cs b/tests/Elastic.Documentation.Configuration.Tests/DocumentationPathsResolverTests.cs
new file mode 100644
index 0000000000..1f67f78ff1
--- /dev/null
+++ b/tests/Elastic.Documentation.Configuration.Tests/DocumentationPathsResolverTests.cs
@@ -0,0 +1,501 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions.TestingHelpers;
+using AwesomeAssertions;
+using Elastic.Documentation;
+using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
+
+namespace Elastic.Documentation.Configuration.Tests;
+
+///
+/// Tests for and
+/// , covering the six-step bootstrap:
+/// invocation → docset anchor → checkout → git directories → git info → output.
+///
+public class DocumentationPathsResolverTests
+{
+ // -----------------------------------------------------------------------
+ // Helpers
+ // -----------------------------------------------------------------------
+
+ ///
+ /// Normalises a Unix-style path through the mock filesystem so that assertions work on
+ /// Windows, where MockFileSystem converts /repo → C:\repo.
+ ///
+ private static string P(MockFileSystem fs, string unixPath) =>
+ fs.DirectoryInfo.New(unixPath).FullName;
+
+ ///
+ /// Builds a minimal regular-repo filesystem:
+ /// /repo/.git/{HEAD,config,refs/...} +
+ /// /repo/docs/docset.yml
+ ///
+ private static MockFileSystem RegularRepo(
+ string repoRoot = "/repo",
+ string docsRelative = "docs",
+ string branch = "main",
+ string sha = "abc1234",
+ string remote = "elastic/test-repo")
+ {
+ var fs = new MockFileSystem();
+ var docsPath = $"{repoRoot}/{docsRelative}";
+ fs.AddDirectory($"{repoRoot}/.git");
+ fs.AddFile($"{repoRoot}/.git/HEAD", new MockFileData($"ref: refs/heads/{branch}\n"));
+ fs.AddFile($"{repoRoot}/.git/refs/heads/{branch}", new MockFileData($"{sha}\n"));
+ fs.AddFile($"{repoRoot}/.git/config", new MockFileData($"""
+ [remote "origin"]
+ url = https://github.com/{remote}.git
+ [branch "{branch}"]
+ remote = origin
+ merge = refs/heads/{branch}
+ """));
+ fs.AddFile($"{docsPath}/docset.yml", new MockFileData("toc: []\n"));
+ return fs;
+ }
+
+ ///
+ /// Builds a worktree filesystem:
+ /// /worktree/.git (file) → /main/.git/worktrees/wt → commondir → /main/.git
+ ///
+ private static MockFileSystem WorktreeWithCommondir(
+ string worktreeRoot = "/worktree",
+ string mainRoot = "/main",
+ string branch = "topic",
+ string sha = "fedcba987654",
+ string remote = "elastic/worktree-repo",
+ string docsRelative = "docs")
+ {
+ var fs = new MockFileSystem();
+ var worktreeGitDir = $"{mainRoot}/.git/worktrees/wt";
+ var mainGitDir = $"{mainRoot}/.git";
+ var docsPath = $"{worktreeRoot}/{docsRelative}";
+
+ // Worktree: .git is a pointer file
+ fs.AddFile($"{worktreeRoot}/.git", new MockFileData($"gitdir: {worktreeGitDir}\n"));
+
+ // Worktree-specific gitdir with commondir pointing back to the main .git
+ fs.AddDirectory(worktreeGitDir);
+ fs.AddFile($"{worktreeGitDir}/commondir", new MockFileData("../..\n")); // → /main/.git
+
+ // Main .git has the shared objects, config, and HEAD
+ fs.AddDirectory(mainGitDir);
+ fs.AddFile($"{mainGitDir}/HEAD", new MockFileData($"ref: refs/heads/{branch}\n"));
+ fs.AddFile($"{mainGitDir}/refs/heads/{branch}", new MockFileData($"{sha}\n"));
+ fs.AddFile($"{mainGitDir}/config", new MockFileData($"""
+ [remote "origin"]
+ url = https://github.com/{remote}.git
+ [branch "{branch}"]
+ remote = origin
+ merge = refs/heads/{branch}
+ """));
+
+ fs.AddFile($"{docsPath}/docset.yml", new MockFileData("toc: []\n"));
+ return fs;
+ }
+
+ // -----------------------------------------------------------------------
+ // Docset scan + checkout convergence
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void InvocationAtRepoRoot_ResolvesDocsetInDocsSubfolder()
+ {
+ var fs = RegularRepo();
+ var invocation = fs.DirectoryInfo.New("/repo");
+
+ var paths = DocumentationPathsResolver.Resolve(invocation, new DocumentationScopeOptions { Inner = fs }, fs);
+
+ paths.SourceDirectory.FullName.Should().Be(P(fs, "/repo/docs"));
+ paths.CheckoutDirectory.FullName.Should().Be(P(fs, "/repo"));
+ }
+
+ [Fact]
+ public void InvocationAtDocsSubfolder_ResolvesDocsetAndCheckout()
+ {
+ var fs = RegularRepo();
+ var invocation = fs.DirectoryInfo.New("/repo/docs");
+
+ var paths = DocumentationPathsResolver.Resolve(invocation, new DocumentationScopeOptions { Inner = fs }, fs);
+
+ paths.SourceDirectory.FullName.Should().Be(P(fs, "/repo/docs"));
+ paths.CheckoutDirectory.FullName.Should().Be(P(fs, "/repo"));
+ }
+
+ [Fact]
+ public void PathRepoRoot_And_PathDocsSubfolder_ResolveIdenticalCheckoutAndSource()
+ {
+ var fs = RegularRepo();
+
+ var fromRoot = DocumentationPathsResolver.Resolve(
+ fs.DirectoryInfo.New("/repo"),
+ new DocumentationScopeOptions { Inner = fs }, fs);
+
+ var fromDocs = DocumentationPathsResolver.Resolve(
+ fs.DirectoryInfo.New("/repo/docs"),
+ new DocumentationScopeOptions { Inner = fs }, fs);
+
+ fromRoot.CheckoutDirectory.FullName.Should().Be(fromDocs.CheckoutDirectory.FullName,
+ "--path /repo and --path /repo/docs must converge on the same checkout");
+ fromRoot.SourceDirectory.FullName.Should().Be(fromDocs.SourceDirectory.FullName,
+ "--path /repo and --path /repo/docs must converge on the same source");
+ }
+
+ [Fact]
+ public void AnchorTwoLevelsBelowInvocationRoot_StillResolvesCheckoutAtGitRoot()
+ {
+ // Mirrors the `elastic/infra` shape: no `--path` (invocation == repo root), and the only
+ // docset sits two levels down (`docs/resilience-team/docset.yml`). The recursive scan in
+ // step 2 finds it fine; without widening `MaxParents` by that same distance, step 3's
+ // default maxParents=1 can't see the `.git` two levels above the anchor and would either
+ // throw (real FS) or silently fall back to the wrong directory (mock FS leniency).
+ var fs = RegularRepo(docsRelative: "docs/resilience-team");
+ var invocation = fs.DirectoryInfo.New("/repo");
+
+ var paths = DocumentationPathsResolver.Resolve(invocation, new DocumentationScopeOptions { Inner = fs }, fs);
+
+ paths.SourceDirectory.FullName.Should().Be(P(fs, "/repo/docs/resilience-team"));
+ paths.CheckoutDirectory.FullName.Should().Be(P(fs, "/repo"),
+ "the anchor's depth below the invocation root should widen the git-root search, not require --git-dir");
+ paths.Git.IsAvailable.Should().BeTrue();
+ }
+
+ [Fact]
+ public void AnchorAtInvocationRoot_UnrelatedAncestorGit_StillOutOfReach()
+ {
+ // The depth-widening must stay anchored to the invocation, not become unbounded: when the
+ // anchor IS the invocation (depth 0), an unrelated repo's .git two levels up must remain
+ // out of reach, exactly as before this change.
+ var fs = new MockFileSystem();
+ fs.AddDirectory("/parent-repo/.git");
+ fs.AddDirectory("/parent-repo/checkout");
+ fs.AddFile("/parent-repo/checkout/docs/docset.yml", new MockFileData("toc: []\n"));
+
+ var invocation = fs.DirectoryInfo.New("/parent-repo/checkout/docs");
+ var opts = new DocumentationScopeOptions { Inner = fs, Git = GitCheckoutInformation.Unavailable };
+
+ var paths = DocumentationPathsResolver.Resolve(invocation, opts, fs);
+
+ paths.CheckoutDirectory.FullName.Should().Be(P(fs, "/parent-repo/checkout/docs"),
+ "depth-widening is relative to the invocation, so an ancestor repo's .git outside the invocation must not be adopted");
+ }
+
+ [Fact]
+ public void InvocationPath_StoredVerbatim_IndependentOfCheckout()
+ {
+ var fs = RegularRepo();
+ var invocationDir = fs.DirectoryInfo.New("/repo/docs");
+
+ var paths = DocumentationPathsResolver.Resolve(invocationDir, new DocumentationScopeOptions { Inner = fs }, fs);
+
+ paths.InvocationPath.FullName.Should().Be(P(fs, "/repo/docs"));
+ paths.CheckoutDirectory.FullName.Should().Be(P(fs, "/repo"));
+ }
+
+ // -----------------------------------------------------------------------
+ // Git directory resolution
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void RegularRepo_GitDirectories_ContainsOneEntry()
+ {
+ var fs = RegularRepo();
+
+ var paths = DocumentationPathsResolver.Resolve(
+ fs.DirectoryInfo.New("/repo"),
+ new DocumentationScopeOptions { Inner = fs }, fs);
+
+ paths.GitDirectories.Should().ContainSingle()
+ .Which.Should().Be(P(fs, "/repo/.git"));
+ }
+
+ [Fact]
+ public void RegularRepo_GitInfo_IsResolved()
+ {
+ var fs = RegularRepo(branch: "my-branch", sha: "deadbeef1234", remote: "elastic/my-repo");
+
+ var paths = DocumentationPathsResolver.Resolve(
+ fs.DirectoryInfo.New("/repo"),
+ new DocumentationScopeOptions { Inner = fs }, fs);
+
+ paths.Git.IsAvailable.Should().BeTrue();
+ paths.Git.Branch.Should().Be("my-branch");
+ paths.Git.Ref.Should().Be("deadbeef1234");
+ paths.Git.RepositoryName.Should().Be("my-repo");
+ }
+
+ // -----------------------------------------------------------------------
+ // Git worktree
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void Worktree_CheckoutIsWorktreeRoot_NotMainRepo()
+ {
+ var fs = WorktreeWithCommondir();
+
+ var paths = DocumentationPathsResolver.Resolve(
+ fs.DirectoryInfo.New("/worktree"),
+ new DocumentationScopeOptions { Inner = fs }, fs);
+
+ paths.CheckoutDirectory.FullName.Should().Be(P(fs, "/worktree"));
+ }
+
+ [Fact]
+ public void Worktree_GitDirectories_ContainsPointerAndMainGit()
+ {
+ var fs = WorktreeWithCommondir();
+
+ var paths = DocumentationPathsResolver.Resolve(
+ fs.DirectoryInfo.New("/worktree"),
+ new DocumentationScopeOptions { Inner = fs }, fs);
+
+ paths.GitDirectories.Should().HaveCount(2);
+ paths.GitDirectories.Should().Contain(P(fs, "/worktree/.git"),
+ "pointer file path must be in scope so the .git file is readable");
+ paths.GitDirectories.Should().Contain(P(fs, "/main/.git"),
+ "resolved commondir target must be included so config/HEAD are readable");
+ }
+
+ [Fact]
+ public void Worktree_GitInfo_ResolvedFromMainDotGit()
+ {
+ var fs = WorktreeWithCommondir(branch: "topic", sha: "fedcba987654", remote: "elastic/worktree-repo");
+
+ var paths = DocumentationPathsResolver.Resolve(
+ fs.DirectoryInfo.New("/worktree"),
+ new DocumentationScopeOptions { Inner = fs }, fs);
+
+ paths.Git.IsAvailable.Should().BeTrue();
+ paths.Git.Branch.Should().Be("topic");
+ paths.Git.Ref.Should().Be("fedcba987654");
+ paths.Git.RepositoryName.Should().Be("worktree-repo");
+ }
+
+ [Fact]
+ public void Worktree_InvocationAtDocsSubfolder_ResolvesIdenticallyToWorktreeRoot()
+ {
+ var fs = WorktreeWithCommondir();
+
+ var fromWorktreeRoot = DocumentationPathsResolver.Resolve(
+ fs.DirectoryInfo.New("/worktree"),
+ new DocumentationScopeOptions { Inner = fs }, fs);
+
+ var fromDocs = DocumentationPathsResolver.Resolve(
+ fs.DirectoryInfo.New("/worktree/docs"),
+ new DocumentationScopeOptions { Inner = fs }, fs);
+
+ fromWorktreeRoot.CheckoutDirectory.FullName.Should().Be(fromDocs.CheckoutDirectory.FullName,
+ "worktree: --path /worktree and --path /worktree/docs must resolve to the same checkout");
+ }
+
+ // -----------------------------------------------------------------------
+ // Explicit --git-dir override
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void ExplicitGitDir_CheckoutIsGitDirParent()
+ {
+ // Layout: docset at /project/docs/, .git at /repo/.git (out-of-tree)
+ var fs = new MockFileSystem();
+ fs.AddDirectory("/repo/.git");
+ fs.AddFile("/repo/.git/HEAD", new MockFileData("ref: refs/heads/main\n"));
+ fs.AddFile("/repo/.git/refs/heads/main", new MockFileData("cafe1234\n"));
+ fs.AddFile("/repo/.git/config", new MockFileData("""
+ [remote "origin"]
+ url = https://github.com/elastic/override-test.git
+ [branch "main"]
+ remote = origin
+ merge = refs/heads/main
+ """));
+ fs.AddFile("/project/docs/docset.yml", new MockFileData("toc: []\n"));
+
+ var opts = new DocumentationScopeOptions
+ {
+ Inner = fs,
+ GitDir = "/repo/.git"
+ };
+
+ var paths = DocumentationPathsResolver.Resolve(fs.DirectoryInfo.New("/project/docs"), opts, fs);
+
+ paths.CheckoutDirectory.FullName.Should().Be(P(fs, "/repo"),
+ "--git-dir /repo/.git → checkout = /repo/.git.Parent = /repo");
+ }
+
+ [Fact]
+ public void ExplicitGitDir_GitInfo_ResolvedFromOverriddenGitDir()
+ {
+ var fs = new MockFileSystem();
+ fs.AddDirectory("/repo/.git");
+ fs.AddFile("/repo/.git/HEAD", new MockFileData("ref: refs/heads/main\n"));
+ fs.AddFile("/repo/.git/refs/heads/main", new MockFileData("aabbcc99\n"));
+ fs.AddFile("/repo/.git/config", new MockFileData("""
+ [remote "origin"]
+ url = https://github.com/elastic/override-repo.git
+ [branch "main"]
+ remote = origin
+ merge = refs/heads/main
+ """));
+ fs.AddFile("/project/docs/docset.yml", new MockFileData("toc: []\n"));
+
+ var opts = new DocumentationScopeOptions
+ {
+ Inner = fs,
+ GitDir = "/repo/.git"
+ };
+
+ var paths = DocumentationPathsResolver.Resolve(fs.DirectoryInfo.New("/project/docs"), opts, fs);
+
+ paths.Git.IsAvailable.Should().BeTrue();
+ paths.Git.Branch.Should().Be("main");
+ paths.Git.Ref.Should().Be("aabbcc99");
+ paths.Git.RepositoryName.Should().Be("override-repo");
+ }
+
+ // -----------------------------------------------------------------------
+ // Mock filesystem — no git layout (graceful fallback)
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void MockFsWithoutGit_DoesNotThrow_CheckoutFallsBackToSource()
+ {
+ var fs = new MockFileSystem();
+ fs.AddFile("/repo/docs/docset.yml", new MockFileData("toc: []\n"));
+
+ var opts = new DocumentationScopeOptions { Inner = fs, Git = GitCheckoutInformation.Unavailable };
+ var paths = DocumentationPathsResolver.Resolve(fs.DirectoryInfo.New("/repo/docs"), opts, fs);
+
+ paths.CheckoutDirectory.FullName.Should().Be(P(fs, "/repo/docs"),
+ "mock FS fallback: no .git → checkout = source directory");
+ paths.GitDirectories.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void MockFsWithoutGit_GitOverride_IsPreservedVerbatim()
+ {
+ var fs = new MockFileSystem();
+ fs.AddFile("/repo/docs/docset.yml", new MockFileData("toc: []\n"));
+
+ var opts = new DocumentationScopeOptions { Inner = fs, Git = GitCheckoutInformation.Unavailable };
+ var paths = DocumentationPathsResolver.Resolve(fs.DirectoryInfo.New("/repo/docs"), opts, fs);
+
+ paths.Git.IsAvailable.Should().BeFalse();
+ paths.Git.Should().Be(GitCheckoutInformation.Unavailable);
+ }
+
+ // -----------------------------------------------------------------------
+ // Output directory default
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void Output_DefaultsToCheckoutArtifacts()
+ {
+ var fs = RegularRepo();
+ var opts = new DocumentationScopeOptions { Inner = fs };
+
+ var paths = DocumentationPathsResolver.Resolve(fs.DirectoryInfo.New("/repo"), opts, fs);
+
+ // Default output is checkout/.artifacts/docs/html — NOT the invocation path.
+ paths.OutputDirectory.FullName.Should().StartWith(P(fs, "/repo/.artifacts"));
+ }
+
+ [Fact]
+ public void Output_InvocationAtDocsSubfolder_StillAnchorsToCheckout()
+ {
+ // Regression: before this fix, --path /repo/docs/ wrote to /repo/docs/.artifacts
+ // instead of /repo/.artifacts. Checkout is /repo, so artifacts belong there.
+ var fs = RegularRepo();
+ var opts = new DocumentationScopeOptions { Inner = fs };
+
+ var fromRoot = DocumentationPathsResolver.Resolve(fs.DirectoryInfo.New("/repo"), opts, fs);
+ var fromDocs = DocumentationPathsResolver.Resolve(fs.DirectoryInfo.New("/repo/docs"), opts, fs);
+
+ fromRoot.OutputDirectory.FullName.Should().Be(fromDocs.OutputDirectory.FullName,
+ "--path /repo and --path /repo/docs must produce the same default output directory");
+ }
+
+ [Fact]
+ public void Output_ExplicitOverride_IsRespected()
+ {
+ var fs = RegularRepo();
+ var opts = new DocumentationScopeOptions
+ {
+ Inner = fs,
+ Output = "/custom/output"
+ };
+
+ var paths = DocumentationPathsResolver.Resolve(fs.DirectoryInfo.New("/repo"), opts, fs);
+
+ paths.OutputDirectory.FullName.Should().Be(P(fs, "/custom/output"));
+ }
+
+ // -----------------------------------------------------------------------
+ // ConfigurationFile option (pre-discovered docset)
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void PreDiscoveredConfigFile_SkipsDocsetScan()
+ {
+ // Docset is at /project/docs/docset.yml, but invocation is the project root
+ var fs = RegularRepo(repoRoot: "/project");
+ var docsetFile = fs.FileInfo.New("/project/docs/docset.yml");
+
+ var opts = new DocumentationScopeOptions { Inner = fs, ConfigurationFile = docsetFile.FullName };
+ var paths = DocumentationPathsResolver.Resolve(fs.DirectoryInfo.New("/project"), opts, fs);
+
+ paths.SourceDirectory.FullName.Should().Be(P(fs, "/project/docs"));
+ paths.ConfigurationPath.FullName.Should().Be(P(fs, "/project/docs/docset.yml"));
+ }
+
+ // -----------------------------------------------------------------------
+ // No docset found → throws
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void NoDocset_Throws_DocumentationPathException()
+ {
+ var fs = new MockFileSystem();
+ fs.AddDirectory("/empty");
+
+ var act = () => DocumentationPathsResolver.Resolve(
+ fs.DirectoryInfo.New("/empty"),
+ new DocumentationScopeOptions { Inner = fs, Git = GitCheckoutInformation.Unavailable },
+ fs);
+
+ act.Should().Throw()
+ .WithMessage("*docset.yml*");
+ }
+
+ // -----------------------------------------------------------------------
+ // DocumentationFileSystem.Resolve — integration (same scenarios via the public API)
+ // -----------------------------------------------------------------------
+
+ [Fact]
+ public void DocumentationFileSystem_Resolve_RegularRepo_ExposesResolvedPaths()
+ {
+ var fs = RegularRepo(branch: "feature", sha: "001122", remote: "elastic/docs-builder");
+ var invocation = fs.DirectoryInfo.New("/repo");
+
+ var docFs = DocumentationFileSystem.Resolve(invocation, new DocumentationScopeOptions { Inner = fs });
+
+ docFs.Paths.CheckoutDirectory.FullName.Should().Be(P(fs, "/repo"));
+ docFs.Paths.SourceDirectory.FullName.Should().Be(P(fs, "/repo/docs"));
+ docFs.Paths.Git.Branch.Should().Be("feature");
+ docFs.Paths.Git.RepositoryName.Should().Be("docs-builder");
+ }
+
+ [Fact]
+ public void DocumentationFileSystem_Resolve_Worktree_ExposesMainGitInfo()
+ {
+ var fs = WorktreeWithCommondir(branch: "topic", sha: "99aabb", remote: "elastic/worktree-test");
+ var invocation = fs.DirectoryInfo.New("/worktree");
+
+ var docFs = DocumentationFileSystem.Resolve(invocation, new DocumentationScopeOptions { Inner = fs });
+
+ docFs.Paths.CheckoutDirectory.FullName.Should().Be(P(fs, "/worktree"));
+ docFs.Paths.Git.Branch.Should().Be("topic");
+ docFs.Paths.Git.Ref.Should().Be("99aabb");
+ docFs.Paths.Git.RepositoryName.Should().Be("worktree-test");
+ }
+}
diff --git a/tests/Elastic.Documentation.Configuration.Tests/FindGitRootTests.cs b/tests/Elastic.Documentation.Configuration.Tests/FindGitRootTests.cs
index 65ec14db9a..63b4d586f9 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/FindGitRootTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/FindGitRootTests.cs
@@ -17,12 +17,11 @@ public void DocsAtRoot_FindsGitRoot()
fs.AddFile("/repo/docset.yml", new("toc: []"));
var start = fs.DirectoryInfo.New("/repo");
- var expected = start.FullName;
- var result = Paths.FindGitRoot(start, ceiling: start);
+ var result = Paths.FindGitRoot(start);
result.Should().NotBeNull();
- result.FullName.Should().Be(expected);
+ result.FullName.Should().Be(start.FullName);
}
[Fact]
@@ -32,13 +31,14 @@ public void DocsInDocsFolder_FindsGitRoot()
fs.AddDirectory("/repo/.git");
fs.AddFile("/repo/docs/docset.yml", new("toc: []"));
- var ceiling = fs.DirectoryInfo.New("/repo");
var start = fs.DirectoryInfo.New("/repo/docs");
+ var expected = fs.DirectoryInfo.New("/repo");
- var result = Paths.FindGitRoot(start, ceiling: ceiling);
+ // Default maxParents=1: docset is one level below checkout, so .git is found at depth 1
+ var result = Paths.FindGitRoot(start);
result.Should().NotBeNull();
- result.FullName.Should().Be(ceiling.FullName);
+ result.FullName.Should().Be(expected.FullName);
}
[Fact]
@@ -48,17 +48,18 @@ public void DocsNestedTwoLevels_FindsGitRoot()
fs.AddDirectory("/repo/.git");
fs.AddFile("/repo/docs/resilience-team/docset.yml", new("toc: []"));
- var ceiling = fs.DirectoryInfo.New("/repo");
var start = fs.DirectoryInfo.New("/repo/docs/resilience-team");
+ var expected = fs.DirectoryInfo.New("/repo");
- var result = Paths.FindGitRoot(start, ceiling: ceiling);
+ // maxParents=2: docset is two levels below checkout
+ var result = Paths.FindGitRoot(start, maxParents: 2);
result.Should().NotBeNull();
- result.FullName.Should().Be(ceiling.FullName);
+ result.FullName.Should().Be(expected.FullName);
}
[Fact]
- public void DocsNestedTwoLevels_WithoutCeiling_ReturnsNull()
+ public void DocsNestedTwoLevels_WithDefaultMaxParents_ReturnsNull()
{
var fs = new MockFileSystem();
fs.AddDirectory("/repo/.git");
@@ -66,58 +67,60 @@ public void DocsNestedTwoLevels_WithoutCeiling_ReturnsNull()
var start = fs.DirectoryInfo.New("/repo/docs/resilience-team");
+ // Default maxParents=1 cannot reach .git at depth 2
var result = Paths.FindGitRoot(start);
result.Should().BeNull();
}
[Fact]
- public void CeilingPreventsEscapingToParentRepo()
+ public void BoundPreventsEscapingToParentRepo()
{
var fs = new MockFileSystem();
fs.AddDirectory("/parent-repo/.git");
fs.AddDirectory("/parent-repo/checkout");
fs.AddFile("/parent-repo/checkout/docs/docset.yml", new("toc: []"));
- var ceiling = fs.DirectoryInfo.New("/parent-repo/checkout");
var start = fs.DirectoryInfo.New("/parent-repo/checkout/docs");
- var result = Paths.FindGitRoot(start, ceiling: ceiling);
+ // maxParents=1: checks checkout/docs (depth 0) and checkout (depth 1), neither has .git
+ var result = Paths.FindGitRoot(start);
- result.Should().BeNull("the .git is above the ceiling and must not be reached");
+ result.Should().BeNull("the .git is two levels above the anchor, beyond maxParents");
}
[Fact]
- public void CeilingPreventsEscapingToParentRepo_DeeplyNested()
+ public void BoundPreventsEscapingToParentRepo_DeeplyNested()
{
var fs = new MockFileSystem();
fs.AddDirectory("/workspace/projects/other-repo/.git");
fs.AddDirectory("/workspace/projects/other-repo/subrepo/docs/team");
fs.AddFile("/workspace/projects/other-repo/subrepo/docs/team/docset.yml", new("toc: []"));
- var ceiling = fs.DirectoryInfo.New("/workspace/projects/other-repo/subrepo");
var start = fs.DirectoryInfo.New("/workspace/projects/other-repo/subrepo/docs/team");
- var result = Paths.FindGitRoot(start, ceiling: ceiling);
+ // Default maxParents=1: cannot reach .git three levels up
+ var result = Paths.FindGitRoot(start);
- result.Should().BeNull("the .git belongs to a parent repo outside the ceiling");
+ result.Should().BeNull("the .git belongs to a parent repo outside the allowed depth");
}
[Fact]
- public void GitRootInsideCeiling_IsAccepted()
+ public void GitRootDeepAboveAnchor_AcceptedWithLargeMaxParents()
{
var fs = new MockFileSystem();
fs.AddDirectory("/workspace/.git");
fs.AddDirectory("/workspace/docs/a/b/c");
fs.AddFile("/workspace/docs/a/b/c/docset.yml", new("toc: []"));
- var ceiling = fs.DirectoryInfo.New("/workspace");
var start = fs.DirectoryInfo.New("/workspace/docs/a/b/c");
+ var expected = fs.DirectoryInfo.New("/workspace");
- var result = Paths.FindGitRoot(start, ceiling: ceiling);
+ // Must pass maxParents=4 since the anchor is 4 levels below the checkout
+ var result = Paths.FindGitRoot(start, maxParents: 4);
result.Should().NotBeNull();
- result.FullName.Should().Be(ceiling.FullName);
+ result.FullName.Should().Be(expected.FullName);
}
[Fact]
@@ -127,27 +130,44 @@ public void NoGitDirectory_ReturnsNull()
fs.AddDirectory("/repo/docs");
fs.AddFile("/repo/docs/docset.yml", new("toc: []"));
- var ceiling = fs.DirectoryInfo.New("/repo");
var start = fs.DirectoryInfo.New("/repo/docs");
- var result = Paths.FindGitRoot(start, ceiling: ceiling);
+ var result = Paths.FindGitRoot(start);
result.Should().BeNull();
}
[Fact]
- public void WorktreeGitFile_InsideCeiling_IsAccepted()
+ public void WorktreeGitFile_FindsGitRoot()
{
var fs = new MockFileSystem();
fs.AddFile("/repo/.git", new("gitdir: /main/.git/worktrees/repo"));
fs.AddFile("/repo/docs/team/docset.yml", new("toc: []"));
- var ceiling = fs.DirectoryInfo.New("/repo");
var start = fs.DirectoryInfo.New("/repo/docs/team");
+ var expected = fs.DirectoryInfo.New("/repo");
+
+ // .git file (worktree pointer) counts the same as a .git dir; depth 2 needs maxParents=2
+ var result = Paths.FindGitRoot(start, maxParents: 2);
+
+ result.Should().NotBeNull();
+ result.FullName.Should().Be(expected.FullName);
+ }
- var result = Paths.FindGitRoot(start, ceiling: ceiling);
+ [Fact]
+ public void WorktreeGitFile_OneLevel_FindsGitRoot()
+ {
+ var fs = new MockFileSystem();
+ fs.AddFile("/repo/.git", new("gitdir: /main/.git/worktrees/repo"));
+ fs.AddFile("/repo/docs/docset.yml", new("toc: []"));
+
+ var start = fs.DirectoryInfo.New("/repo/docs");
+ var expected = fs.DirectoryInfo.New("/repo");
+
+ // Docset one level below worktree root: found with default maxParents=1
+ var result = Paths.FindGitRoot(start);
result.Should().NotBeNull();
- result.FullName.Should().Be(ceiling.FullName);
+ result.FullName.Should().Be(expected.FullName);
}
}
diff --git a/tests/Elastic.Documentation.Configuration.Tests/GitCheckoutResolutionTests.cs b/tests/Elastic.Documentation.Configuration.Tests/GitCheckoutResolutionTests.cs
new file mode 100644
index 0000000000..17df8c3656
--- /dev/null
+++ b/tests/Elastic.Documentation.Configuration.Tests/GitCheckoutResolutionTests.cs
@@ -0,0 +1,234 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions.TestingHelpers;
+using AwesomeAssertions;
+using Elastic.Documentation;
+using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
+
+namespace Elastic.Documentation.Configuration.Tests;
+
+///
+/// Tests for driven through
+/// a seeded with various git layouts.
+/// Previously impossible because the mock short-circuit returned canned data for any MockFileSystem;
+/// the inverted short-circuit now attempts real resolution first and only falls back to canned data
+/// when resolution yields nothing.
+///
+public class GitCheckoutResolutionTests
+{
+ private static MockFileSystem BuildFs(string root, string? branch = "main", string? sha = null, string? remote = null, bool worktree = false, string? worktreeGitDir = null)
+ {
+ var fs = new MockFileSystem();
+ sha ??= "abc1234def5678";
+ remote ??= "elastic/test-repo";
+
+ if (!worktree)
+ {
+ fs.AddDirectory($"{root}/.git");
+ // HEAD
+ if (branch is not null)
+ fs.AddFile($"{root}/.git/HEAD", new MockFileData($"ref: refs/heads/{branch}\n"));
+ else
+ fs.AddFile($"{root}/.git/HEAD", new MockFileData($"{sha}\n")); // detached
+ // ref file
+ if (branch is not null)
+ fs.AddFile($"{root}/.git/refs/heads/{branch}", new MockFileData($"{sha}\n"));
+ // config
+ fs.AddFile($"{root}/.git/config", new MockFileData($"""
+ [core]
+ repositoryformatversion = 0
+ [remote "origin"]
+ url = https://github.com/{remote}.git
+ [branch "{branch ?? "main"}"]
+ remote = origin
+ merge = refs/heads/{branch ?? "main"}
+ """));
+ }
+ else
+ {
+ // Worktree: .git is a file pointing to the real git dir
+ var realGitDir = worktreeGitDir ?? $"/main-repo/.git/worktrees/branch";
+ fs.AddFile($"{root}/.git", new MockFileData($"gitdir: {realGitDir}\n"));
+ // The real .git directory
+ fs.AddDirectory(realGitDir);
+ fs.AddFile($"{realGitDir}/HEAD", new MockFileData($"ref: refs/heads/{branch}\n"));
+ fs.AddFile($"{realGitDir}/refs/heads/{branch}", new MockFileData($"{sha}\n"));
+ fs.AddFile($"{realGitDir}/config", new MockFileData($"""
+ [core]
+ repositoryformatversion = 0
+ [remote "origin"]
+ url = https://github.com/{remote}.git
+ [branch "{branch}"]
+ remote = origin
+ merge = refs/heads/{branch}
+ """));
+ }
+
+ return fs;
+ }
+
+ [Fact]
+ public void RegularRepo_ReturnsGitInfo()
+ {
+ var fs = BuildFs("/repo", branch: "feature/my-branch", sha: "deadbeef1234");
+
+ var checkout = fs.DirectoryInfo.New("/repo");
+ var scoped = new CheckoutsFileSystem(fs.DirectoryInfo.New("/repo"), inner: fs);
+
+ var result = GitCheckoutInformationFactory.Create(checkout, scoped);
+
+ result.IsAvailable.Should().BeTrue();
+ result.Branch.Should().Be("feature/my-branch");
+ result.Ref.Should().Be("deadbeef1234");
+ // Remote is the full config url with .git suffix stripped; RepositoryName is the last segment
+ result.Remote.Should().EndWith("/elastic/test-repo");
+ result.RepositoryName.Should().Be("test-repo");
+ }
+
+ [Fact]
+ public void RegularRepo_DetachedHead_NeverReturnsRandomGuid()
+ {
+ var fs = BuildFs("/repo", branch: null, sha: "cafebabe9876");
+
+ var checkout = fs.DirectoryInfo.New("/repo");
+ var scoped = new CheckoutsFileSystem(fs.DirectoryInfo.New("/repo"), inner: fs);
+
+ var result = GitCheckoutInformationFactory.Create(checkout, scoped);
+
+ // The production code reads GITHUB_PR_REF_NAME ?? GITHUB_REF_NAME ?? "detached/head".
+ // Mirror that lookup so the test passes both locally and on CI (where GITHUB_REF_NAME=3789/merge).
+ var expectedBranch = Environment.GetEnvironmentVariable("GITHUB_PR_REF_NAME")
+ ?? Environment.GetEnvironmentVariable("GITHUB_REF_NAME")
+ ?? "detached/head";
+ result.IsAvailable.Should().BeTrue();
+ result.Ref.Should().Be("cafebabe9876", "detached HEAD must use the actual SHA, never a random GUID");
+ result.Branch.Should().Be(expectedBranch);
+ }
+
+ [Fact]
+ public void WorktreeWithAbsoluteGitDir_ResolvesViaMainRepo()
+ {
+ var sha = "1a2b3c4d5e6f";
+ var fs = BuildFs("/worktree", branch: "my-feature", sha: sha, remote: "elastic/worktree-repo",
+ worktree: true, worktreeGitDir: "/main-repo/.git/worktrees/my-feature");
+
+ // Scope must cover both the worktree dir and the main .git
+ var scoped = new CheckoutsFileSystem(fs.DirectoryInfo.New("/worktree"), inner: fs);
+ var extended = new Nullean.ScopedFileSystem.ScopedFileSystem(fs,
+ new Nullean.ScopedFileSystem.ScopedFileSystemOptions(["/worktree", "/main-repo/.git/worktrees/my-feature"])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" }
+ });
+
+ var checkout = fs.DirectoryInfo.New("/worktree");
+ var result = GitCheckoutInformationFactory.Create(checkout, extended);
+
+ result.IsAvailable.Should().BeTrue();
+ result.Branch.Should().Be("my-feature");
+ result.Ref.Should().Be(sha);
+ result.RepositoryName.Should().Be("worktree-repo");
+ }
+
+ [Fact]
+ public void WorktreeWithRelativeGitDir_ResolvesAgainstGitFileDirectory()
+ {
+ var fs = new MockFileSystem();
+ // .git file with relative gitdir — resolved relative to /worktree, NOT process CWD
+ fs.AddFile("/worktree/.git", new MockFileData("gitdir: ../.git/worktrees/branch\n"));
+ fs.AddDirectory("/.git/worktrees/branch");
+ fs.AddFile("/.git/worktrees/branch/HEAD", new MockFileData("ref: refs/heads/feature\n"));
+ fs.AddFile("/.git/worktrees/branch/refs/heads/feature", new MockFileData("aabbccdd\n"));
+ fs.AddFile("/.git/worktrees/branch/config", new MockFileData("""
+ [remote "origin"]
+ url = https://github.com/elastic/relative-test.git
+ [branch "feature"]
+ remote = origin
+ merge = refs/heads/feature
+ """));
+
+ var checkout = fs.DirectoryInfo.New("/worktree");
+ var scoped = new Nullean.ScopedFileSystem.ScopedFileSystem(fs,
+ new Nullean.ScopedFileSystem.ScopedFileSystemOptions(["/worktree", "/.git"])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" }
+ });
+
+ var result = GitCheckoutInformationFactory.Create(checkout, scoped);
+
+ result.IsAvailable.Should().BeTrue();
+ result.Branch.Should().Be("feature");
+ result.Ref.Should().Be("aabbccdd");
+ result.RepositoryName.Should().Be("relative-test");
+ }
+
+ [Fact]
+ public void WorktreeWithCommondir_ResolvesViaCommondir()
+ {
+ var fs = new MockFileSystem();
+ // .git file → worktree-specific dir → commondir → shared .git
+ fs.AddFile("/worktree/.git", new MockFileData("gitdir: /main/.git/worktrees/wt\n"));
+ fs.AddDirectory("/main/.git/worktrees/wt");
+ fs.AddFile("/main/.git/worktrees/wt/commondir", new MockFileData("../..\n")); // points to /main/.git
+ // The shared .git directory
+ fs.AddDirectory("/main/.git");
+ fs.AddFile("/main/.git/HEAD", new MockFileData("ref: refs/heads/topic\n"));
+ fs.AddFile("/main/.git/refs/heads/topic", new MockFileData("fedcba987654\n"));
+ fs.AddFile("/main/.git/config", new MockFileData("""
+ [remote "origin"]
+ url = https://github.com/elastic/commondir-test.git
+ [branch "topic"]
+ remote = origin
+ merge = refs/heads/topic
+ """));
+
+ var checkout = fs.DirectoryInfo.New("/worktree");
+ var scoped = new Nullean.ScopedFileSystem.ScopedFileSystem(fs,
+ new Nullean.ScopedFileSystem.ScopedFileSystemOptions(["/worktree", "/main/.git"])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" }
+ });
+
+ var result = GitCheckoutInformationFactory.Create(checkout, scoped);
+
+ result.IsAvailable.Should().BeTrue();
+ result.Branch.Should().Be("topic");
+ result.Ref.Should().Be("fedcba987654");
+ result.RepositoryName.Should().Be("commondir-test");
+ }
+
+ [Fact]
+ public void WorktreeMissingGitDir_ReturnsUnavailable()
+ {
+ var fs = new MockFileSystem();
+ fs.AddFile("/worktree/.git", new MockFileData("gitdir: /nonexistent/.git/worktrees/wt\n"));
+
+ var checkout = fs.DirectoryInfo.New("/worktree");
+ var scoped = new CheckoutsFileSystem(fs.DirectoryInfo.New("/worktree"), inner: fs);
+
+ var result = GitCheckoutInformationFactory.Create(checkout, scoped);
+
+ result.IsAvailable.Should().BeFalse("a worktree pointer to a missing gitdir must yield Unavailable");
+ }
+
+ [Fact]
+ public void MockWithNoGitLayout_ReturnsCannedTestData()
+ {
+ // Back-compat: tests that seed no .git at all must continue to receive the canned
+ // test instance rather than Unavailable, so existing test suites need no churn.
+ var fs = new MockFileSystem();
+ var checkout = fs.DirectoryInfo.New("/some/path");
+ var scoped = new CheckoutsFileSystem(fs.DirectoryInfo.New("/some/path"), inner: fs);
+
+ var result = GitCheckoutInformationFactory.Create(checkout, scoped);
+
+ result.IsAvailable.Should().BeTrue("mock with no layout falls back to canned data");
+ result.Branch.Should().Be("test-e35fcb27-5f60-4e");
+ result.RepositoryName.Should().Be("docs-builder");
+ }
+}
diff --git a/tests/Elastic.Documentation.Configuration.Tests/ProductFeaturesTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ProductFeaturesTests.cs
index dcaf7ef439..9984c43381 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/ProductFeaturesTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/ProductFeaturesTests.cs
@@ -6,6 +6,7 @@
using AwesomeAssertions;
using Elastic.Documentation.Configuration.Products;
using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.FileSystems;
using Microsoft.Extensions.Logging.Abstractions;
namespace Elastic.Documentation.Configuration.Tests;
@@ -107,8 +108,7 @@ public void GetProductByRepositoryName_WorksForProductsWithDisabledFeatures()
private static ProductsConfiguration LoadActualProductsConfiguration()
{
- var fileSystem = new FileSystem();
- var provider = new ConfigurationFileProvider(new NullLoggerFactory(), fileSystem);
+ var provider = new ConfigurationFileProvider(new NullLoggerFactory(), new ConfigurationFileSystem());
var versionsConfig = provider.CreateVersionConfiguration();
return provider.CreateProducts(versionsConfig);
}
diff --git a/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs b/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs
index 4ea6c22fd2..f3aefa5047 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs
@@ -9,6 +9,7 @@
using Elastic.Documentation.Configuration.Inference;
using Elastic.Documentation.Configuration.Products;
using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Versions;
using Microsoft.Extensions.Logging.Abstractions;
@@ -498,7 +499,7 @@ public void IsVersionlessCorrectlyIdentifiesAllVersionlessSystemsFromActualConfi
var versionsPath = fileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, "config", "versions.yml");
File.Exists(versionsPath).Should().BeTrue($"Expected versions file to exist at {versionsPath}");
- var provider = new ConfigurationFileProvider(new NullLoggerFactory(), fileSystem);
+ var provider = new ConfigurationFileProvider(new NullLoggerFactory(), new ConfigurationFileSystem());
var versionsConfig = provider.CreateVersionConfiguration();
// Verify all expected versionless systems are marked as versionless
@@ -530,7 +531,7 @@ public void AllVersioningSystemsInConfigAreAccountedFor()
var versionsPath = fileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, "config", "versions.yml");
File.Exists(versionsPath).Should().BeTrue($"Expected versions file to exist at {versionsPath}");
- var provider = new ConfigurationFileProvider(new NullLoggerFactory(), fileSystem);
+ var provider = new ConfigurationFileProvider(new NullLoggerFactory(), new ConfigurationFileSystem());
var versionsConfig = provider.CreateVersionConfiguration();
// Count how many are versionless vs versioned
diff --git a/tests/Elastic.Markdown.Tests/Assembler/AssemblerHtmxMarkdownLinkTests.cs b/tests/Elastic.Markdown.Tests/Assembler/AssemblerHtmxMarkdownLinkTests.cs
index bd9cb950e3..9ba1bfd67a 100644
--- a/tests/Elastic.Markdown.Tests/Assembler/AssemblerHtmxMarkdownLinkTests.cs
+++ b/tests/Elastic.Markdown.Tests/Assembler/AssemblerHtmxMarkdownLinkTests.cs
@@ -9,7 +9,6 @@
using Elastic.Documentation.Diagnostics;
using Elastic.Markdown.IO;
using Elastic.Markdown.Tests.Inline;
-using Nullean.ScopedFileSystem;
using Xunit;
namespace Elastic.Markdown.Tests.Assembler;
@@ -24,7 +23,7 @@ protected override BuildContext CreateBuildContext(
TestDiagnosticsCollector collector,
MockFileSystem fileSystem,
IConfigurationContext configurationContext) =>
- new(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext)
+ new(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext)
{
UrlPathPrefix = "/docs/platform/elasticsearch",
BuildType = BuildType.Assembler
@@ -59,7 +58,7 @@ protected override BuildContext CreateBuildContext(
TestDiagnosticsCollector collector,
MockFileSystem fileSystem,
IConfigurationContext configurationContext) =>
- new(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext)
+ new(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext)
{
UrlPathPrefix = "/docs/platform/elasticsearch",
BuildType = BuildType.Assembler
@@ -87,7 +86,7 @@ protected override BuildContext CreateBuildContext(
TestDiagnosticsCollector collector,
MockFileSystem fileSystem,
IConfigurationContext configurationContext) =>
- new(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext)
+ new(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext)
{
UrlPathPrefix = "/docs",
BuildType = BuildType.Assembler
@@ -117,7 +116,7 @@ protected override BuildContext CreateBuildContext(
TestDiagnosticsCollector collector,
MockFileSystem fileSystem,
IConfigurationContext configurationContext) =>
- new(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext)
+ new(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext)
{
UrlPathPrefix = "/docs/platform/elasticsearch",
BuildType = BuildType.Assembler
@@ -146,7 +145,7 @@ protected override BuildContext CreateBuildContext(
TestDiagnosticsCollector collector,
MockFileSystem fileSystem,
IConfigurationContext configurationContext) =>
- new(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext)
+ new(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext)
{
UrlPathPrefix = "/docs/platform/elasticsearch",
BuildType = BuildType.Assembler
@@ -185,7 +184,7 @@ protected override BuildContext CreateBuildContext(
TestDiagnosticsCollector collector,
MockFileSystem fileSystem,
IConfigurationContext configurationContext) =>
- new(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext)
+ new(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext)
{
UrlPathPrefix = "/docs/platform/elasticsearch",
BuildType = BuildType.Assembler
@@ -213,7 +212,7 @@ protected override BuildContext CreateBuildContext(
TestDiagnosticsCollector collector,
MockFileSystem fileSystem,
IConfigurationContext configurationContext) =>
- new(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext)
+ new(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext)
{
UrlPathPrefix = "/docs/platform/elasticsearch",
BuildType = BuildType.Assembler
diff --git a/tests/Elastic.Markdown.Tests/BrandingCopyTests.cs b/tests/Elastic.Markdown.Tests/BrandingCopyTests.cs
index 22ff9a961d..8c640eff1d 100644
--- a/tests/Elastic.Markdown.Tests/BrandingCopyTests.cs
+++ b/tests/Elastic.Markdown.Tests/BrandingCopyTests.cs
@@ -18,7 +18,7 @@ public async Task CopyBrandingResources_SeparateFileSystems_DoesNotThrow()
{
var logger = new TestLoggerFactory(output);
- var readFs = new MockFileSystem(new Dictionary
+ var fs = new MockFileSystem(new Dictionary
{
{ "docs/docset.yml",
//language=yaml
@@ -36,16 +36,9 @@ public async Task CopyBrandingResources_SeparateFileSystems_DoesNotThrow()
CurrentDirectory = Paths.WorkingDirectoryRoot.FullName
});
- var writeFs = new MockFileSystem(new MockFileSystemOptions
- {
- CurrentDirectory = Paths.WorkingDirectoryRoot.FullName
- });
-
await using var collector = new DiagnosticsCollector([]).StartAsync(TestContext.Current.CancellationToken);
- var configurationContext = TestHelpers.CreateConfigurationContext(readFs);
- var readScoped = FileSystemFactory.ScopeCurrentWorkingDirectory(readFs);
- var writeScoped = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(writeFs);
- var context = new BuildContext(collector, readScoped, writeScoped, configurationContext, ExportOptions.Default);
+ var configurationContext = TestHelpers.CreateConfigurationContext(fs);
+ var context = new BuildContext(collector, TestHelpers.CreateDocumentationFileSystem(fs), configurationContext);
var linkResolver = new TestCrossLinkResolver();
var set = new DocumentationSet(context, logger, linkResolver);
@@ -55,6 +48,6 @@ public async Task CopyBrandingResources_SeparateFileSystems_DoesNotThrow()
await collector.StopAsync(TestContext.Current.CancellationToken);
var outputStaticDir = Path.Join(set.OutputDirectory.FullName, "_static");
- writeFs.File.Exists(Path.Join(outputStaticDir, "logo.svg")).Should().BeTrue();
+ fs.File.Exists(Path.Join(outputStaticDir, "logo.svg")).Should().BeTrue();
}
}
diff --git a/tests/Elastic.Markdown.Tests/BuildContextConfigurationFileTests.cs b/tests/Elastic.Markdown.Tests/BuildContextConfigurationFileTests.cs
index 61c6ed9e09..fa708156ce 100644
--- a/tests/Elastic.Markdown.Tests/BuildContextConfigurationFileTests.cs
+++ b/tests/Elastic.Markdown.Tests/BuildContextConfigurationFileTests.cs
@@ -7,7 +7,7 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Builder;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
using Xunit;
namespace Elastic.Markdown.Tests;
@@ -33,21 +33,19 @@ public void ExplicitConfigurationFile_OverridesDefaultDiscovery()
fs.AddFile(publicDocsetPath, new MockFileData("toc: []\n"));
fs.AddFile(internalDocsetPath, new MockFileData("registry: internal\ntoc: []\n"));
- var readFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
- var writeFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
var collector = new TestDiagnosticsCollector(output);
_ = collector.StartAsync(TestContext.Current.CancellationToken);
var configurationContext = TestHelpers.CreateConfigurationContext(fs);
+ var docFs = DocumentationFileSystem.Resolve(
+ fs.DirectoryInfo.New(repoPath),
+ new DocumentationScopeOptions
+ {
+ Inner = fs,
+ ConfigurationFile = internalDocsetPath,
+ Output = Path.Join(root, "codex-configuration-file-test-out")
+ });
- var context = new BuildContext(
- collector,
- readFs,
- writeFs,
- configurationContext,
- ExportOptions.Default,
- source: repoPath,
- output: Path.Combine(root, "codex-configuration-file-test-out"),
- configurationFile: readFs.FileInfo.New(internalDocsetPath));
+ var context = new BuildContext(collector, docFs, configurationContext);
context.ConfigurationPath.FullName.Should().Be(internalDocsetPath);
context.DocumentationSourceDirectory.FullName.Should().Be(Path.Combine(repoPath, "docs-dev"));
@@ -67,20 +65,18 @@ public void NoExplicitConfigurationFile_FallsBackToDefaultDiscovery()
fs.AddDirectory(Path.Combine(repoPath, ".git"));
fs.AddFile(publicDocsetPath, new MockFileData("toc: []\n"));
- var readFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
- var writeFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
var collector = new TestDiagnosticsCollector(output);
_ = collector.StartAsync(TestContext.Current.CancellationToken);
var configurationContext = TestHelpers.CreateConfigurationContext(fs);
+ var docFs = DocumentationFileSystem.Resolve(
+ fs.DirectoryInfo.New(repoPath),
+ new DocumentationScopeOptions
+ {
+ Inner = fs,
+ Output = Path.Join(root, "codex-configuration-file-fallback-test-out")
+ });
- var context = new BuildContext(
- collector,
- readFs,
- writeFs,
- configurationContext,
- ExportOptions.Default,
- source: repoPath,
- output: Path.Combine(root, "codex-configuration-file-fallback-test-out"));
+ var context = new BuildContext(collector, docFs, configurationContext);
context.ConfigurationPath.FullName.Should().Be(publicDocsetPath);
}
diff --git a/tests/Elastic.Markdown.Tests/BuildContextDocumentationCheckoutDirectoryTests.cs b/tests/Elastic.Markdown.Tests/BuildContextDocumentationCheckoutDirectoryTests.cs
index d37f0b32a5..a69f2a8ba1 100644
--- a/tests/Elastic.Markdown.Tests/BuildContextDocumentationCheckoutDirectoryTests.cs
+++ b/tests/Elastic.Markdown.Tests/BuildContextDocumentationCheckoutDirectoryTests.cs
@@ -7,15 +7,23 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Builder;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
using Xunit;
namespace Elastic.Markdown.Tests;
///
-/// Regression: Codex must pass the repository clone root as source
-/// so FindGitRoot(..., ceiling: rootFolder) can see .git under the ceiling (#3115).
-/// Prior bug used DocsDirectory only, capping the ceiling at the docs subtree.
+/// Tests that resolves correctly
+/// for various --path / source combinations.
+///
+/// Key invariant: --path repo/ and --path repo/docs/ must resolve to the
+/// same CheckoutDirectory because the docset scan converges first (both land on
+/// repo/docs/ as the anchor), and FindGitRoot then walks at most one parent.
+///
+///
+/// Regression guard (#3115): Codex passes the repository clone root as source so that
+/// FindGitRoot can see .git within the default maxParents range.
+///
///
public class BuildContextDocumentationCheckoutDirectoryTests(ITestOutputHelper output)
{
@@ -28,26 +36,24 @@ public void SourceAsRepositoryRoot_SetsDocumentationCheckoutDirectory()
fs.AddDirectory(Path.Combine(repoPath, ".git"));
fs.AddFile(Path.Combine(repoPath, "docs", "docset.yml"), new MockFileData("toc: []\n"));
- var readFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
- var writeFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
var collector = new TestDiagnosticsCollector(output);
_ = collector.StartAsync(TestContext.Current.CancellationToken);
var configurationContext = TestHelpers.CreateConfigurationContext(fs);
- var context = new BuildContext(
- collector,
- readFs,
- writeFs,
- configurationContext,
- ExportOptions.Default,
- source: repoPath,
- output: Path.Combine(root, "codex-checkout-dir-test-out"));
+ var docFs = DocumentationFileSystem.Resolve(
+ fs.DirectoryInfo.New(repoPath),
+ new DocumentationScopeOptions
+ {
+ Inner = fs,
+ Output = Path.Join(root, "codex-checkout-dir-test-out")
+ });
+ var context = new BuildContext(collector, docFs, configurationContext);
Assert.NotNull(context.DocumentationCheckoutDirectory);
context.DocumentationCheckoutDirectory.FullName.Should().Be(repoPath);
}
[Fact]
- public void SourceAsDocsSubtreeOnly_LeavesDocumentationCheckoutDirectoryNull()
+ public void SourceAsDocsSubtree_ResolvesCheckoutFromParent()
{
var root = Paths.WorkingDirectoryRoot.FullName;
var repoPath = Path.Combine(root, "codex-docs-only-test");
@@ -56,20 +62,55 @@ public void SourceAsDocsSubtreeOnly_LeavesDocumentationCheckoutDirectoryNull()
fs.AddDirectory(Path.Combine(repoPath, ".git"));
fs.AddFile(Path.Combine(docsPath, "docset.yml"), new MockFileData("toc: []\n"));
- var readFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
- var writeFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
var collector = new TestDiagnosticsCollector(output);
_ = collector.StartAsync(TestContext.Current.CancellationToken);
var configurationContext = TestHelpers.CreateConfigurationContext(fs);
- var context = new BuildContext(
- collector,
- readFs,
- writeFs,
- configurationContext,
- ExportOptions.Default,
- source: docsPath,
- output: Path.Combine(root, "codex-docs-only-test-out"));
+ var docFs = DocumentationFileSystem.Resolve(
+ fs.DirectoryInfo.New(docsPath),
+ new DocumentationScopeOptions
+ {
+ Inner = fs,
+ Output = Path.Join(root, "codex-docs-only-test-out")
+ });
+ var context = new BuildContext(collector, docFs, configurationContext);
- context.DocumentationCheckoutDirectory.Should().BeNull();
+ // --path repo/docs/ now resolves the same checkout as --path repo/:
+ // the docset scan anchors at repo/docs/, FindGitRoot walks one parent to repo/.git
+ Assert.NotNull(context.DocumentationCheckoutDirectory);
+ context.DocumentationCheckoutDirectory.FullName.Should().Be(repoPath);
+ }
+
+ [Fact]
+ public void PathAndDocsSubfolder_ResolveIdenticalCheckout()
+ {
+ var root = Paths.WorkingDirectoryRoot.FullName;
+ var repoPath = Path.Combine(root, "codex-equiv-test");
+ var docsPath = Path.Combine(repoPath, "docs");
+ var fs = new MockFileSystem(new MockFileSystemOptions { CurrentDirectory = root });
+ fs.AddDirectory(Path.Combine(repoPath, ".git"));
+ fs.AddFile(Path.Combine(docsPath, "docset.yml"), new MockFileData("toc: []\n"));
+
+ var collector = new TestDiagnosticsCollector(output);
+ _ = collector.StartAsync(TestContext.Current.CancellationToken);
+ var configurationContext = TestHelpers.CreateConfigurationContext(fs);
+ var opts = new DocumentationScopeOptions
+ {
+ Inner = fs,
+ Output = Path.Combine(root, "codex-equiv-test-out")
+ };
+
+ var fsFromRepoRoot = DocumentationFileSystem.Resolve(fs.DirectoryInfo.New(repoPath), opts);
+ var fsFromDocsFolder = DocumentationFileSystem.Resolve(fs.DirectoryInfo.New(docsPath), opts);
+ var contextFromRepoRoot = new BuildContext(collector, fsFromRepoRoot, configurationContext);
+ var contextFromDocsFolder = new BuildContext(collector, fsFromDocsFolder, configurationContext);
+
+ contextFromRepoRoot.DocumentationCheckoutDirectory.Should().NotBeNull();
+ contextFromDocsFolder.DocumentationCheckoutDirectory.Should().NotBeNull();
+ contextFromRepoRoot.DocumentationCheckoutDirectory.FullName
+ .Should().Be(contextFromDocsFolder.DocumentationCheckoutDirectory.FullName,
+ "--path repo/ and --path repo/docs/ must resolve to the same CheckoutDirectory");
+ contextFromRepoRoot.DocumentationSourceDirectory.FullName
+ .Should().Be(contextFromDocsFolder.DocumentationSourceDirectory.FullName,
+ "--path repo/ and --path repo/docs/ must resolve to the same SourceDirectory");
}
}
diff --git a/tests/Elastic.Markdown.Tests/Codex/CodexHtmxCrossLinkTests.cs b/tests/Elastic.Markdown.Tests/Codex/CodexHtmxCrossLinkTests.cs
index 36b838c9de..18e7469627 100644
--- a/tests/Elastic.Markdown.Tests/Codex/CodexHtmxCrossLinkTests.cs
+++ b/tests/Elastic.Markdown.Tests/Codex/CodexHtmxCrossLinkTests.cs
@@ -9,7 +9,6 @@
using Elastic.Documentation.Links.CrossLinks;
using Elastic.Markdown.IO;
using Elastic.Markdown.Tests.Inline;
-using Nullean.ScopedFileSystem;
namespace Elastic.Markdown.Tests.Codex;
@@ -20,7 +19,7 @@ protected override BuildContext CreateBuildContext(
TestDiagnosticsCollector collector,
MockFileSystem fileSystem,
IConfigurationContext configurationContext) =>
- new(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext)
+ new(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext)
{
UrlPathPrefix = "/r/codex-environments",
BuildType = BuildType.Codex
@@ -58,7 +57,7 @@ protected override BuildContext CreateBuildContext(
TestDiagnosticsCollector collector,
MockFileSystem fileSystem,
IConfigurationContext configurationContext) =>
- new(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext)
+ new(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext)
{
UrlPathPrefix = "/docs",
BuildType = BuildType.Isolated
diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs
index cc0fc2c822..acb5c13fe7 100644
--- a/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs
+++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs
@@ -5,6 +5,7 @@
using System.Collections.Frozen;
using System.IO.Abstractions.TestingHelpers;
using AwesomeAssertions;
+using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.ReleaseNotes;
using Elastic.Documentation.ReleaseNotes;
@@ -559,6 +560,9 @@ public class ChangelogCdnInferredProductUnavailableTests(ITestOutputHelper outpu
:::
""")
{
+ // Force Unavailable so InferCdnProductFromRepository() returns null — the "could not be inferred" path.
+ protected override GitCheckoutInformation? GetGitCheckoutInformation() => GitCheckoutInformation.Unavailable;
+
[Fact]
public void EmitsErrorWhenProductCannotBeInferred()
{
diff --git a/tests/Elastic.Markdown.Tests/Directives/CsvIncludeTests.cs b/tests/Elastic.Markdown.Tests/Directives/CsvIncludeTests.cs
index 0307f5ff3d..d705229689 100644
--- a/tests/Elastic.Markdown.Tests/Directives/CsvIncludeTests.cs
+++ b/tests/Elastic.Markdown.Tests/Directives/CsvIncludeTests.cs
@@ -5,6 +5,7 @@
using System.IO.Abstractions.TestingHelpers;
using AwesomeAssertions;
using Elastic.Documentation.Configuration;
+using Elastic.Documentation.FileSystems;
using Elastic.Markdown.Myst.Directives.CsvInclude;
namespace Elastic.Markdown.Tests.Directives;
@@ -38,7 +39,7 @@ public CsvIncludeTests(ITestOutputHelper output) : base(output,
[Fact]
public void ParsesCsvDataCorrectly()
{
- var csvData = CsvReader.ReadCsvFile(Block!.CsvFilePath!, Block.Separator, FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem)).ToList();
+ var csvData = CsvReader.ReadCsvFile(Block!.CsvFilePath!, Block.Separator, CheckoutsFileSystem.FromWorkingDirectory(FileSystem)).ToList();
csvData.Should().HaveCount(4);
csvData[0].Should().BeEquivalentTo(["Name", "Age", "City"]);
csvData[1].Should().BeEquivalentTo(["John Doe", "30", "New York"]);
@@ -72,7 +73,7 @@ public CsvIncludeWithOptionsTests(ITestOutputHelper output) : base(output,
[Fact]
public void ParsesWithCustomSeparator()
{
- var csvData = CsvReader.ReadCsvFile(Block!.CsvFilePath!, Block.Separator, FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem)).ToList();
+ var csvData = CsvReader.ReadCsvFile(Block!.CsvFilePath!, Block.Separator, CheckoutsFileSystem.FromWorkingDirectory(FileSystem)).ToList();
csvData.Should().HaveCount(3);
csvData[0].Should().BeEquivalentTo(["Name", "Age", "City"]);
csvData[1].Should().BeEquivalentTo(["John Doe", "30", "New York"]);
@@ -94,7 +95,7 @@ public CsvIncludeWithQuotesTests(ITestOutputHelper output) : base(output,
[Fact]
public void HandlesQuotedFieldsWithCommas()
{
- var csvData = CsvReader.ReadCsvFile(Block!.CsvFilePath!, Block.Separator, FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem)).ToList();
+ var csvData = CsvReader.ReadCsvFile(Block!.CsvFilePath!, Block.Separator, CheckoutsFileSystem.FromWorkingDirectory(FileSystem)).ToList();
csvData.Should().HaveCount(3);
csvData[0].Should().BeEquivalentTo(["Name", "Description", "Location"]);
csvData[1].Should().BeEquivalentTo(["John Doe", "Software Engineer, Senior", "New York"]);
@@ -116,7 +117,7 @@ public CsvIncludeWithEscapedQuotesTests(ITestOutputHelper output) : base(output,
[Fact]
public void HandlesEscapedQuotes()
{
- var csvData = CsvReader.ReadCsvFile(Block!.CsvFilePath!, Block.Separator, FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem)).ToList();
+ var csvData = CsvReader.ReadCsvFile(Block!.CsvFilePath!, Block.Separator, CheckoutsFileSystem.FromWorkingDirectory(FileSystem)).ToList();
csvData.Should().HaveCount(3);
csvData[0].Should().BeEquivalentTo(["Name", "Description"]);
csvData[1].Should().BeEquivalentTo(["John Doe", "He said \"Hello World\" today"]);
diff --git a/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs b/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs
index 28c57c3d8d..6add618421 100644
--- a/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs
+++ b/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs
@@ -10,7 +10,6 @@
using Elastic.Markdown.Myst.Directives;
using JetBrains.Annotations;
using Markdig.Syntax;
-using Nullean.ScopedFileSystem;
namespace Elastic.Markdown.Tests.Directives;
@@ -70,12 +69,12 @@ protected DirectiveTest(ITestOutputHelper output, [LanguageInjection("markdown")
var root = FileSystem.DirectoryInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName, "docs/"));
// ReSharper disable once VirtualMemberCallInConstructor
FileSystem.GenerateDocSetYaml(root, products: GetDocsetProducts(), extraYaml: GetDocsetExtraYaml());
-
Collector = new TestDiagnosticsCollector(output);
var configurationContext = TestHelpers.CreateConfigurationContext(FileSystem);
// ReSharper disable once VirtualMemberCallInConstructor
var environment = GetEnvironment();
- var context = new BuildContext(Collector, FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem), configurationContext, environment);
+ // ReSharper disable once VirtualMemberCallInConstructor
+ var context = new BuildContext(Collector, TestHelpers.CreateDocumentationFileSystem(FileSystem, root, GetGitCheckoutInformation()), configurationContext, environment);
var linkResolver = new TestCrossLinkResolver();
// ReSharper disable once VirtualMemberCallInConstructor
Set = new DocumentationSet(context, logger, linkResolver, GetReleaseNotesResolver());
@@ -86,6 +85,12 @@ protected DirectiveTest(ITestOutputHelper output, [LanguageInjection("markdown")
protected virtual void AddToFileSystem(MockFileSystem fileSystem) { }
+ ///
+ /// Override to supply an explicit for the build context.
+ /// Returns by default (factory produces canned test data).
+ ///
+ protected virtual GitCheckoutInformation? GetGitCheckoutInformation() => null;
+
///
/// Override to specify products for the docset configuration.
/// Returns null by default (no products configured).
diff --git a/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs b/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs
index 95dcc9dbea..5331eb018e 100644
--- a/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs
+++ b/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs
@@ -2,15 +2,14 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
-using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using AwesomeAssertions;
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Builder;
+using Elastic.Documentation.FileSystems;
using Elastic.Markdown.IO;
using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
namespace Elastic.Markdown.Tests.DocSet;
@@ -19,16 +18,16 @@ public class NavigationTestsBase : IAsyncLifetime
protected NavigationTestsBase(ITestOutputHelper output)
{
LoggerFactory = new TestLoggerFactory(output);
- var mockWriteFs = new MockFileSystem(new MockFileSystemOptions //use in memory mock fs to test generation
+ var mockWriteFs = new MockFileSystem(new MockFileSystemOptions
{
CurrentDirectory = Paths.WorkingDirectoryRoot.FullName
});
- ReadFileSystem = FileSystemFactory.RealGitRootForPath(null);
- WriteFileSystem = FileSystemFactory.ScopeCurrentWorkingDirectory(mockWriteFs);
- var collector = new TestDiagnosticsCollector(output);
- var configurationContext = TestHelpers.CreateConfigurationContext(ReadFileSystem);
var docsTestsPath = Path.Join(Paths.WorkingDirectoryRoot.FullName, "docs-tests");
- var context = new BuildContext(collector, ReadFileSystem, WriteFileSystem, configurationContext, ExportOptions.Default, source: docsTestsPath)
+ var invocation = new System.IO.Abstractions.FileSystem().DirectoryInfo.New(docsTestsPath);
+ FileSystem = DocumentationFileSystem.Resolve(invocation, new DocumentationScopeOptions { InnerWrite = mockWriteFs });
+ var collector = new TestDiagnosticsCollector(output);
+ var configurationContext = TestHelpers.CreateConfigurationContext(FileSystem.Read);
+ var context = new BuildContext(collector, FileSystem, configurationContext)
{
Force = false,
UrlPathPrefix = null
@@ -43,8 +42,7 @@ protected NavigationTestsBase(ITestOutputHelper output)
protected ILoggerFactory LoggerFactory { get; }
- protected ScopedFileSystem ReadFileSystem { get; set; }
- protected ScopedFileSystem WriteFileSystem { get; set; }
+ protected DocumentationFileSystem FileSystem { get; }
protected DocumentationSet Set { get; }
protected DocumentationGenerator Generator { get; }
protected ConfigurationFile? Configuration { get; set; }
diff --git a/tests/Elastic.Markdown.Tests/DocSet/ReportIssueUrlTests.cs b/tests/Elastic.Markdown.Tests/DocSet/ReportIssueUrlTests.cs
index 9cc7745dd9..0285d0ce59 100644
--- a/tests/Elastic.Markdown.Tests/DocSet/ReportIssueUrlTests.cs
+++ b/tests/Elastic.Markdown.Tests/DocSet/ReportIssueUrlTests.cs
@@ -7,9 +7,9 @@
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Builder;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation;
using Elastic.Markdown.IO;
-using Nullean.ScopedFileSystem;
namespace Elastic.Markdown.Tests.DocSet;
@@ -36,12 +36,12 @@ public ReportIssueUrlTests(ITestOutputHelper output)
{
CurrentDirectory = Paths.WorkingDirectoryRoot.FullName
});
- var readFileSystem = FileSystemFactory.RealGitRootForPath(null);
- var writeFileSystem = FileSystemFactory.ScopeCurrentWorkingDirectory(mockWriteFs);
+ var invocation = new System.IO.Abstractions.FileSystem().DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
+ var fs = DocumentationFileSystem.Resolve(invocation, new DocumentationScopeOptions { InnerWrite = mockWriteFs });
var collector = new TestDiagnosticsCollector(output);
- var configurationContext = TestHelpers.CreateConfigurationContext(readFileSystem);
+ var configurationContext = TestHelpers.CreateConfigurationContext(fs.Read);
- var context = new BuildContext(collector, readFileSystem, writeFileSystem, configurationContext, ExportOptions.Default)
+ var context = new BuildContext(collector, fs, configurationContext)
{
Force = false,
UrlPathPrefix = UrlPathPrefix,
diff --git a/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs b/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs
index 21ccbf2707..823e27ddc0 100644
--- a/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs
+++ b/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs
@@ -34,8 +34,8 @@ public class GitCheckoutInformationTests(ITestOutputHelper output) : NavigationT
[Fact]
public void Create()
{
- var root = ReadFileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
- var git = GitCheckoutInformationFactory.Create(root, ReadFileSystem, LoggerFactory.CreateLogger(nameof(GitCheckoutInformation)));
+ var root = FileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
+ var git = GitCheckoutInformationFactory.Create(root, FileSystem.Read, LoggerFactory.CreateLogger(nameof(GitCheckoutInformation)));
git.Should().NotBeNull();
git.Branch.Should().NotBeNullOrWhiteSpace();
diff --git a/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs b/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs
index 07b4a2cd26..5eb70a966b 100644
--- a/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs
+++ b/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs
@@ -15,7 +15,6 @@
using Elastic.Markdown.Myst;
using Elastic.Markdown.Myst.InlineParsers;
using Elastic.Markdown.Tests;
-using Nullean.ScopedFileSystem;
using Xunit;
namespace Elastic.Markdown.Tests.Inline;
@@ -93,7 +92,7 @@ private async Task ResolveUrlForBuildMode(string relativeAssetPath, Bu
_ = collector.StartAsync(TestContext.Current.CancellationToken);
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
- var buildContext = new BuildContext(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext)
+ var buildContext = new BuildContext(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext)
{
UrlPathPrefix = "/docs",
BuildType = buildType
diff --git a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs
index a42ee0ea4e..bbc860da2e 100644
--- a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs
+++ b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs
@@ -10,7 +10,6 @@
using JetBrains.Annotations;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
-using Nullean.ScopedFileSystem;
namespace Elastic.Markdown.Tests.Inline;
@@ -133,7 +132,7 @@ protected virtual BuildContext CreateBuildContext(
TestDiagnosticsCollector collector,
MockFileSystem fileSystem,
IConfigurationContext configurationContext) =>
- new(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext)
+ new(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext)
{
UrlPathPrefix = "/docs"
};
diff --git a/tests/Elastic.Markdown.Tests/MissingTocFileTests.cs b/tests/Elastic.Markdown.Tests/MissingTocFileTests.cs
index 4307bed63c..ba81aeb26f 100644
--- a/tests/Elastic.Markdown.Tests/MissingTocFileTests.cs
+++ b/tests/Elastic.Markdown.Tests/MissingTocFileTests.cs
@@ -7,7 +7,6 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
using Elastic.Markdown.IO;
-using Nullean.ScopedFileSystem;
namespace Elastic.Markdown.Tests;
@@ -36,7 +35,7 @@ public void TocReferencesMissingFile_DoesNotThrow_AndEmitsClearError()
var collector = new TestDiagnosticsCollector(output);
_ = collector.StartAsync(TestContext.Current.CancellationToken);
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
- var context = new BuildContext(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext);
+ var context = new BuildContext(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext);
var act = () => _ = new DocumentationSet(context, logger, new TestCrossLinkResolver());
diff --git a/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs b/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs
index 6d4e9221fc..682c5815cf 100644
--- a/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs
+++ b/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs
@@ -18,7 +18,7 @@ public async Task RelativeLinks()
var workingDirectory = Set.Configuration.SourceFile.DirectoryName;
Directory.SetCurrentDirectory(workingDirectory!);
- var mover = new Move(LoggerFactory, ReadFileSystem, WriteFileSystem, Set);
+ var mover = new Move(LoggerFactory, FileSystem.Read, FileSystem.Write, Set);
await mover.Execute("mover/first-page.md", "new-folder/hello-world.md", true, TestContext.Current.CancellationToken);
mover.Changes.Should().HaveCount(1);
@@ -47,7 +47,7 @@ public async Task MoveToFolder()
var workingDirectory = Set.Configuration.SourceFile.DirectoryName;
Directory.SetCurrentDirectory(workingDirectory!);
- var mover = new Move(LoggerFactory, ReadFileSystem, WriteFileSystem, Set);
+ var mover = new Move(LoggerFactory, FileSystem.Read, FileSystem.Write, Set);
await mover.Execute("mover/first-page.md", "new-folder", true, TestContext.Current.CancellationToken);
mover.Changes.Should().HaveCount(1);
@@ -75,7 +75,7 @@ public async Task MoveFolderToFolder()
var workingDirectory = Set.Configuration.SourceFile.DirectoryName;
Directory.SetCurrentDirectory(workingDirectory!);
- var mover = new Move(LoggerFactory, ReadFileSystem, WriteFileSystem, Set);
+ var mover = new Move(LoggerFactory, FileSystem.Read, FileSystem.Write, Set);
await mover.Execute("mover", "new-folder", true, TestContext.Current.CancellationToken);
mover.Changes.Should().HaveCount(2);
diff --git a/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs b/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs
index d6dfa1295c..fab4e2cedc 100644
--- a/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs
+++ b/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs
@@ -7,7 +7,6 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
using Elastic.Markdown.IO;
-using Nullean.ScopedFileSystem;
namespace Elastic.Markdown.Tests;
@@ -33,7 +32,7 @@ public async Task CreatesDefaultOutputDirectory()
});
await using var collector = new DiagnosticsCollector([]).StartAsync(TestContext.Current.CancellationToken);
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
- var context = new BuildContext(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext);
+ var context = new BuildContext(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext);
var linkResolver = new TestCrossLinkResolver();
var set = new DocumentationSet(context, logger, linkResolver);
var generator = new DocumentationGenerator(set, logger);
@@ -66,7 +65,7 @@ public void FilesWithSnippetsInNameNotTreatedAsSnippets()
});
var collector = new TestDiagnosticsCollector(output);
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
- var context = new BuildContext(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext);
+ var context = new BuildContext(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext);
var linkResolver = new TestCrossLinkResolver();
var set = new DocumentationSet(context, logger, linkResolver);
diff --git a/tests/Elastic.Markdown.Tests/RootIndexValidationTests.cs b/tests/Elastic.Markdown.Tests/RootIndexValidationTests.cs
index 4c29e609e7..20cf1f4b74 100644
--- a/tests/Elastic.Markdown.Tests/RootIndexValidationTests.cs
+++ b/tests/Elastic.Markdown.Tests/RootIndexValidationTests.cs
@@ -8,7 +8,6 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
using Elastic.Markdown.IO;
-using Nullean.ScopedFileSystem;
namespace Elastic.Markdown.Tests;
@@ -34,7 +33,7 @@ public void InternalRegistry_MissingIndexMd_EmitsError()
var collector = new TestDiagnosticsCollector(output);
_ = collector.StartAsync(TestContext.Current.CancellationToken);
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
- var context = new BuildContext(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext);
+ var context = new BuildContext(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext);
_ = new DocumentationSet(context, logger, new TestCrossLinkResolver());
collector.Errors.Should().BeGreaterThan(0);
@@ -65,7 +64,7 @@ public void InternalRegistry_WithIndexMd_NoError()
var collector = new TestDiagnosticsCollector(output);
_ = collector.StartAsync(TestContext.Current.CancellationToken);
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
- var context = new BuildContext(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext);
+ var context = new BuildContext(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext);
_ = new DocumentationSet(context, logger, new TestCrossLinkResolver());
collector.Diagnostics
@@ -93,7 +92,7 @@ public void PublicRegistry_MissingIndexMd_NoError()
var collector = new TestDiagnosticsCollector(output);
_ = collector.StartAsync(TestContext.Current.CancellationToken);
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
- var context = new BuildContext(collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem), configurationContext);
+ var context = new BuildContext(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext);
_ = new DocumentationSet(context, logger, new TestCrossLinkResolver());
collector.Diagnostics
diff --git a/tests/Elastic.Markdown.Tests/TestHelpers.cs b/tests/Elastic.Markdown.Tests/TestHelpers.cs
index 2aa37abeab..07296fc9c4 100644
--- a/tests/Elastic.Markdown.Tests/TestHelpers.cs
+++ b/tests/Elastic.Markdown.Tests/TestHelpers.cs
@@ -4,18 +4,39 @@
using System.Collections.Frozen;
using System.IO.Abstractions;
+using System.IO.Abstractions.TestingHelpers;
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.LegacyUrlMappings;
using Elastic.Documentation.Configuration.Products;
using Elastic.Documentation.Configuration.Search;
using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Versions;
namespace Elastic.Markdown.Tests;
public static class TestHelpers
{
+ ///
+ /// Resolves a over for tests
+ /// that only need a scoped FS without real git data. Ensures a stub .git directory exists
+ /// at the working-directory root (no config file) so
+ /// GitCheckoutInformationFactory.IsLegacyTestWithoutGitLayout returns the well-known
+ /// canned test instance instead of failing.
+ ///
+ public static DocumentationFileSystem CreateDocumentationFileSystem(
+ MockFileSystem fileSystem,
+ IDirectoryInfo? invocation = null,
+ GitCheckoutInformation? git = null)
+ {
+ var gitPath = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".git");
+ if (!fileSystem.Directory.Exists(gitPath))
+ fileSystem.Directory.CreateDirectory(gitPath);
+ invocation ??= fileSystem.DirectoryInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName, "docs"));
+ return DocumentationFileSystem.Resolve(invocation, new DocumentationScopeOptions { Inner = fileSystem, Git = git });
+ }
+
public static IConfigurationContext CreateConfigurationContext(IFileSystem fileSystem, VersionsConfiguration? versionsConfiguration = null, ProductsConfiguration? productsConfiguration = null)
{
versionsConfiguration ??= new VersionsConfiguration
@@ -115,7 +136,7 @@ public static IConfigurationContext CreateConfigurationContext(IFileSystem fileS
{
Elasticsearch = ElasticsearchEndpoint.Default,
},
- ConfigurationFileProvider = new ConfigurationFileProvider(new TestLoggerFactory(TestContext.Current.TestOutputHelper), fileSystem),
+ ConfigurationFileProvider = new ConfigurationFileProvider(new TestLoggerFactory(TestContext.Current.TestOutputHelper), new ConfigurationFileSystem(fileSystem)),
VersionsConfiguration = versionsConfiguration,
ProductsConfiguration = productsConfiguration,
SearchConfiguration = search,
diff --git a/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs
index 508b09cd2e..fca9bab7b9 100644
--- a/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs
+++ b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs
@@ -6,6 +6,7 @@
using Elastic.Documentation.Assembler.Navigation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Toc;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation.Assembler;
using Elastic.Documentation.Navigation.Isolated;
using Elastic.Documentation.Navigation.Isolated.Leaf;
@@ -38,7 +39,7 @@ public async Task MultipleSectionsFromSameRepository_UseContentHashesAndCacheByR
var docset = DocumentationSetFile.LoadAndResolve(
context.Collector,
fileSystem.FileInfo.New($"{repositoryPath}/docs/docset.yml"),
- FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
documentationSets.Add(
new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance));
}
@@ -101,7 +102,7 @@ public void ComplexNavigationWithMultipleNestedTocsAppliesPathPrefixToRootUrls()
? $"{repo.FullName}/docs/docset.yml"
: $"{repo.FullName}/docs/_docset.yml";
- var docset = DocumentationSetFile.LoadAndResolve(context.Collector, fileSystem.FileInfo.New(docsetPath), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var docset = DocumentationSetFile.LoadAndResolve(context.Collector, fileSystem.FileInfo.New(docsetPath), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var navigation = new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance);
documentationSets.Add(navigation);
@@ -179,7 +180,7 @@ public void DeeplyNestedNavigationMaintainsPathPrefixThroughoutHierarchy()
var platformContext = SiteNavigationTestFixture.CreateAssemblerContext(fileSystem, "/checkouts/current/platform", output);
var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector,
- fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var documentationSets = new List
{
@@ -230,7 +231,7 @@ public void FileNavigationLeafUrlsReflectPathPrefixInDeeplyNestedStructures()
var platformContext = SiteNavigationTestFixture.CreateAssemblerContext(fileSystem, "/checkouts/current/platform", output);
var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector,
- fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var documentationSets = new List
{
@@ -292,7 +293,7 @@ public void FolderNavigationWithinNestedTocsHasCorrectPathPrefix()
var platformContext = SiteNavigationTestFixture.CreateContext(
fileSystem, "/checkouts/current/platform", output);
var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector,
- fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var documentationSets = new List
{
diff --git a/tests/Navigation.Tests/Assembler/IdentifierCollectionTests.cs b/tests/Navigation.Tests/Assembler/IdentifierCollectionTests.cs
index 9936f9f569..8be34fddb3 100644
--- a/tests/Navigation.Tests/Assembler/IdentifierCollectionTests.cs
+++ b/tests/Navigation.Tests/Assembler/IdentifierCollectionTests.cs
@@ -5,6 +5,7 @@
using AwesomeAssertions;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Toc;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation.Isolated;
using Elastic.Documentation.Navigation.Isolated.Node;
@@ -21,7 +22,7 @@ public void DocumentationSetNavigationCollectsRootIdentifier()
var platformContext = SiteNavigationTestFixture.CreateContext(
fileSystem, "/checkouts/current/platform", output);
var platformDocset = DocumentationSetFile.LoadAndResolve(
- platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var platformNav = new DocumentationSetNavigation(platformDocset, platformContext, GenericDocumentationFileFactory.Instance);
// Root identifier should be ://
@@ -36,7 +37,7 @@ public void DocumentationSetNavigationCollectsNestedTocIdentifiers()
// Test platform repository with nested TOCs
var platformContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output);
- var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector, platformContext.ConfigurationPath, FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector, platformContext.ConfigurationPath, new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var platformNav = new DocumentationSetNavigation(platformDocset, platformContext, GenericDocumentationFileFactory.Instance);
// Should collect identifiers from nested TOCs
@@ -57,7 +58,7 @@ public void DocumentationSetNavigationWithSimpleStructure()
// Test observability repository (no nested TOCs)
var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
- var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var observabilityNav = new DocumentationSetNavigation(observabilityDocset, observabilityContext, GenericDocumentationFileFactory.Instance);
// Should only have root identifier
@@ -74,7 +75,7 @@ public void TableOfContentsNavigationHasCorrectIdentifier()
var platformContext = SiteNavigationTestFixture.CreateContext(
fileSystem, "/checkouts/current/platform", output);
var platformDocset = DocumentationSetFile.LoadAndResolve(
- platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var platformNav = new DocumentationSetNavigation(platformDocset, platformContext, GenericDocumentationFileFactory.Instance);
// Get the deployment-guide TOC
@@ -97,13 +98,13 @@ public void MultipleDocumentationSetsHaveDistinctIdentifiers()
var platformContext = SiteNavigationTestFixture.CreateContext(
fileSystem, "/checkouts/current/platform", output);
var platformDocset = DocumentationSetFile.LoadAndResolve(
- platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var platformNav = new DocumentationSetNavigation(platformDocset, platformContext, GenericDocumentationFileFactory.Instance);
var observabilityContext = SiteNavigationTestFixture.CreateContext(
fileSystem, "/checkouts/current/observability", output);
var observabilityDocset = DocumentationSetFile.LoadAndResolve(
- observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var observabilityNav = new DocumentationSetNavigation(observabilityDocset, observabilityContext, GenericDocumentationFileFactory.Instance);
// Each should have its own set of identifiers
diff --git a/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs b/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs
index 821abe8497..2861cdc71c 100644
--- a/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs
+++ b/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs
@@ -5,6 +5,7 @@
using AwesomeAssertions;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Toc;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation.Assembler;
using Elastic.Documentation.Navigation.Isolated;
using Elastic.Documentation.Navigation.Isolated.Leaf;
@@ -47,7 +48,7 @@ public void CreatesDocumentationSetNavigationsFromCheckoutFolders()
? $"{repo.FullName}/docs/docset.yml"
: $"{repo.FullName}/docs/_docset.yml";
- var docset = DocumentationSetFile.LoadAndResolve(context.Collector, fileSystem.FileInfo.New(docsetPath), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var docset = DocumentationSetFile.LoadAndResolve(context.Collector, fileSystem.FileInfo.New(docsetPath), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var navigation = new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance);
documentationSets.Add(navigation);
@@ -84,15 +85,15 @@ public void SiteNavigationIntegratesWithDocumentationSets()
var documentationSets = new List();
var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
- var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
documentationSets.Add(new DocumentationSetNavigation(observabilityDocset, observabilityContext, GenericDocumentationFileFactory.Instance));
var searchContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/serverless-search", output);
- var searchDocset = DocumentationSetFile.LoadAndResolve(searchContext.Collector, fileSystem.FileInfo.New("/checkouts/current/serverless-search/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var searchDocset = DocumentationSetFile.LoadAndResolve(searchContext.Collector, fileSystem.FileInfo.New("/checkouts/current/serverless-search/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
documentationSets.Add(new DocumentationSetNavigation(searchDocset, searchContext, GenericDocumentationFileFactory.Instance));
var securityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/serverless-security", output);
- var securityDocset = DocumentationSetFile.LoadAndResolve(securityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/serverless-security/docs/_docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var securityDocset = DocumentationSetFile.LoadAndResolve(securityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/serverless-security/docs/_docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
documentationSets.Add(new DocumentationSetNavigation(securityDocset, securityContext, GenericDocumentationFileFactory.Instance));
// Create site navigation context (using any repository's filesystem)
@@ -134,7 +135,7 @@ public void SiteNavigationWithNestedTocs()
// Create DocumentationSetNavigation for platform
var platformContext = SiteNavigationTestFixture.CreateAssemblerContext(fileSystem, "/checkouts/current/platform", output);
- var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var platformNav = new DocumentationSetNavigation(platformDocset, platformContext, GenericDocumentationFileFactory.Instance);
platformNav.Url.Should().Be("/");
platformNav.Index.Url.Should().Be("/");
@@ -201,7 +202,7 @@ public void SiteNavigationWithAllRepositories()
? $"{repo.FullName}/docs/docset.yml"
: $"{repo.FullName}/docs/_docset.yml";
- var docset = DocumentationSetFile.LoadAndResolve(context.Collector, fileSystem.FileInfo.New(docsetPath), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var docset = DocumentationSetFile.LoadAndResolve(context.Collector, fileSystem.FileInfo.New(docsetPath), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var navigation = new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance);
documentationSets.Add(navigation);
@@ -241,7 +242,7 @@ public void DocumentationSetNavigationHasCorrectStructure()
// Test observability repository structure
var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
- var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var observabilityNav = new DocumentationSetNavigation(observabilityDocset, observabilityContext, GenericDocumentationFileFactory.Instance);
observabilityNav.NavigationTitle.Should().Be(observabilityNav.NavigationTitle);
@@ -270,7 +271,7 @@ public void DocumentationSetWithNestedTocs()
// Test platform repository with nested TOCs
var platformContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output);
- var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var platformNav = new DocumentationSetNavigation(platformDocset, platformContext, GenericDocumentationFileFactory.Instance);
platformNav.NavigationTitle.Should().Be("Platform");
@@ -300,7 +301,7 @@ public void DocumentationSetWithUnderscoreDocset()
// Test serverless-security repository with _docset.yml
var securityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/serverless-security", output);
- var securityDocset = DocumentationSetFile.LoadAndResolve(securityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/serverless-security/docs/_docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var securityDocset = DocumentationSetFile.LoadAndResolve(securityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/serverless-security/docs/_docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var securityNav = new DocumentationSetNavigation(securityDocset, securityContext, GenericDocumentationFileFactory.Instance);
securityNav.NavigationTitle.Should().Be("Serverless Security");
@@ -331,7 +332,7 @@ public void SiteNavigationAppliesPathPrefixToAllUrls()
var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem();
var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
- var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var documentationSets = new List { new DocumentationSetNavigation(observabilityDocset, observabilityContext, GenericDocumentationFileFactory.Instance) };
var siteContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
@@ -368,7 +369,7 @@ public void SiteNavigationWithNestedTocsAppliesCorrectPathPrefixes()
var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem();
var platformContext = SiteNavigationTestFixture.CreateAssemblerContext(fileSystem, "/checkouts/current/platform", output);
- var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var documentationSets = new List { new DocumentationSetNavigation(platformDocset, platformContext, GenericDocumentationFileFactory.Instance) };
var siteContext = SiteNavigationTestFixture.CreateAssemblerContext(fileSystem, "/checkouts/current/platform", output);
@@ -399,7 +400,7 @@ public void SiteNavigationRequiresPathPrefix()
var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem();
var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
- var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var documentationSets = new List { new DocumentationSetNavigation(observabilityDocset, observabilityContext, GenericDocumentationFileFactory.Instance) };
var siteContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
@@ -422,7 +423,7 @@ public void ObservabilityDocumentationSetNavigationHasNoDiagnostics()
var context = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
var docsetPath = fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml");
- var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var navigation = new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance);
@@ -442,7 +443,7 @@ public void ServerlessSearchDocumentationSetNavigationHasNoDiagnostics()
var context = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/serverless-search", output);
var docsetPath = fileSystem.FileInfo.New("/checkouts/current/serverless-search/docs/docset.yml");
- var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var navigation = new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance);
@@ -462,7 +463,7 @@ public void ServerlessSecurityDocumentationSetNavigationHasNoDiagnostics()
var context = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/serverless-security", output);
var docsetPath = fileSystem.FileInfo.New("/checkouts/current/serverless-security/docs/_docset.yml");
- var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var navigation = new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance);
@@ -482,7 +483,7 @@ public void PlatformDocumentationSetNavigationHasNoDiagnostics()
var context = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output);
var docsetPath = fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml");
- var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var navigation = new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance);
@@ -502,7 +503,7 @@ public void ElasticsearchReferenceDocumentationSetNavigationHasNoDiagnostics()
var context = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/elasticsearch-reference", output);
var docsetPath = fileSystem.FileInfo.New("/checkouts/current/elasticsearch-reference/docs/docset.yml");
- var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var navigation = new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance);
@@ -533,7 +534,7 @@ public void AllDocumentationSetsHaveNoDiagnostics()
? $"{repo.FullName}/docs/docset.yml"
: $"{repo.FullName}/docs/_docset.yml";
- var docset = DocumentationSetFile.LoadAndResolve(context.Collector, fileSystem.FileInfo.New(docsetPath), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var docset = DocumentationSetFile.LoadAndResolve(context.Collector, fileSystem.FileInfo.New(docsetPath), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var navigation = new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance);
@@ -554,7 +555,7 @@ public void DocumentationSetNavigationWithNestedTocsHasNoDiagnostics()
var context = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output);
var docsetPath = fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml");
- var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var navigation = new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance);
@@ -580,7 +581,7 @@ public void DocumentationSetNavigationWithFoldersHasNoDiagnostics()
var context = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
var docsetPath = fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml");
- var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var docset = DocumentationSetFile.LoadAndResolve(context.Collector, docsetPath, new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var navigation = new DocumentationSetNavigation(docset, context, GenericDocumentationFileFactory.Instance);
diff --git a/tests/Navigation.Tests/Assembler/SiteNavigationTestFixture.cs b/tests/Navigation.Tests/Assembler/SiteNavigationTestFixture.cs
index a9555fb552..7a15d0e7a8 100644
--- a/tests/Navigation.Tests/Assembler/SiteNavigationTestFixture.cs
+++ b/tests/Navigation.Tests/Assembler/SiteNavigationTestFixture.cs
@@ -34,6 +34,7 @@ private static void SetupServerlessObservabilityRepository(MockFileSystem fileSy
{
var baseDir = "/checkouts/current/observability";
fileSystem.AddDirectory(baseDir);
+ fileSystem.AddDirectory($"{baseDir}/.git");
// Add docset.yml
// language=yaml
@@ -68,6 +69,7 @@ private static void SetupServerlessSearchRepository(MockFileSystem fileSystem)
{
var baseDir = "/checkouts/current/serverless-search";
fileSystem.AddDirectory(baseDir);
+ fileSystem.AddDirectory($"{baseDir}/.git");
// Add docset.yml
// language=yaml
@@ -100,6 +102,7 @@ private static void SetupServerlessSecurityRepository(MockFileSystem fileSystem)
{
var baseDir = "/checkouts/current/serverless-security";
fileSystem.AddDirectory(baseDir);
+ fileSystem.AddDirectory($"{baseDir}/.git");
// Add docset.yml with underscore prefix
// language=yaml
@@ -132,6 +135,7 @@ private static void SetupPlatformRepository(MockFileSystem fileSystem)
{
var baseDir = "/checkouts/current/platform";
fileSystem.AddDirectory(baseDir);
+ fileSystem.AddDirectory($"{baseDir}/.git");
// Add docset.yml
// language=yaml
@@ -186,6 +190,7 @@ private static void SetupElasticsearchReferenceRepository(MockFileSystem fileSys
{
var baseDir = "/checkouts/current/elasticsearch-reference";
fileSystem.AddDirectory(baseDir);
+ fileSystem.AddDirectory($"{baseDir}/.git");
// Add docset.yml
// language=yaml
diff --git a/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs b/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs
index c79316636d..b82724fffc 100644
--- a/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs
+++ b/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs
@@ -6,6 +6,7 @@
using AwesomeAssertions;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Toc;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation.Assembler;
using Elastic.Documentation.Navigation.Isolated;
using Elastic.Documentation.Navigation.Isolated.Node;
@@ -42,11 +43,11 @@ public void ConstructorCreatesSiteNavigation()
// Create DocumentationSetNavigation instances for the referenced repos
var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
- var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var observabilityNav = new DocumentationSetNavigation(observabilityDocset, observabilityContext, GenericDocumentationFileFactory.Instance);
var searchContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/serverless-search", output);
- var searchDocset = DocumentationSetFile.LoadAndResolve(searchContext.Collector, fileSystem.FileInfo.New("/checkouts/current/serverless-search/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var searchDocset = DocumentationSetFile.LoadAndResolve(searchContext.Collector, fileSystem.FileInfo.New("/checkouts/current/serverless-search/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var searchNav = new DocumentationSetNavigation(searchDocset, searchContext, GenericDocumentationFileFactory.Instance);
var documentationSets = new List { observabilityNav, searchNav };
@@ -80,7 +81,7 @@ public void SiteNavigationWithNestedChildren()
// Create DocumentationSetNavigation for platform
var platformContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform", output);
- var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var platformDocset = DocumentationSetFile.LoadAndResolve(platformContext.Collector, fileSystem.FileInfo.New("/checkouts/current/platform/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var platformNav = new DocumentationSetNavigation(platformDocset, platformContext, GenericDocumentationFileFactory.Instance);
var documentationSets = new List { platformNav };
@@ -118,7 +119,7 @@ public void SitePrefixNormalizesSlashes(string? sitePrefix, string expectedRootU
var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem();
var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
- var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var observabilityNav = new DocumentationSetNavigation(observabilityDocset, observabilityContext, GenericDocumentationFileFactory.Instance);
var documentationSets = new List { observabilityNav };
@@ -154,7 +155,7 @@ public void SitePrefixAppliedToNavigationItemUrls(string? sitePrefix, string exp
var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem();
var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
- var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var observabilityNav = new DocumentationSetNavigation(observabilityDocset, observabilityContext, GenericDocumentationFileFactory.Instance);
var documentationSets = new List { observabilityNav };
@@ -213,11 +214,11 @@ public void NavigationNodeIdsAreUniqueAcrossDocsets()
// Create navigation for both docsets
var productAContext = SiteNavigationTestFixture.CreateContext(fileSystem, productADir, output);
- var productADocsetFile = DocumentationSetFile.LoadAndResolve(productAContext.Collector, fileSystem.FileInfo.New($"{productADir}/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var productADocsetFile = DocumentationSetFile.LoadAndResolve(productAContext.Collector, fileSystem.FileInfo.New($"{productADir}/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var productANav = new DocumentationSetNavigation(productADocsetFile, productAContext, GenericDocumentationFileFactory.Instance);
var productBContext = SiteNavigationTestFixture.CreateContext(fileSystem, productBDir, output);
- var productBDocsetFile = DocumentationSetFile.LoadAndResolve(productBContext.Collector, fileSystem.FileInfo.New($"{productBDir}/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var productBDocsetFile = DocumentationSetFile.LoadAndResolve(productBContext.Collector, fileSystem.FileInfo.New($"{productBDir}/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var productBNav = new DocumentationSetNavigation(productBDocsetFile, productBContext, GenericDocumentationFileFactory.Instance);
// Get the "getting-started" folders from each docset
@@ -285,11 +286,11 @@ public void SitePrefixAppliedToMultipleNavigationItems(string? sitePrefix, strin
var fileSystem = SiteNavigationTestFixture.CreateMultiRepositoryFileSystem();
var observabilityContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability", output);
- var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var observabilityDocset = DocumentationSetFile.LoadAndResolve(observabilityContext.Collector, fileSystem.FileInfo.New("/checkouts/current/observability/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var observabilityNav = new DocumentationSetNavigation(observabilityDocset, observabilityContext, GenericDocumentationFileFactory.Instance);
var searchContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/serverless-search", output);
- var searchDocset = DocumentationSetFile.LoadAndResolve(searchContext.Collector, fileSystem.FileInfo.New("/checkouts/current/serverless-search/docs/docset.yml"), FileSystemFactory.ScopeSourceDirectory(fileSystem, "/checkouts"));
+ var searchDocset = DocumentationSetFile.LoadAndResolve(searchContext.Collector, fileSystem.FileInfo.New("/checkouts/current/serverless-search/docs/docset.yml"), new CheckoutsFileSystem(fileSystem.DirectoryInfo.New("/checkouts"), inner: fileSystem));
var searchNav = new DocumentationSetNavigation(searchDocset, searchContext, GenericDocumentationFileFactory.Instance);
var documentationSets = new List { observabilityNav, searchNav };
diff --git a/tests/Navigation.Tests/Codex/CodexConfigurationLoaderTests.cs b/tests/Navigation.Tests/Codex/CodexConfigurationLoaderTests.cs
index e18dc14018..7049993ca9 100644
--- a/tests/Navigation.Tests/Codex/CodexConfigurationLoaderTests.cs
+++ b/tests/Navigation.Tests/Codex/CodexConfigurationLoaderTests.cs
@@ -6,7 +6,7 @@
using AwesomeAssertions;
using Elastic.Codex;
using Elastic.Documentation.Configuration;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Documentation.Navigation.Tests.Codex;
@@ -21,8 +21,8 @@ public class CodexConfigurationLoaderTests(ITestOutputHelper output)
private static readonly string ConfigPath =
Path.Join(Paths.WorkingDirectoryRoot.FullName, "codex.yml");
- private ScopedFileSystem ScopedFs(MockFileSystem mockFs) =>
- FileSystemFactory.ScopeCurrentWorkingDirectory(mockFs);
+ private CheckoutsFileSystem ScopedFs(MockFileSystem mockFs) =>
+ CheckoutsFileSystem.FromWorkingDirectory(mockFs);
private TestDiagnosticsCollector Collector() => new(output);
diff --git a/tests/Navigation.Tests/Codex/CodexNavigationTestBase.cs b/tests/Navigation.Tests/Codex/CodexNavigationTestBase.cs
index ea50bacdde..cc5bbef219 100644
--- a/tests/Navigation.Tests/Codex/CodexNavigationTestBase.cs
+++ b/tests/Navigation.Tests/Codex/CodexNavigationTestBase.cs
@@ -9,9 +9,9 @@
using Elastic.Documentation.Configuration.Codex;
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation;
using Elastic.Documentation.Navigation.Isolated.Node;
-using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Navigation.Tests.Codex;
@@ -79,8 +79,7 @@ internal sealed class TestCodexDocumentationContext(IDiagnosticsCollector collec
public IFileInfo ConfigurationPath => _fileSystem.FileInfo.New(_fileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, "codex.yml"));
public IDiagnosticsCollector Collector => collector;
- public ScopedFileSystem ReadFileSystem => FileSystemFactory.ScopeCurrentWorkingDirectory(_fileSystem);
- public ScopedFileSystem WriteFileSystem => FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(_fileSystem);
+ public DocumentationWriteFileSystem WriteFileSystem => new(_fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), null, _fileSystem);
public IDirectoryInfo OutputDirectory => _fileSystem.DirectoryInfo.New(_fileSystem.Path.Join(Paths.ApplicationData.FullName, "codex", "output"));
public BuildType BuildType => BuildType.Codex;
diff --git a/tests/Navigation.Tests/Codex/FindDocsetFileTests.cs b/tests/Navigation.Tests/Codex/FindDocsetFileTests.cs
index 36f00b3902..d936af460f 100644
--- a/tests/Navigation.Tests/Codex/FindDocsetFileTests.cs
+++ b/tests/Navigation.Tests/Codex/FindDocsetFileTests.cs
@@ -6,7 +6,7 @@
using AwesomeAssertions;
using Elastic.Codex.Sourcing;
using Elastic.Documentation.Configuration;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Documentation.Navigation.Tests.Codex;
@@ -16,8 +16,8 @@ public class FindDocsetFileTests
private static readonly string RepoRoot = Path.Join(Paths.WorkingDirectoryRoot.FullName, "repo");
- private static ScopedFileSystem CreateScopedFs(MockFileSystem mockFs) =>
- FileSystemFactory.ScopeCurrentWorkingDirectory(mockFs);
+ private static CheckoutsFileSystem CreateScopedFs(MockFileSystem mockFs) =>
+ CheckoutsFileSystem.FromWorkingDirectory(mockFs);
[Fact]
public void StandardPath_Found()
diff --git a/tests/Navigation.Tests/Codex/GroupNavigationTests.cs b/tests/Navigation.Tests/Codex/GroupNavigationTests.cs
index f692922cdd..7a2de3d6b6 100644
--- a/tests/Navigation.Tests/Codex/GroupNavigationTests.cs
+++ b/tests/Navigation.Tests/Codex/GroupNavigationTests.cs
@@ -5,7 +5,7 @@
using AwesomeAssertions;
using Elastic.Codex.Navigation;
using Elastic.Documentation.Configuration;
-using Nullean.ScopedFileSystem;
+using Elastic.Documentation.FileSystems;
namespace Elastic.Documentation.Navigation.Tests.Codex;
@@ -138,8 +138,7 @@ private sealed class MinimalCodexContext : ICodexDocumentationContext
private readonly System.IO.Abstractions.TestingHelpers.MockFileSystem _fs = new();
public System.IO.Abstractions.IFileInfo ConfigurationPath => _fs.FileInfo.New("/codex.yml");
public Elastic.Documentation.Diagnostics.IDiagnosticsCollector Collector => new Elastic.Documentation.Diagnostics.DiagnosticsCollector([]);
- public ScopedFileSystem ReadFileSystem => FileSystemFactory.ScopeCurrentWorkingDirectory(_fs);
- public ScopedFileSystem WriteFileSystem => FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(_fs);
+ public DocumentationWriteFileSystem WriteFileSystem => new(_fs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), null, _fs);
public System.IO.Abstractions.IDirectoryInfo OutputDirectory => _fs.DirectoryInfo.New("/output");
public BuildType BuildType => BuildType.Codex;
public void EmitError(string message) { }
diff --git a/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs b/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs
index ea7bc41fbe..811ad530fe 100644
--- a/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs
+++ b/tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs
@@ -7,6 +7,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Navigation.Isolated.Leaf;
using Elastic.Documentation.Navigation.Isolated.Node;
using Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -28,7 +29,7 @@ public async Task PhysicalDocsetCanBeNavigated()
var configPath = fileSystem.FileInfo.New(docsetPath);
var context = new TestDocumentationSetContext(fileSystem, docsDir, outputDir, configPath, output, "docs-builder");
- var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, configPath, context.ReadFileSystem, noSuppress: [HintType.DeepLinkingVirtualFile]);
+ var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, configPath, context.ReadFileSystem as ScopedFileSystem, noSuppress: [HintType.DeepLinkingVirtualFile]);
_ = context.Collector.StartAsync(TestContext.Current.CancellationToken);
@@ -68,7 +69,7 @@ public async Task PhysicalDocsetNavigationHasCorrectUrls()
var configPath = fileSystem.FileInfo.New(docsetPath);
var context = new TestDocumentationSetContext(fileSystem, docsDir, outputDir, configPath, output, "docs-builder");
- var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, configPath, FileSystemFactory.RealGitRootForPath(null));
+ var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, configPath, DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName));
_ = context.Collector.StartAsync(TestContext.Current.CancellationToken);
var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance);
@@ -94,7 +95,7 @@ public async Task PhysicalDocsetNavigationIncludesNestedTocs()
var configPath = fileSystem.FileInfo.New(docsetPath);
var context = new TestDocumentationSetContext(fileSystem, docsDir, outputDir, configPath, output, "docs-builder");
- var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, configPath, FileSystemFactory.RealGitRootForPath(null));
+ var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, configPath, DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName));
_ = context.Collector.StartAsync(TestContext.Current.CancellationToken);
var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance);
@@ -126,7 +127,7 @@ public async Task PhysicalDocsetNavigationHandlesHiddenFiles()
var configPath = fileSystem.FileInfo.New(docsetPath);
var context = new TestDocumentationSetContext(fileSystem, docsDir, outputDir, configPath, output, "docs-builder");
- var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, configPath, FileSystemFactory.RealGitRootForPath(null));
+ var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, configPath, DocumentationFileSystem.Resolve(Paths.WorkingDirectoryRoot.FullName));
_ = context.Collector.StartAsync(TestContext.Current.CancellationToken);
var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance);
diff --git a/tests/Navigation.Tests/TestDocumentationSetContext.cs b/tests/Navigation.Tests/TestDocumentationSetContext.cs
index 37e5458055..b7c299cd00 100644
--- a/tests/Navigation.Tests/TestDocumentationSetContext.cs
+++ b/tests/Navigation.Tests/TestDocumentationSetContext.cs
@@ -8,13 +8,13 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Extensions;
+using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Links.CrossLinks;
using Elastic.Documentation.Navigation.Isolated;
using Markdig;
using Markdig.Parsers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
-using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Navigation.Tests;
@@ -83,8 +83,8 @@ public TestDocumentationSetContext(IFileSystem fileSystem,
TestDiagnosticsCollector? collector = null
)
{
- ReadFileSystem = FileSystemFactory.ScopeSourceDirectory(fileSystem, sourceDirectory.FullName);
- WriteFileSystem = FileSystemFactory.ScopeSourceDirectoryForWrite(fileSystem, outputDirectory.FullName);
+ ReadFileSystem = DocumentationFileSystem.Resolve(sourceDirectory, new DocumentationScopeOptions { Inner = fileSystem, ConfigurationFile = configPath.FullName });
+ WriteFileSystem = new DocumentationWriteFileSystem(sourceDirectory, outputDirectory, fileSystem);
DocumentationSourceDirectory = sourceDirectory;
OutputDirectory = outputDirectory;
ConfigurationPath = configPath;
@@ -102,8 +102,8 @@ public TestDocumentationSetContext(IFileSystem fileSystem,
}
public IDiagnosticsCollector Collector { get; }
- public ScopedFileSystem ReadFileSystem { get; }
- public ScopedFileSystem WriteFileSystem { get; }
+ public IDocumentationFileSystem ReadFileSystem { get; }
+ public DocumentationWriteFileSystem WriteFileSystem { get; }
public IDirectoryInfo OutputDirectory { get; }
public IDirectoryInfo DocumentationSourceDirectory { get; }
public GitCheckoutInformation Git { get; }
diff --git a/tests/authoring/Framework/CrossLinkResolverAssertions.fs b/tests/authoring/Framework/CrossLinkResolverAssertions.fs
index 53b55f939f..7d58587158 100644
--- a/tests/authoring/Framework/CrossLinkResolverAssertions.fs
+++ b/tests/authoring/Framework/CrossLinkResolverAssertions.fs
@@ -7,6 +7,7 @@ namespace authoring
open System
open System.Collections.Generic
open System.Collections.Frozen
+open System.IO.Abstractions
open System.IO.Abstractions.TestingHelpers
open Elastic.Documentation.Diagnostics
open Elastic.Documentation.Links
@@ -15,6 +16,7 @@ open Elastic.Documentation
open Swensen.Unquote
open Elastic.Documentation.Configuration
open Elastic.Documentation.Configuration.Builder
+open Elastic.Documentation.FileSystems
open authoring
module CrossLinkResolverAssertions =
@@ -31,8 +33,8 @@ module CrossLinkResolverAssertions =
member _.Collector = collector
member _.DocumentationSourceDirectory = mockFileSystem.DirectoryInfo.New("/docs")
member _.Git = GitCheckoutInformation.Unavailable
- member _.ReadFileSystem = FileSystemFactory.ScopeCurrentWorkingDirectory(mockFileSystem)
- member _.WriteFileSystem = FileSystemFactory.ScopeCurrentWorkingDirectory(mockFileSystem)
+ member _.ReadFileSystem = DocumentationFileSystem.Resolve(mockFileSystem.DirectoryInfo.New("/docs"), DocumentationScopeOptions(Inner = (mockFileSystem :> IFileSystem), ConfigurationFile = "/docs/docset.yml")) :> IDocumentationFileSystem
+ member _.WriteFileSystem = DocumentationWriteFileSystem(mockFileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName), null, mockFileSystem)
member _.ConfigurationPath = mockFileSystem.FileInfo.New("mock_docset.yml")
member _.OutputDirectory = mockFileSystem.DirectoryInfo.New(".artifacts")
member _.BuildType = BuildType.Isolated
diff --git a/tests/authoring/Framework/Setup.fs b/tests/authoring/Framework/Setup.fs
index 96605871f5..a62ca2458a 100644
--- a/tests/authoring/Framework/Setup.fs
+++ b/tests/authoring/Framework/Setup.fs
@@ -9,10 +9,12 @@ open System
open System.Collections.Frozen
open System.Collections.Generic
open System.IO
+open System.IO.Abstractions
open System.IO.Abstractions.TestingHelpers
open System.Threading.Tasks
open YamlDotNet.RepresentationModel
open Elastic.Documentation
+open Elastic.Documentation.FileSystems
open Elastic.Documentation.Versions
open Elastic.Documentation.Configuration
open Elastic.Documentation.Configuration.LegacyUrlMappings
@@ -467,7 +469,7 @@ type Setup =
DisplayName = "Elastic Cloud Control ECCTL",
VersioningSystem = versionConfig.VersioningSystems[VersioningSystemId.Ecctl]))
- let configurationFileProvider = ConfigurationFileProvider(new TestLoggerFactory(), fileSystem)
+ let configurationFileProvider = ConfigurationFileProvider(new TestLoggerFactory(), ConfigurationFileSystem(fileSystem))
let configurationContext = ConfigurationContext(
VersionsConfiguration = versionConfig,
ConfigurationFileProvider = configurationFileProvider,
@@ -479,9 +481,18 @@ type Setup =
LegacyUrlMappings = LegacyUrlMappingConfiguration(Mappings = []),
SearchConfiguration = SearchConfiguration(Synonyms = Array.empty, Rules = [], DiminishTerms = [])
)
+ // A bare .git directory (no config) anchors FindGitRoot so checkout = WorkingDirectoryRoot,
+ // which makes OutputDirectory = WorkingDirectoryRoot/.artifacts/docs/html (not docs/.artifacts).
+ // The missing config triggers IsLegacyTestWithoutGitLayout → canned data, but we override that
+ // with Git = Unavailable because authoring tests exercise rendering, not real git metadata.
+ let gitPath = Path.Combine(Paths.WorkingDirectoryRoot.FullName, ".git")
+ if not (fileSystem.Directory.Exists gitPath) then
+ fileSystem.Directory.CreateDirectory gitPath |> ignore
+ let invocation = fileSystem.DirectoryInfo.New(Path.Combine(Paths.WorkingDirectoryRoot.FullName, "docs/"))
+ let docFs = DocumentationFileSystem.Resolve(invocation, DocumentationScopeOptions(Inner = (fileSystem :> IFileSystem), Git = GitCheckoutInformation.Unavailable))
let context = BuildContext(
collector,
- FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem),
+ docFs,
configurationContext,
UrlPathPrefix = (options.UrlPathPrefix |> Option.defaultValue ""),
CanonicalBaseUrl = Uri("https://www.elastic.co/")
@@ -499,7 +510,7 @@ type Setup =
ConversionCollector= conversionCollector
Set = set
Generator = generator
- ReadFileSystem = fileSystem
+ ReadFileSystem = docFs :> IDocumentationFileSystem
WriteFileSystem = fileSystem
}
context.Bootstrap()
diff --git a/tests/authoring/Framework/TestValues.fs b/tests/authoring/Framework/TestValues.fs
index b2be8ff49a..4359970a3f 100644
--- a/tests/authoring/Framework/TestValues.fs
+++ b/tests/authoring/Framework/TestValues.fs
@@ -7,6 +7,7 @@ namespace authoring
open System
open System.Collections.Concurrent
open System.IO.Abstractions
+open Elastic.Documentation.FileSystems
open Elastic.Documentation.Diagnostics
open Elastic.Markdown
open Elastic.Markdown.IO
@@ -94,7 +95,7 @@ and MarkdownTestContext =
ConversionCollector: TestConversionCollector
Set: DocumentationSet
Generator: DocumentationGenerator
- ReadFileSystem: IFileSystem
+ ReadFileSystem: IDocumentationFileSystem
WriteFileSystem: IFileSystem
}