diff --git a/doc/doc-support/package.nix b/doc/doc-support/package.nix index f1d9996120ce..c2bbdf09e533 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 aeec93118fc0..4f6ab400f22c 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 000000000000..a473fd1533ca --- /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 000000000000..d09849ac4866 --- /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 000000000000..50f7bcb95507 --- /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 000000000000..8b137891791f --- /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 000000000000..ae648d5e26c9 --- /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 57a6d6bbb3aa..1a891a1af238 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 @@ The first element of an identifier's redirects list must denote its current loca 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)