diff --git a/doc/doc-support/package.nix b/doc/doc-support/package.nix index f1d9996120cee..c2bbdf09e5339 100644 --- a/doc/doc-support/package.nix +++ b/doc/doc-support/package.nix @@ -9,6 +9,8 @@ mkShellNoCC, documentation-highlighter, nixos-render-docs, + nixos-render-docs-redirects, + writeShellScriptBin, nixpkgs ? { }, }: @@ -105,8 +107,14 @@ stdenvNoCC.mkDerivation ( buildArgs = "./."; open = "/share/doc/nixpkgs/manual.html"; }; + nixos-render-docs-redirects' = writeShellScriptBin "redirects" "${lib.getExe nixos-render-docs-redirects} --file ${toString ../redirects.json} $@"; in - mkShellNoCC { packages = [ devmode' ]; }; + mkShellNoCC { + packages = [ + devmode' + nixos-render-docs-redirects' + ]; + }; tests.manpage-urls = callPackage ../tests/manpage-urls.nix { }; }; diff --git a/nixos/doc/manual/shell.nix b/nixos/doc/manual/shell.nix index aeec93118fc01..4f6ab400f22c7 100644 --- a/nixos/doc/manual/shell.nix +++ b/nixos/doc/manual/shell.nix @@ -10,7 +10,11 @@ let buildArgs = "../../release.nix -A manualHTML.${builtins.currentSystem}"; open = "/${outputPath}/${indexPath}"; }; + nixos-render-docs-redirects = pkgs.writeShellScriptBin "redirects" "${pkgs.lib.getExe pkgs.nixos-render-docs-redirects} --file ${toString ./redirects.json} $@"; in pkgs.mkShellNoCC { - packages = [ devmode ]; + packages = [ + devmode + nixos-render-docs-redirects + ]; } diff --git a/pkgs/by-name/ni/nixos-render-docs-redirects/package.nix b/pkgs/by-name/ni/nixos-render-docs-redirects/package.nix new file mode 100644 index 0000000000000..a473fd1533ca5 --- /dev/null +++ b/pkgs/by-name/ni/nixos-render-docs-redirects/package.nix @@ -0,0 +1,22 @@ +{ lib, python3 }: + +python3.pkgs.buildPythonApplication { + pname = "nixos-render-docs-redirects"; + version = "0.0"; + pyproject = true; + + src = ./src; + + build-system = with python3.pkgs; [ setuptools ]; + + nativeCheckInputs = with python3.pkgs; [ + pytestCheckHook + ]; + + meta = { + description = "Redirects manipulation for nixos manuals"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ getpsyched ]; + mainProgram = "redirects"; + }; +} diff --git a/pkgs/by-name/ni/nixos-render-docs-redirects/src/nixos_render_docs_redirects/__init__.py b/pkgs/by-name/ni/nixos-render-docs-redirects/src/nixos_render_docs_redirects/__init__.py new file mode 100644 index 0000000000000..d09849ac48669 --- /dev/null +++ b/pkgs/by-name/ni/nixos-render-docs-redirects/src/nixos_render_docs_redirects/__init__.py @@ -0,0 +1,130 @@ +import argparse +import json +import sys +from pathlib import Path + + +def add_content(redirects: dict[str, list[str]], identifier: str, path: str) -> dict[str, list[str]]: + if identifier in redirects: + raise IdentifierExists(identifier) + + # Insert the new identifier in alphabetical order + new_redirects = list(redirects.items()) + insertion_index = 0 + for i, (key, _) in enumerate(new_redirects): + if identifier > key: + insertion_index = i + 1 + else: + break + new_redirects.insert(insertion_index, (identifier, [f"{path}#{identifier}"])) + return dict(new_redirects) + + +def move_content(redirects: dict[str, list[str]], identifier: str, path: str) -> dict[str, list[str]]: + if identifier not in redirects: + raise IdentifierNotFound(identifier) + redirects[identifier].insert(0, f"{path}#{identifier}") + return redirects + + +def rename_identifier( + redirects: dict[str, list[str]], + old_identifier: str, + new_identifier: str +) -> dict[str, list[str]]: + if old_identifier not in redirects: + raise IdentifierNotFound(old_identifier) + if new_identifier in redirects: + raise IdentifierExists(new_identifier) + + # To minimise the diff, we recreate the redirects mapping allowing + # the new key to be updated in-place, preserving the index. + new_redirects = {} + current_path = "" + for key, value in redirects.items(): + if key == old_identifier: + new_redirects[new_identifier] = value + current_path = value[0].split('#')[0] + continue + new_redirects[key] = value + new_redirects[new_identifier].insert(0, f"{current_path}#{new_identifier}") + return new_redirects + + +def remove_and_redirect( + redirects: dict[str, list[str]], + old_identifier: str, + new_identifier: str +) -> dict[str, list[str]]: + if old_identifier not in redirects: + raise IdentifierNotFound(old_identifier) + if new_identifier not in redirects: + raise IdentifierNotFound(new_identifier) + redirects[new_identifier].extend(redirects.pop(old_identifier)) + return redirects + + +def main(): + parser = argparse.ArgumentParser(description="redirects manipulation for nixos manuals") + commands = parser.add_subparsers(dest="command", required=True) + parser.add_argument("-f", "--file", type=Path, required=True) + + add_content_cmd = commands.add_parser("add-content") + add_content_cmd.add_argument("identifier", type=str) + add_content_cmd.add_argument("path", type=str) + + move_content_cmd = commands.add_parser("move-content") + move_content_cmd.add_argument("identifier", type=str) + move_content_cmd.add_argument("path", type=str) + + rename_id_cmd = commands.add_parser("rename-identifier") + rename_id_cmd.add_argument("old_identifier", type=str) + rename_id_cmd.add_argument("new_identifier", type=str) + + remove_redirect_cmd = commands.add_parser("remove-and-redirect") + remove_redirect_cmd.add_argument("identifier", type=str) + remove_redirect_cmd.add_argument("target_identifier", type=str) + + args = parser.parse_args() + + with open(args.file) as file: + redirects = json.load(file) + + try: + if args.command == "add-content": + redirects = add_content(redirects, args.identifier, args.path) + print(f"Added new identifier: {args.identifier}") + + elif args.command == "move-content": + redirects = move_content(redirects, args.identifier, args.path) + print(f"Moved '{args.identifier}' to the new path: {args.path}") + + elif args.command == "rename-identifier": + redirects = rename_identifier(redirects, args.old_identifier, args.new_identifier) + print(f"Renamed identifier from {args.old_identifier} to {args.new_identifier}") + + elif args.command == "remove-and-redirect": + redirects = remove_and_redirect(redirects, args.identifier, args.target_identifier) + print(f"Redirect from '{args.identifier}' to '{args.target_identifier}' added.") + except Exception as error: + print(error, file=sys.stderr) + else: + with open(args.file, "w") as file: + json.dump(redirects, file, indent=2) + file.write("\n") + + +class IdentifierExists(Exception): + def __init__(self, identifier: str): + self.identifier = identifier + + def __str__(self): + return f"The identifier '{self.identifier}' already exists." + + +class IdentifierNotFound(Exception): + def __init__(self, identifier: str): + self.identifier = identifier + + def __str__(self): + return f"The identifier '{self.identifier}' does not exist in the redirect mapping." diff --git a/pkgs/by-name/ni/nixos-render-docs-redirects/src/pyproject.toml b/pkgs/by-name/ni/nixos-render-docs-redirects/src/pyproject.toml new file mode 100644 index 0000000000000..50f7bcb95507b --- /dev/null +++ b/pkgs/by-name/ni/nixos-render-docs-redirects/src/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "nixos-render-docs-redirects" +version = "0.0" +description = "redirects manipulation for nixos manuals" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.scripts] +redirects = "nixos_render_docs_redirects:main" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/pkgs/by-name/ni/nixos-render-docs-redirects/src/tests/__init__.py b/pkgs/by-name/ni/nixos-render-docs-redirects/src/tests/__init__.py new file mode 100644 index 0000000000000..8b137891791fe --- /dev/null +++ b/pkgs/by-name/ni/nixos-render-docs-redirects/src/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/pkgs/by-name/ni/nixos-render-docs-redirects/src/tests/test_redirects.py b/pkgs/by-name/ni/nixos-render-docs-redirects/src/tests/test_redirects.py new file mode 100644 index 0000000000000..ae648d5e26c9a --- /dev/null +++ b/pkgs/by-name/ni/nixos-render-docs-redirects/src/tests/test_redirects.py @@ -0,0 +1,86 @@ +import unittest +from nixos_render_docs_redirects import ( + add_content, + move_content, + rename_identifier, + remove_and_redirect, + IdentifierExists, + IdentifierNotFound, +) + + +class RedirectsTestCase(unittest.TestCase): + def test_add_content(self): + initial_redirects = { + "bar": ["path/to/bar.html#bar"], + "foo": ["path/to/foo.html#foo"], + } + final_redirects = { + "bar": ["path/to/bar.html#bar"], + "baz": ["path/to/baz.html#baz"], + "foo": ["path/to/foo.html#foo"], + } + + result = add_content(initial_redirects, "baz", "path/to/baz.html") + self.assertEqual(list(result.items()), list(final_redirects.items())) + + with self.assertRaises(IdentifierExists): + add_content(result, "foo", "another/path.html") + + + def test_move_content(self): + initial_redirects = { + "foo": ["path/to/foo.html#foo"], + "bar": ["path/to/bar.html#bar"], + } + final_redirects = { + "foo": ["new/path.html#foo", "path/to/foo.html#foo"], + "bar": ["path/to/bar.html#bar"], + } + + result = move_content(initial_redirects, "foo", "new/path.html") + self.assertEqual(list(result.items()), list(final_redirects.items())) + + with self.assertRaises(IdentifierNotFound): + move_content(result, "baz", "path.html") + + + def test_rename_identifier(self): + initial_redirects = { + "foo": ["path/to/foo.html#foo"], + "bar": ["path/to/bar.html#bar"], + "baz": ["path/to/baz.html#baz"], + } + final_redirects = { + "foo": ["path/to/foo.html#foo"], + "boo": ["path/to/bar.html#boo", "path/to/bar.html#bar"], + "baz": ["path/to/baz.html#baz"], + } + + result = rename_identifier(initial_redirects, "bar", "boo") + self.assertEqual(list(result.items()), list(final_redirects.items())) + + with self.assertRaises(IdentifierNotFound): + rename_identifier(result, "bar", "boo") + with self.assertRaises(IdentifierExists): + rename_identifier(result, "boo", "boo") + + + def test_remove_and_redirect(self): + initial_redirects = { + "foo": ["new/path.html#foo", "path/to/foo.html#foo"], + "bar": ["path/to/bar.html#bar"], + "baz": ["path/to/baz.html#baz"], + } + final_redirects = { + "bar": ["path/to/bar.html#bar", "new/path.html#foo", "path/to/foo.html#foo"], + "baz": ["path/to/baz.html#baz"], + } + + result = remove_and_redirect(initial_redirects, "foo", "bar") + self.assertEqual(list(result.items()), list(final_redirects.items())) + + with self.assertRaises(IdentifierNotFound): + remove_and_redirect(result, "foo", "bar") + with self.assertRaises(IdentifierNotFound): + remove_and_redirect(initial_redirects, "foo", "baz") diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py index 57a6d6bbb3aa5..1a891a1af238e 100644 --- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py +++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py @@ -47,23 +47,49 @@ def __str__(self): If you moved content, add its new location as the first element of the redirects mapping. Please update doc/redirects.json or nixos/doc/manual/redirects.json! -""") # TODO: automatically detect if you just missed adding a new location, and make a tool to do that for you +""") if self.identifiers_without_redirects: error_messages.append(f""" Identifiers present in the source must have a mapping in the redirects file. - {"\n - ".join(self.identifiers_without_redirects)} This can happen when an identifier was added or renamed. - Please update doc/redirects.json or nixos/doc/manual/redirects.json! -""") # TODO: add tooling in the development shell to do that automatically and point to that command + + Added new content? + redirects add-content ❬identifier❭ ❬path❭ + + Moved existing content to a different output path? + redirects move-content ❬identifier❭ ❬path❭ + + Renamed existing identifiers? + redirects rename-identifier ❬old-identifier❭ ❬new-identifier❭ + + Removed content? Redirect to alternatives or relevant release notes. + redirects remove-and-redirect ❬identifier❭ ❬target-identifier❭ + + Note that you need to run `nix-shell doc` or `nix-shell nixos/doc/manual` to be able to run this command. +""") if self.orphan_identifiers: error_messages.append(f""" Keys of the redirects mapping must correspond to some identifier in the source. - {"\n - ".join(self.orphan_identifiers)} This can happen when an identifier was removed or renamed. - Please update doc/redirects.json or nixos/doc/manual/redirects.json! -""") # TODO: add tooling in the development shell to do that automatically and point to that command + + Added new content? + redirects add-content ❬identifier❭ ❬path❭ + + Moved existing content to a different output path? + redirects move-content ❬identifier❭ ❬path❭ + + Renamed existing identifiers? + redirects rename-identifier ❬old-identifier❭ ❬new-identifier❭ + + Removed content? (good for redirecting deprecations to new content or release notes) + redirects remove-and-redirect ❬identifier❭ ❬target-identifier❭ + + Note that you need to run `nix-shell doc` or `nix-shell nixos/doc/manual` to be able to run this command. +""") error_messages.append("NOTE: If your Manual build passes locally and you see this message in CI, you probably need a rebase.") return "\n".join(error_messages)