Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions autoform_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,8 +431,8 @@ def main(argv: Sequence[str] | None = None) -> int:
render.add_argument("--repository-url", help="project URL, e.g. https://github.com/owner/repo")
render.add_argument(
"--ref",
help="commit or branch to link; links appear only if its local commit "
"holds the exact rendered inputs",
help="commit or branch to link; code links appear only if its local commit holds "
"the captured Lean sources, and links into the blueprint only if it also holds the blueprint",
)
render.add_argument(
"--require-declarations",
Expand Down
21 changes: 17 additions & 4 deletions autoform_cli/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,13 @@ def _sources_base(blueprint: Path, repo_root: Path, linker: SourceLinker) -> "_S
# The vault is outside the repository being linked, so no blob URL
# describes it. Better no link than one that 404s.
return None
spellings = {unicodedata.normalize("NFC", path): committed for path, committed in linker.spellings.items()}
spellings: dict[str, str] = {}
for path, committed in linker.spellings.items():
# A link may name a folder as well as a note, so each leading path maps too.
parts, committed_parts = path.split("/"), committed.split("/")
for depth in range(1, len(parts) + 1):
prefix = unicodedata.normalize("NFC", "/".join(parts[:depth]))
spellings.setdefault(prefix, "/".join(committed_parts[:depth]))
return _SourceBase(linker.repository_url, linker.ref, relative, spellings)


Expand All @@ -1151,12 +1157,19 @@ class _SourceBase:
ref: str
relative: str
spellings: Mapping[str, str] = field(default_factory=dict)
"""Committed paths keyed by NFC form, since a link spells a note however its author typed it."""
"""Committed captured paths and their leading folders, keyed by NFC form, since a link spells a path as typed."""

def href(self, tail: tuple[str, ...]) -> str:
verb = "blob" if tail else "tree"
path = "/".join((self.relative, *tail))
path = self.spellings.get(unicodedata.normalize("NFC", path), path)
parts = "/".join((self.relative, *tail)).split("/")
# A folder with no captured file below it, such as one holding only a `.gitkeep`, has no entry of its own,
# so only the leading folders it shares with a captured file take their committed spelling.
for depth in range(len(parts), 0, -1):
committed = self.spellings.get(unicodedata.normalize("NFC", "/".join(parts[:depth])))
if committed is not None:
parts[:depth] = committed.split("/")
break
path = "/".join(parts)
return f"{self.repository_url}/{verb}/{self.ref}/{quote(path, safe='/')}"


Expand Down
54 changes: 44 additions & 10 deletions tests/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -2432,6 +2432,19 @@ def test_markdown_source_permalink_quotes_the_original_repository_path(tmp_path:
assert ".autoform-publication" not in page and "autoform-blueprint-" not in page


def _print_composed_names(monkeypatch: pytest.MonkeyPatch) -> None:
"""Make `git ls-tree` print e + U+0301 composed, as macOS Git does; elsewhere Git keeps names as written."""
run = subprocess.run

def ls_tree(command, *args, **kwargs):
completed = run(command, *args, **kwargs)
if command[:2] == ["git", "ls-tree"]:
completed.stdout = completed.stdout.replace("e\u0301".encode(), "\u00e9".encode())
return completed

monkeypatch.setattr(lean_module.subprocess, "run", ls_tree)


def test_decomposed_file_names_link_to_the_spelling_git_committed(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand All @@ -2449,16 +2462,7 @@ def test_decomposed_file_names_link_to_the_spelling_git_committed(
subprocess.run(["git", "config", "core.precomposeunicode", "true"], cwd=project, check=True)
commit = _git_commit(project, "fixture")
output = tmp_path / "out"
run = subprocess.run

def precomposed_ls_tree(command, *args, **kwargs):
# macOS Git prints the composed spelling it committed; elsewhere Git keeps names as written.
completed = run(command, *args, **kwargs)
if command[:2] == ["git", "ls-tree"]:
completed.stdout = completed.stdout.replace("e\u0301".encode(), "\u00e9".encode())
return completed

monkeypatch.setattr(lean_module.subprocess, "run", precomposed_ls_tree)
_print_composed_names(monkeypatch)

report = render_site(project / "blueprint", output, lean_root=project, repository_url=_REPOSITORY, ref=commit)

Expand All @@ -2470,6 +2474,36 @@ def precomposed_ls_tree(command, *args, **kwargs):
assert report.warnings == []


def test_a_link_to_a_folder_names_the_spelling_git_committed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
project = _project(tmp_path)
blueprint = project / "Poincare\u0301" / "blueprint" # a folder name decomposed on disk, as Finder writes it
blueprint.parent.mkdir()
(project / "blueprint").rename(blueprint)
(blueprint / "sources").mkdir()
(blueprint / "sources/paper.md").write_text("# Paper\n", encoding="utf-8")
(blueprint / "sources/drafts").mkdir()
(blueprint / "sources/drafts/.gitkeep").write_text("", encoding="utf-8") # committed, but never captured
_edit(
blueprint / "roadmap/README.md",
"# Roadmap",
"# Roadmap\n\nSee [the notes](../sources/), [the paper](../sources/paper.md) and [drafts](../sources/drafts/).",
)
subprocess.run(["git", "init", "-q"], cwd=project, check=True)
subprocess.run(["git", "config", "core.precomposeunicode", "true"], cwd=project, check=True)
commit = _git_commit(project, "fixture")
_print_composed_names(monkeypatch)

report = render_site(blueprint, tmp_path / "out", lean_root=project, repository_url=_REPOSITORY, ref=commit)

chapter = (tmp_path / "out/roadmap/README.md").read_text(encoding="utf-8")
assert f"/tree/{commit}/Poincar%C3%A9/blueprint/sources" in chapter
assert f"/blob/{commit}/Poincar%C3%A9/blueprint/sources/paper.md" in chapter
assert f"/blob/{commit}/Poincar%C3%A9/blueprint/roadmap/top.md" in chapter
assert f"/{commit}/Poincar%C3%A9/blueprint/sources/drafts" in chapter
assert "Poincare%CC%81" not in chapter
assert report.warnings == []


def test_a_source_note_link_names_the_committed_spelling_however_it_was_typed() -> None:
from autoform_cli.render import _SourceBase

Expand Down
Loading