Skip to content

DM-55634: Fix the Edit on GitHub link's document path - #352

Merged
jonathansick merged 9 commits into
mainfrom
tickets/DM-55634
Jul 27, 2026
Merged

DM-55634: Fix the Edit on GitHub link's document path#352
jonathansick merged 9 commits into
mainfrom
tickets/DM-55634

Conversation

@jonathansick

@jonathansick jonathansick commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

  • The "Edit on GitHub" button linked to the wrong file whenever the Sphinx source directory differs from the directory holding conf.py. pydata-sphinx-theme joins doc_path with a source-relative file name, but doc_path was derived from the current working directory at conf.py exec time — the config directory. Projects that build with sphinx-build -c . docs _build/html therefore emitted URLs that 404: https://dp2.lsst.io/ links to .../edit/main/./index.rst instead of .../edit/main/docs/index.rst. A source directory at the repository root also produced a stray ./ in the URL.
  • The source directory isn't knowable in conf.py, so the computation moves into a new documenteer.ext.githubeditlink extension that runs at config-inited — the earliest event where srcdir is known, and the safest one for disabling the button (a theme may copy html_theme_options when the builder initializes, so a later change may not reach the template).
  • A doc_path set explicitly in html_context is honored and short-circuits auto-detection, so the button also keeps working outside a Git checkout when the path is supplied by hand.
  • Removing the conf-time Git access also fixes a hard failure: building a guide outside a Git checkout (an sdist, or a Docker image built without .git) raised InvalidGitRepositoryError. The button is now omitted instead, logged at info rather than warning level so that -W builds still pass. Five test roots no longer need show_github_edit_link = false to work around that, so they now cover the degradation path.

Validation steps

  • Built the real failing project against this branch and confirmed both the URL and that -W still passes:

    git clone https://github.com/lsst/dp2_lsst_io && cd dp2_lsst_io
    uv pip install '/path/to/documenteer[guide]' -r requirements.txt
    sphinx-build -b html -n -W -c . docs _build/html   # exits 0
    grep -o 'https://github.com/lsst/dp2_lsst_io/edit[^"]*' _build/html/index.html
    # -> https://github.com/lsst/dp2_lsst_io/edit/main/docs/index.rst

    A/B against released 2.4.0 on the same checkout: all 100 edit URLs change from /edit/main/./X to /edit/main/docs/X. Resolving each against the working tree, 2.4.0 gets 0/100 real files and this branch 100/100; GitHub returns 404 for the old form and 200 for the new. Button coverage (100 of 103 pages) and the warning-free -n -W build are unchanged.

  • Confirmed Documenteer's own docs (where confdir == srcdir) are unchanged — nox -s docs, then check docs/_build/html/guides/toml-reference.html still links to .../edit/main/docs/guides/toml-reference.rst.

  • Review the new docs/sphinx-extensions/github-edit-link.rst page and the show_github_edit_link note in the documenteer.toml reference.

References

Computes a path's POSIX-style location within the Git working tree,
normalizing the two cases that matter for building a URL from the result:
the repository root yields an empty string rather than ".", and both
sides are resolved so a symlinked path still compares as inside the
tree.
pydata-sphinx-theme joins doc_path with a source-directory-relative file
name, so doc_path must be the srcdir's path in the working tree. It was
derived instead from the current working directory at conf.py exec time,
which is the *config* directory. Those differ under 'sphinx-build -c .
docs ...', where the resulting URLs 404, and a srcdir at the repository
root produced a stray './' in the URL.

The srcdir isn't knowable in conf.py, so the computation moves to a new
config-inited handler. That event is both the earliest point where the
srcdir is known and the last at which the button can still be disabled,
since builder.init() resolves the theme options before builder-inited.

Dropping the conf-time Git access also means a build outside a Git
checkout no longer raises InvalidGitRepositoryError; the button is
omitted instead, and only at info level so that '-W' builds still pass.
The test roots no longer need show_github_edit_link = false to avoid
that hard failure, so they now cover the degradation path.
Adds a sphinx-extensions page covering the URL template, why doc_path
must be srcdir-relative, and the non-git degradation, plus a note in the
documenteer.toml reference for show_github_edit_link.
The config-inited handler runs after conf.py, so unconditionally setting
html_context['doc_path'] made the value impossible to override -- a
regression against the previous conf-time behavior for anyone who had
already worked around the wrong-path bug by hand.

A configured path now short-circuits auto-detection entirely, so it also
keeps the button working outside a Git checkout: supplying the path
answers the question the working tree would have been consulted for.
This is the escape hatch for layouts a working-tree path can't describe,
such as a generated source directory.
The existing cases drive a hand-written conf.py, so removing the
extension from the guide preset's extensions list, or breaking
set_edit_on_github's enable path, passed the whole suite. This case
builds documenteer.conf.guide itself in the dp2 layout; verified that it
fails when the extension is unregistered.

The sources are committed to Git because the preset auto-loads
sphinx-last-updated-by-git, which strips 'sourcename' for untracked
files -- and pydata's edit-this-page component is gated on it, so an
uncommitted page renders no button regardless of doc_path.
Relaxing the five test roots removed the only execution of the
show_github_edit_link = false early return, and the missing-github_url
ConfigError had no coverage either. Both guards survived the move to the
extension, so pin them directly, along with the fact that this method no
longer sets doc_path.
The doc_path-is-None branch warns rather than logging at info, on the
grounds that a repository whose working tree excludes the srcdir is a
real misconfiguration. It had no coverage, so pin it with a mock.

The warning-stream filter matched any 'is already registered' line.
Anchor it on the re-setup of a built-in sphinx.* extension instead,
which is the actual cross-application artifact -- note that this noise
covers re-registered directives and roles too, not just node classes, so
a narrower match on the node message alone does not hold.
The claim that config-inited is the last event at which the button can
be disabled is too strong: on Sphinx 8.2 and 9.1 the HTML builder's
get_theme_config returns html_theme_options itself, so builder-inited
would still work there. The real reason is that a theme *may* copy the
options when the builder initializes -- pydata's own
get_theme_options_dict documents that ambiguity -- so settling it at
config-inited avoids depending on the version's behavior.

Also corrects 'silently omitted' in the TOML reference, which described
the pre-review behavior; the omission is logged at info level.
Verified empirically: outside a Git checkout the guide build now exits 0
and omits the button, but sphinx-last-updated-by-git -- auto-loaded via
sphinx-sitemap -- still warns 'Error getting data from Git
[git.subprocess_error]', so a -W build still fails. Rubin projects build
with -n -W, so the unqualified 'the build proceeds' was misleading
exactly where it mattered.

None of that extension's own config values gate the warning; the fix
available to a project is suppress_warnings, which is now documented
rather than claimed away.
@jonathansick
jonathansick merged commit 4661195 into main Jul 27, 2026
12 checks passed
@jonathansick
jonathansick deleted the tickets/DM-55634 branch July 27, 2026 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant