Merge branch 'renames' into docs-mw

From Vegard Nossum:

When we rename an .rst file, that also changes the URL for the document
at https://docs.kernel.org/ and results in a 404, which can be anonying
for people who bookmark URLs and/or follow links from search engines
and old changelogs and emails.

In order to be able to fearlessly rename individual documentation files
and reorganize Documentation/, add two scripts:

- tools/docs/gen-renames.py : use git to figure out which .rst files
  have been renamed

- tools/docs/gen-redirects.py : actually generate .html stubs for the
  locations, redirecting to the new locations

The reason for splitting this into two is that trawling git history is
slightly slow (on the order of 20-30 seconds on my laptop) whereas just
generating the HTML files is very fast. This also allows us to cache
the historical renames in Documentation/.renames.txt or add manual
fixups as needed.
This commit is contained in:
Jonathan Corbet
2025-09-09 13:40:35 -06:00
5 changed files with 1382 additions and 2 deletions

1191
Documentation/.renames.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -108,6 +108,9 @@ htmldocs:
@$(srctree)/scripts/sphinx-pre-install --version-check
@+$(foreach var,$(SPHINXDIRS),$(call loop_cmd,sphinx,html,$(var),,$(var)))
htmldocs-redirects: $(srctree)/Documentation/.renames.txt
@tools/docs/gen-redirects.py --output $(BUILDDIR) < $<
# If Rust support is available and .config exists, add rustdoc generated contents.
# If there are any, the errors from this make rustdoc will be displayed but
# won't stop the execution of htmldocs
@@ -175,6 +178,7 @@ cleandocs:
dochelp:
@echo ' Linux kernel internal documentation in different formats from ReST:'
@echo ' htmldocs - HTML'
@echo ' htmldocs-redirects - generate HTML redirects for moved pages'
@echo ' texinfodocs - Texinfo'
@echo ' infodocs - Info'
@echo ' latexdocs - LaTeX'

View File

@@ -1799,8 +1799,9 @@ $(help-board-dirs): help-%:
# Documentation targets
# ---------------------------------------------------------------------------
DOC_TARGETS := xmldocs latexdocs pdfdocs htmldocs epubdocs cleandocs \
linkcheckdocs dochelp refcheckdocs texinfodocs infodocs
DOC_TARGETS := xmldocs latexdocs pdfdocs htmldocs htmldocs-redirects \
epubdocs cleandocs linkcheckdocs dochelp refcheckdocs \
texinfodocs infodocs
PHONY += $(DOC_TARGETS)
$(DOC_TARGETS):
$(Q)$(MAKE) $(build)=Documentation $@

54
tools/docs/gen-redirects.py Executable file
View File

@@ -0,0 +1,54 @@
#! /usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
#
# Copyright © 2025, Oracle and/or its affiliates.
# Author: Vegard Nossum <vegard.nossum@oracle.com>
"""Generate HTML redirects for renamed Documentation/**.rst files using
the output of tools/docs/gen-renames.py.
Example:
tools/docs/gen-redirects.py --output Documentation/output/ < Documentation/.renames.txt
"""
import argparse
import os
import sys
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-o', '--output', help='output directory')
args = parser.parse_args()
for line in sys.stdin:
line = line.rstrip('\n')
old_name, new_name = line.split(' ', 2)
old_html_path = os.path.join(args.output, old_name + '.html')
new_html_path = os.path.join(args.output, new_name + '.html')
if not os.path.exists(new_html_path):
print(f"warning: target does not exist: {new_html_path} (redirect from {old_html_path})")
continue
old_html_dir = os.path.dirname(old_html_path)
if not os.path.exists(old_html_dir):
os.makedirs(old_html_dir)
relpath = os.path.relpath(new_name, os.path.dirname(old_name)) + '.html'
with open(old_html_path, 'w') as f:
print(f"""\
<!DOCTYPE html>
<html lang="en">
<head>
<title>This page has moved</title>
<meta http-equiv="refresh" content="0; url={relpath}">
</head>
<body>
<p>This page has moved to <a href="{relpath}">{new_name}</a>.</p>
</body>
</html>""", file=f)

130
tools/docs/gen-renames.py Executable file
View File

@@ -0,0 +1,130 @@
#! /usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
#
# Copyright © 2025, Oracle and/or its affiliates.
# Author: Vegard Nossum <vegard.nossum@oracle.com>
"""Trawl repository history for renames of Documentation/**.rst files.
Example:
tools/docs/gen-renames.py --rev HEAD > Documentation/.renames.txt
"""
import argparse
import itertools
import os
import subprocess
import sys
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--rev', default='HEAD', help='generate renames up to this revision')
args = parser.parse_args()
def normalize(path):
prefix = 'Documentation/'
suffix = '.rst'
assert path.startswith(prefix)
assert path.endswith(suffix)
return path[len(prefix):-len(suffix)]
class Name(object):
def __init__(self, name):
self.names = [name]
def rename(self, new_name):
self.names.append(new_name)
names = {
}
for line in subprocess.check_output([
'git', 'log',
'--reverse',
'--oneline',
'--find-renames',
'--diff-filter=RD',
'--name-status',
'--format=commit %H',
# ~v4.8-ish is when Sphinx/.rst was added in the first place
f'v4.8..{args.rev}',
'--',
'Documentation/'
], text=True).splitlines():
# rename
if line.startswith('R'):
_, old, new = line[1:].split('\t', 2)
if old.endswith('.rst') and new.endswith('.rst'):
old = normalize(old)
new = normalize(new)
name = names.get(old)
if name is None:
name = Name(old)
else:
del names[old]
name.rename(new)
names[new] = name
continue
# delete
if line.startswith('D'):
_, old = line.split('\t', 1)
if old.endswith('.rst'):
old = normalize(old)
# TODO: we could save added/modified files as well and propose
# them as alternatives
name = names.get(old)
if name is None:
pass
else:
del names[old]
continue
#
# Get the set of current files so we can sanity check that we aren't
# redirecting any of those
#
current_files = set()
for line in subprocess.check_output([
'git', 'ls-tree',
'-r',
'--name-only',
args.rev,
'Documentation/',
], text=True).splitlines():
if line.endswith('.rst'):
current_files.add(normalize(line))
#
# Format/group/output result
#
result = []
for _, v in names.items():
old_names = v.names[:-1]
new_name = v.names[-1]
for old_name in old_names:
if old_name == new_name:
# A file was renamed to its new name twice; don't redirect that
continue
if old_name in current_files:
# A file was recreated with a former name; don't redirect those
continue
result.append((old_name, new_name))
for old_name, new_name in sorted(result):
print(f"{old_name} {new_name}")