From b240ceb9f18b699ec8aa1b1e985dfc2479b24795 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Sat, 10 Oct 2026 12:00:11 +0800 Subject: [PATCH] Match wiki pages by address so names with spaces are the same page Gitea stores the title "Home da" as Home-da. The publisher compared title strings, so a file named Home-da.md would have tried to create a page that already exists. Identify a page by the address Gitea reports (sub_url), treat a space and a hyphen as the same, and refuse two files that name one page. Found while preparing the pilot in sqa-qc-checklists, whose wiki has pages named with spaces. Task: MIL-006#3 --- .gitea/scoped_workflows/publish-wiki.yml | 33 ++++++++-- tests/test_publish_wiki.py | 81 +++++++++++++++++++++++- 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/.gitea/scoped_workflows/publish-wiki.yml b/.gitea/scoped_workflows/publish-wiki.yml index 4ba9f4e..97f7671 100644 --- a/.gitea/scoped_workflows/publish-wiki.yml +++ b/.gitea/scoped_workflows/publish-wiki.yml @@ -142,9 +142,27 @@ jobs: return owner, repo + def page_key(title): + """Return the identity of a page: Gitea stores a space as a hyphen. + + The page "Home da" is the file Home-da.md and its address is Home-da, so + both spellings must name the same page. + """ + return str(title).replace(" ", "-") + + def read_pages(wiki_dir): - """Return {page title: content} for every Markdown file in wiki_dir.""" - pages = {path.stem: path.read_bytes() for path in sorted(wiki_dir.glob("*.md"))} + """Return {page key: content} for every Markdown file in wiki_dir.""" + pages = {} + for path in sorted(wiki_dir.glob("*.md")): + key = page_key(path.stem) + if key in pages: + raise WorkflowError( + "configuration", + f"Two files in {wiki_dir} name the wiki page {key}; a space and " + "a hyphen are the same in a page name.", + ) + pages[key] = path.read_bytes() if not pages: # Publishing an empty directory would delete the whole wiki. raise WorkflowError( @@ -154,7 +172,10 @@ jobs: def list_wiki_titles(wiki_url, headers, send): - """Return the titles of the pages that exist in the wiki. + """Return the keys of the pages that exist in the wiki. + + The key is the address Gitea reports (sub_url), or the title with its + spaces replaced by hyphens. Gitea answers 404 for a wiki that has never been written to; that is an empty wiki, not a failure. @@ -171,7 +192,11 @@ jobs: raise if not isinstance(batch, list): raise WorkflowError("api", "Gitea returned an invalid wiki page list.") - titles.update(str(item.get("title", "")) for item in batch) + titles.update( + urllib.parse.unquote(str(item.get("sub_url") or "")) + or page_key(item.get("title", "")) + for item in batch + ) if len(batch) < PAGE_LIMIT: return titles page += 1 diff --git a/tests/test_publish_wiki.py b/tests/test_publish_wiki.py index 8172324..d59b1be 100644 --- a/tests/test_publish_wiki.py +++ b/tests/test_publish_wiki.py @@ -67,6 +67,27 @@ class TestReadPages: publish_wiki.read_pages(tmp_path) +class TestPageKey: + @pytest.mark.parametrize( + ("name", "key"), + [("Home", "Home"), ("Home da", "Home-da"), ("Home-da", "Home-da"), ("Home.da", "Home.da"), + ("AI Prompt Template ms", "AI-Prompt-Template-ms"), ("_Sidebar", "_Sidebar")], + ) + def test_a_space_and_a_hyphen_are_the_same_page(self, publish_wiki, name, key): + assert publish_wiki.page_key(name) == key + + def test_file_names_with_spaces_and_hyphens_are_keyed_the_same(self, publish_wiki, tmp_path): + (tmp_path / "Home da.md").write_bytes(b"hjem") + (tmp_path / "_Sidebar.md").write_bytes(b"menu") + assert publish_wiki.read_pages(tmp_path) == {"Home-da": b"hjem", "_Sidebar": b"menu"} + + def test_two_files_for_one_page_are_refused(self, publish_wiki, tmp_path): + (tmp_path / "Home da.md").write_bytes(b"one") + (tmp_path / "Home-da.md").write_bytes(b"two") + with pytest.raises(publish_wiki.WorkflowError, match="name the wiki page Home-da"): + publish_wiki.read_pages(tmp_path) + + class FakeGitea: """Answers like the Gitea repository and wiki endpoints.""" @@ -84,7 +105,7 @@ class FakeGitea: if url.startswith(f"{WIKI_URL}/pages"): if not self.wiki_initialised: raise self.error("configuration", "404") - return [{"title": title} for title in self.wiki] + return [{"title": key.replace("-", " "), "sub_url": key} for key in self.wiki] if method == "POST" and url == f"{WIKI_URL}/new": self.wiki[body["title"]] = body["content_base64"] return None @@ -265,6 +286,64 @@ class TestPublish: assert gitea.wiki == {"Home": b64("keep me")} +class TestPageIdentity: + """A wiki that already has pages named with spaces, as Gitea stores them.""" + + def test_a_file_named_with_a_space_updates_the_existing_page( + self, publish_wiki, environment + ): + (environment / "Home.md").unlink() + (environment / "Credentials.md").unlink() + (environment / ".publish").write_text("", encoding="utf-8") + (environment / "Home da.md").write_text("hjem v2", encoding="utf-8") + gitea = FakeGitea(publish_wiki, wiki={"Home-da": b64("hjem v1")}) + + assert run_main(publish_wiki, gitea) == 0 + + assert gitea.wiki == {"Home-da": b64("hjem v2")} + assert [call[0] for call in gitea.calls].count("POST") == 0 + assert ("PATCH", f"{WIKI_URL}/page/Home-da") in [(c[0], c[1]) for c in gitea.calls] + + def test_identical_pages_with_spaces_are_a_no_op(self, publish_wiki, environment): + (environment / "Home.md").unlink() + (environment / "Credentials.md").unlink() + (environment / "Home da.md").write_text("hjem", encoding="utf-8") + (environment / "_Sidebar.md").write_text("menu", encoding="utf-8") + gitea = FakeGitea( + publish_wiki, wiki={"Home-da": b64("hjem"), "_Sidebar": b64("menu")} + ) + + assert run_main(publish_wiki, gitea) == 0 + + assert gitea.writes == [] + + def test_prune_does_not_delete_a_page_that_is_in_the_folder_under_another_spelling( + self, publish_wiki, environment + ): + (environment / "Home.md").unlink() + (environment / "Credentials.md").unlink() + (environment / "Install ms.md").write_text("pasang", encoding="utf-8") + gitea = FakeGitea( + publish_wiki, wiki={"Install-ms": b64("pasang"), "Old-page": b64("x")} + ) + + assert run_main(publish_wiki, gitea) == 0 + + assert gitea.wiki == {"Install-ms": b64("pasang")} + + def test_the_address_wins_over_the_title_and_is_decoded(self, publish_wiki): + def listing(url, headers=None, **kwargs): + return [ + {"title": "Whatever", "sub_url": "Real-Address"}, + {"title": "Only title"}, + {"title": "Encoded", "sub_url": "Caf%C3%A9"}, + ] + + keys = publish_wiki.list_wiki_titles(WIKI_URL, {}, listing) + + assert keys == {"Real-Address", "Only-title", "Café"} + + class TestDescribeToken: def test_recognises_a_gitea_token_shape(self, publish_wiki): description = publish_wiki.describe_token("a1b2c3d4e5" * 4)