Fix site name and git info when --path points to docs subfolder - #3789
Open
Mpdreamz wants to merge 9 commits into
Open
Fix site name and git info when --path points to docs subfolder#3789Mpdreamz wants to merge 9 commits into
Mpdreamz wants to merge 9 commits into
Conversation
reakaleek
approved these changes
Aug 6, 2026
Mpdreamz
force-pushed
the
fix/path-unknown-docs-name
branch
from
August 6, 2026 12:54
07d9981 to
8da6efd
Compare
When running `docs-builder serve --path ~/Projects/docs-eng-team/docs/`,
FindGitRoot ceiling correctly stops at rootFolder (the docs folder), so
DocumentationCheckoutDirectory is null for local direct-path invocations.
Previously the Name fallback was `unknown-{sourceDir.Name}` (e.g. unknown-docs).
The fix: fall back to the parent directory name, which is the repository clone
directory — giving the expected name (e.g. docs-eng-team) without touching
the intentional null-ceiling behavior that tests validate.
Double-chevron: island entries in the parent nav show >> (icon-chevron-double-down
in the SVG sprite, rotated right by nav-chevron CSS) to signal they open a
sub-navigation rather than expand an inline subtree.
NavigationRenderNodeKind: add Island as a dedicated kind so the template has a
clean three-way branch (Leaf / Island / Node) instead of a Node kind with a
separate IsIslandListing boolean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mpdreamz
force-pushed
the
fix/path-unknown-docs-name
branch
from
August 6, 2026 13:01
8da6efd to
3b7d3d1
Compare
…stable git checkout
Replace the broken ceiling-based git-root walk with a maxParents bound anchored at
the docset directory, making --path repo/ and --path repo/docs/ resolve to the same
DocumentationCheckoutDirectory. Fix GitCheckoutInformationFactory to stop escaping
ScopedFileSystem, use TryReadGitDirPointer (handling commondir), and never emit a
random GUID as a git ref. Move the #if DEBUG *.slnx relaxation into FindGitRoot
where the depth policy belongs.
Changes:
- Paths.FindGitRoot(IDirectoryInfo, int maxParents=1): replace ceiling param with
maxParents so the walk is always bounded, docset-anchored, and the DEBUG *.slnx
relaxation lives in one place
- Paths.TryReadGitDirPointer: single implementation of gitdir parsing that handles
StartsWith("gitdir:"), relative paths resolved against the .git file directory
(not CWD), and commondir following for nested worktrees
- GitCheckoutInformationFactory: use TryReadGitDirPointer, route all probes through
guarded fileSystem.File/Directory.Exists members, eliminate fakeRef (failed HEAD
-> Unavailable), invert mock shortcircuit (attempt real resolution first; canned
fallback only when .git is absent or a bare dir with no config)
- BuildContext: FindGitRoot(DocumentationSourceDirectory, maxParents:1) instead of
ceiling:rootFolder; removes gitRoot ?? .Parent fallback added in previous commit
- FindGitRootTests: port ceiling: cases to maxParents:; add WorktreeGitFile_OneLevel
- BuildContextDocumentationCheckoutDirectoryTests: SourceAsDocsSubtreeOnly now
asserts CheckoutDirectory == repoPath (was null); add PathAndDocsSubfolder_ResolveIdenticalCheckout
- GitCheckoutResolutionTests: new suite covering regular repo, detached HEAD,
worktree with absolute/relative gitdir, commondir, missing gitdir, canned fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the six-step bootstrap resolution order as the record's <summary>: 1. <path> = --path ?? cwd 2. SourceDirectory = docset scan (the anchor) 3. CheckoutDirectory = --git-dir?.Parent ?? FindGitRoot(maxParents) ?? error (required) 4. GitDirectories = real .git dirs for scope widening (worktrees) 5. Git = GitCheckoutInformation resolved once, never re-derived 6. OutputDirectory = --output ?? <path>/.artifacts/docs/html Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Delete Paths.FindGitRoot(string startPath): raw System.IO bootstrap overload
is no longer needed; the docset anchor is always resolved first through
IFileSystem, so the IDirectoryInfo overload covers every call site.
- FileSystemFactory.InMemoryForPath / RealGitRootForPath / RealGitRootForPathWrite:
switch from the deleted string overload to new FileSystem().DirectoryInfo.New()
+ FindGitRoot(IDirectoryInfo, maxParents). RealGitRootForPath also replaces the
ad-hoc worktree resolution (Replace/Path.Join('..','..')) with TryReadGitDirPointer,
which handles relative gitdir paths and commondir correctly.
- Codex commands (CodexCommands x3, CodexIndexCommand, CodexUpdateRedirectsCommand):
replace Paths.FindGitRoot(config.FullName) with the IDirectoryInfo overload via
plain.DirectoryInfo.New(config.DirectoryName!).
- GitCheckoutInformationFactory: extract the mock fallback condition into
IsLegacyTestWithoutGitLayout() with a xmldoc summary that explains in clear human
terms that this exists for test setups predating testable git resolution.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Why
Every consumer previously received a non-descriptive ScopedFileSystem.
The type communicated nothing about what it scoped or whether it was a
read or write scope. BuildContext.ReadFileSystem and WriteFileSystem were
both typed ScopedFileSystem, so nothing prevented callers from passing a
read scope as a write scope — and several test sites did exactly that.
Root cause: the filesystem factory pattern (FileSystemFactory) encoded
scope policy as static helper methods and ambient statics (RealRead,
RealWrite, AppData). These statics made the dependency invisible in
signatures and impossible to stub per-invocation. The underlying bug
(serve --path repo/docs showing 'unknown-docs') was caused by checkout
resolution being scattered across three implementations that disagreed
on where to walk.
## New type hierarchy (src/Elastic.Documentation.Tooling/FileSystems/ and
src/Elastic.Documentation/FileSystems/)
DocumentationFileSystem — read scope for a single documentation set.
Constructed via DocumentationFileSystem.Resolve(invocation, options).
Runs the six-step bootstrap (invocation → docset anchor → checkout →
git dirs → git info → output) and exposes the result as .Paths.
Owns .Write (a DocumentationWriteFileSystem) derived from the same
resolved paths, so read and write can never disagree about the checkout.
DocumentationWriteFileSystem — write scope for a documentation build.
Identical roots to the read scope but .git is excluded from the
allow-list. Constructed from the same ResolvedDocumentationPaths.
CheckoutsFileSystem — read/write scope over a directory of clones.
Used by the assembler, which operates on a tree of cloned repos rather
than a single documentation set. No docset anchoring, no git resolution.
ApplicationDataFileSystem — scope over the OS application-data directory.
Used for the codex link-index clone. No per-invocation inputs.
DocsetScanFileSystem — bootstrap-only scope rooted at the invocation
path, nothing above it. Used by DocumentationPathsResolver step 1-2
to locate docset.yml without being able to escape the invocation root.
GitResolveFileSystem — bootstrap-only scope rooted maxParents above the
docset anchor (not the invocation). Used by steps 3-5 to walk up to
.git and then read config/HEAD from resolved git directories (including
worktree commondir targets that lie outside the anchor's ancestry).
## What changed
DocumentationPathsResolver.Resolve implements the ordered six steps that
were previously scattered across BuildContext's constructor, FileSystemFactory,
and three separate git-resolution paths. Each step creates its own minimal
bootstrap scope and discards it; the final DocumentationFileSystem carries
only the resolved paths.
BuildContext stores one DocumentationFileSystem and computes ReadFileSystem,
WriteFileSystem, DocumentationSourceDirectory, DocumentationCheckoutDirectory,
ConfigurationPath, OutputDirectory, and Git as projections of it. Nothing is
copied independently, so nothing can drift out of agreement.
The legacy ScopedFileSystem pair constructor on BuildContext is deleted.
All seven src/ callers are migrated to DocumentationFileSystem.Resolve.
DocumentationScopeOptions replaces seven separate constructor parameters:
Output (--output), GitDir (--git-dir), ConfigurationFile (pre-discovered
docset), Git (pre-computed override for Assembler/Codex), ExtraRoots
(RUNNER_TEMP etc.), MaxParents, Inner (mock seam), InnerWrite (separate
write mock for navigation tests).
Worktree resolution fix: ResolveGitDirectories now uses the unscoped inner
filesystem for commondir traversal, because the main .git directory lies
outside the gitScope root by design. Explicit --git-dir carries its path
into gitDirectories directly so the factory scope covers it.
## Tests
DocumentationPathsResolverTests — 21 new tests covering:
- Normal repo: --path /repo and --path /repo/docs resolve same checkout
- Git worktree: checkout = worktree root, gitDirectories includes both
the .git pointer path and the resolved commondir target
- Explicit --git-dir: checkout = gitDir.Parent, git info from override
- Mock FS without .git: graceful fallback, checkout = source
- Output defaults to checkout/.artifacts (not invocation), same for
both --path /repo and --path /repo/docs (regression guard)
Nullean.ScopedFileSystem bumped to 0.4.1-canary.0.2 (local package) for
the TryValidateSymlinkAccess early-exit fix needed when docRoot == directory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PC5GiyZKhreYSqf2grz7pi
… source 0.4.2 ships the TryValidateSymlinkAccess early-exit fix from nullean/scoped-filesystem#12 as a proper release. The canary pin and nuget.config local-dev source are no longer needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PC5GiyZKhreYSqf2grz7pi
On CI the checkouts directory is stored inside the OS application-data directory (e.g. /home/runner/.local/share/elastic/docs-builder/checkouts/ nested inside /home/runner/.local/share/elastic/docs-builder), so adding both as ScopedFileSystem roots throws 'Scope roots must be disjoint'. Apply the same ancestor/descendant filter to the AppData root that was already applied to extraRoots: if AppData is a parent or child of the checkouts root, skip it (the narrower root already covers the needed access). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PC5GiyZKhreYSqf2grz7pi
…utsFileSystem - CodexCommands (CloneAndBuild, Clone, Build): replace bare ScopeCurrentWorkingDirectory with CheckoutsFileSystem anchored at WorkingDirectoryRoot, config's git root as extraRoot - CodexCommands.Serve: remove filesystem entirely; use Path.Join directly (no file I/O) - CodexIndexCommand: same CheckoutsFileSystem migration - CodexSyncCommand.LoadContext: replace RealRead with CheckoutsFileSystem, add config git root as extraRoot so --config outside CWD is no longer silently out of scope - CodexUpdateRedirectsCommand: same CheckoutsFileSystem migration - DeployCommands.Plan/Apply: single CheckoutsFileSystem.FromWorkingDirectory() per method instead of two separate instances (one for .Read, one for .Write) - DeployCommands.UpdateRedirects: RealRead -> CheckoutsFileSystem.FromWorkingDirectory() - ConfigurationCommands.Init (assembler init): RealRead -> CheckoutsFileSystem.FromWorkingDirectory() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PC5GiyZKhreYSqf2grz7pi
Same issue as CheckoutsFileSystem: on CI each docset checkout lives inside AppData (/home/runner/.local/share/.../checkouts/current/<repo>), so unconditionally adding AppData as a second scope root trips ValidateRootsAreDisjoint when checkout is a sub-path of AppData. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PC5GiyZKhreYSqf2grz7pi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Running
docs-builder serve --path ~/project/docs/showed unknown-docs as the site title and an empty branch/commit panel instead of the repository name and branch.Root cause (original bug):
Paths.FindGitRoot(DocumentationSourceDirectory, ceiling: rootFolder). When--pathpoints to the docs subfolder,rootFolder == docsPathso the ceiling locked the git-root walk inside that folder and could never see.gitone level up —CheckoutDirectorywas null, git info was Unavailable, and the name fell back tounknown-docs.Root cause (design):
FileSystemFactoryexposed ambient statics (RealRead,RealWrite,AppData) that any code could reach for without declaring the dependency in its signature. The documentation scope depends on--pathand--output, which are per-invocation — a singleton can never supply it correctly. Every command that calledFileSystemFactory.RealReadsilently coupled itself to the process CWD, making the scope invisible and untestable.The statics also made the read/write distinction unenforceable.
BuildContext.ReadFileSystemandBuildContext.WriteFileSystemwere both typedScopedFileSystem— a type that says "something scoped me" and nothing more. Nothing in the type system prevented passing a read scope as a write scope, and several test sites did exactly that. Git checkout resolution was scattered across three divergent implementations that could disagree on where the checkout was.What
Strongly-typed, intent-carrying filesystems
The core idea: a type that accepts
ScopedFileSystemaccepts any scope. A caller looking at aScopedFileSystemparameter has no idea whether it expects a read scope, a write scope, or whether.gitaccess is allowed. The name in the variable —readFs,writeFs— is the only signal, and names aren't checked by the compiler.Named subclasses fix this. A parameter typed
DocumentationWriteFileSystemrejects a read scope at compile time. A parameter typedCheckoutsFileSystemcan't be satisfied by aDocumentationFileSystemfor a single docs set. The type carries the intent the bareScopedFileSystemcould not.Call-site types — what consumers construct and pass around:
DocumentationFileSystemsrc/Elastic.Documentation.Tooling/FileSystems/DocumentationFileSystem.Resolve(invocation, options). Runs the six-step bootstrap (invocation → docset anchor → checkout → git dirs → git info → output), exposes the result as.Paths, and is the read scope. Callers use it asfs.Readandfs.Write—.Readis the instance itself;.Writeis aDocumentationWriteFileSystemproduced during construction, never separately instantiated.DocumentationWriteFileSystemsrc/Elastic.Documentation/FileSystems/DocumentationFileSystem, accessed via.Write. Same checkout roots as.Readbut.gitis excluded from the allowlist — writing into.gitis structurally impossible. Because both scopes come from the sameResolvedDocumentationPaths, read and write can never disagree about the checkout or output path.CheckoutsFileSystemsrc/Elastic.Documentation.Tooling/FileSystems/.Read(itself) and.Writefollowing the same pattern.ApplicationDataFileSystemsrc/Elastic.Documentation.Tooling/FileSystems/Bootstrap helpers — instantiated internally by
DocumentationPathsResolver, not by callers:DocsetScanFileSystemsrc/Elastic.Documentation.Tooling/FileSystems/docset.yml/_docset.ymlin steps 1–2 without escaping the invocation root. Discarded once the anchor is found.GitResolveFileSystemsrc/Elastic.Documentation.Tooling/FileSystems/maxParentsabove the docset anchor (not the invocation). Walks up to.gitand readsconfig/HEADfrom resolved git directories, including worktreecommondirtargets outside the anchor's ancestry, in steps 3–5. Discarded once git info is resolved.Statics removed
FileSystemFactory.RealRead,RealWrite, andAppDataare gone. Statics made the filesystem dependency invisible in signatures — a type that reached forFileSystemFactory.RealReaddeclared no dependency and could not be tested with a mock. Because the documentation scope is per-invocation (depends on--pathand--output), a singleton was always wrong: it hardcoded the process CWD as the checkout root, which is exactly the bug that caused--path repo/docsto showunknown-docs.With the statics gone, every consumer that wants a filesystem must declare it. The dependency is visible, the scope is per-invocation, and the type tells the reader what the scope is for.
Missing
.gitis now a hard error with a remedyPreviously, failing to find
.gitsilently produced anullCheckoutDirectory, which then caused the name fallback tounknown-docsand empty git info downstream — the symptom was observable only at render time with no pointer to the cause.Now
DocumentationPathsResolverthrowsDocumentationPathExceptionimmediately:The message names the flag that fixes it.
--git-diraccepts the path to the.gitdirectory; its parent becomes the checkout. Worktrees are handled automatically throughcommondir—--git-dirshould point at the worktree's own.gitfile's location, not the shared object store.Ordered six-step bootstrap (
DocumentationPathsResolver)Previously scattered across
BuildContext's constructor,FileSystemFactory, and three separate git-resolution paths. Now a single static class with one method per step, each creating its own minimal scope and discarding it:--pathargument or CWD.docset.yml/_docset.yml(skipped whenConfigurationFileis pre-supplied). UsesDocsetScanFileSystem.--git-dir?.Parent??FindGitRoot(anchor, maxParents)?? hard error naming--git-dir. UsesGitResolveFileSystem..gitpath(s) needed in scope. For worktrees:checkout/.git(pointer file) + thecommondirtarget resolved via the unscoped inner filesystem.GitCheckoutInformationFactory.Createthrough a scope widened by step 4. OrDocumentationScopeOptions.Gitif the caller pre-computed it (Assembler, Codex).--outputargument orcheckout/.artifacts/docs/html(anchored at the checkout, never the invocation, so--path /repoand--path /repo/docsboth write to/repo/.artifacts).BuildContextsimplificationStores one
DocumentationFileSystemand computesReadFileSystem,WriteFileSystem,DocumentationSourceDirectory,DocumentationCheckoutDirectory,ConfigurationPath,OutputDirectory, andGitas projections of.Paths. Nothing is copied independently, so nothing can drift out of agreement. The legacyScopedFileSystempair constructor is deleted; all sevensrc/callers are migrated toDocumentationFileSystem.Resolve.Worktree resolution fix
ResolveGitDirectoriesnow uses the unscoped inner filesystem forcommondirtraversal, because the main.gitdirectory lies outside the gitScope root by design. Explicit--git-dirinjects its path intogitDirectoriesdirectly so the factory scope covers it.Test plan
DocumentationPathsResolverTests— 21 new tests:--path /repoand--path /repo/docs→ sameCheckoutDirectoryandSourceDirectorycheckout= worktree root,GitDirectoriesincludes the.gitpointer path and resolvedcommondirtarget, git info resolved from main.git--path /worktree/docs→ same checkout as--path /worktree--git-dir: checkout = gitDir.Parent, git info from overridden dir.git: graceful fallback,checkout = source,Gitoverride preservedcheckout/.artifacts(regression guard: same for--path /repoand--path /repo/docs)--outputoverride respectedConfigurationFileskips docset scanDocumentationPathExceptionDocumentationFileSystem.Resolveintegration for regular repo and worktreeGitCheckoutResolutionTests— regular repo, detached HEAD, worktree absolute/relative gitdir, commondir, missing gitdir, canned-fallback guardFindGitRootTests— maxParents cases, worktree pointerBuildContextDocumentationCheckoutDirectoryTests—--path /repoand--path /repo/docsconverge./build.sh unit-test— 4332 tests pass (0 failures)./build.sh lint— cleanserve --path ~/Projects/docs-eng-team/docs— header shows repo name, branch/commit populatedserve --path ~/Projects/docs-eng-team/— identical to aboveservefrom inside the repo — identicalgit worktreecheckout🤖 Generated with Claude Code
https://claude.ai/code/session_01PC5GiyZKhreYSqf2grz7pi