diff --git a/pygittools/src/pygittools/merge.py b/pygittools/src/pygittools/merge.py
index 2d8a039..a28984e 100644
--- a/pygittools/src/pygittools/merge.py
+++ b/pygittools/src/pygittools/merge.py
@@ -7,7 +7,7 @@ import re
 import secrets
 from datetime import UTC, datetime
 from enum import Enum
-from typing import Any
+from typing import Any, TypedDict
 
 from pygit2 import Commit, GitError, Oid, Repository, Signature, Tag, reference_is_valid_name
 from pygit2.enums import MergeAnalysis
@@ -17,6 +17,13 @@ from pygittools.tasks import GIT_OBJECT_TAG, _build_tag_raw, _DateTimeEncoder
 MR_REF_PREFIX: str = "refs/tags/mr/"
 
 
+class MergeRequestStatusFields(TypedDict):
+    status_code: str
+    status_label: str
+    status_detail: str
+    show_merge_button: bool
+
+
 def generate_mr_ref() -> str:
     """Return ``refs/tags/mr/<16 hex chars>`` for distributed-safe ref names."""
     return f"{MR_REF_PREFIX}{secrets.token_hex(8)}"
@@ -89,6 +96,15 @@ class MergeRequestStatus(Enum):
     BLOCKED_DIVERGED = "blocked_diverged"
 
 
+MERGE_REQUEST_STATUS_LABELS: dict[MergeRequestStatus, str] = {
+    MergeRequestStatus.CAN_FAST_FORWARD: "Can fast-forward",
+    MergeRequestStatus.UP_TO_DATE: "Already up to date",
+    MergeRequestStatus.BLOCKED_DIVERGED: "Blocked: histories diverged (not fast-forward)",
+    MergeRequestStatus.OURS_REF_MISSING: "Blocked: ours ref is missing",
+    MergeRequestStatus.MR_HEAD_MISSING: "Blocked: merge request head is missing from the repository",
+}
+
+
 """
 A MergeRequest is an annotated tag (and ref under refs/tags/mr/).
 The tag points at the commit (or tag for merge-train) being merged.
@@ -245,3 +261,48 @@ def get_merge_request_by_oid(repo: Repository, oid: Oid) -> MergeRequest:
     if not isinstance(obj, Tag):
         raise ValueError(f"Object is not a merge request tag: {oid}")
     return _merge_request_from_tag(obj)
+
+
+def try_merge_request_from_tag(repo: Repository, tag: Tag) -> MergeRequest | None:
+    try:
+        return get_merge_request_by_oid(repo, tag.id)
+    except (ValueError, json.JSONDecodeError, TypeError, KeyError):
+        return None
+
+
+def resolve_merge_request_tag_to_tip(repo: Repository, obj: Tag) -> tuple[Tag, MergeRequest] | None:
+    """If ``obj`` is an MR tag and ``refs/tags/mr/…`` points to a newer tag object, return that tip."""
+    mr = try_merge_request_from_tag(repo, obj)
+    if mr is None:
+        return None
+    if mr.name not in repo.references:
+        return obj, mr
+    tip_id = repo.references[mr.name].resolve().target
+    if tip_id == obj.id:
+        return obj, mr
+    tip_obj = repo[tip_id]
+    if not isinstance(tip_obj, Tag):
+        return obj, mr
+    mr_tip = try_merge_request_from_tag(repo, tip_obj)
+    if mr_tip is None:
+        return obj, mr
+    return tip_obj, mr_tip
+
+
+def merge_request_status_fields(repo: Repository, mr: MergeRequest) -> MergeRequestStatusFields:
+    try:
+        status = mr.check_status(repo)
+    except ValueError as exc:
+        return {
+            "status_code": "error",
+            "status_label": "Unable to analyze merge",
+            "status_detail": str(exc),
+            "show_merge_button": False,
+        }
+    label = MERGE_REQUEST_STATUS_LABELS.get(status, status.value)
+    return {
+        "status_code": status.value,
+        "status_label": label,
+        "status_detail": "",
+        "show_merge_button": status == MergeRequestStatus.CAN_FAST_FORWARD,
+    }
diff --git a/pygittools/src/pygittools/merge_test.py b/pygittools/src/pygittools/merge_test.py
index e117361..abf67c1 100644
--- a/pygittools/src/pygittools/merge_test.py
+++ b/pygittools/src/pygittools/merge_test.py
@@ -1,7 +1,7 @@
 from pathlib import Path
 
 import pytest
-from pygit2 import GIT_OBJECT_BLOB, Oid, Repository, Signature, init_repository
+from pygit2 import GIT_OBJECT_BLOB, Oid, Repository, Signature, Tag, init_repository
 
 from pygittools.merge import (
     MR_REF_PREFIX,
@@ -11,7 +11,10 @@ from pygittools.merge import (
     generate_mr_ref,
     get_merge_request,
     get_merge_request_by_oid,
+    merge_request_status_fields,
     peel_mr_head_to_commit_oid,
+    resolve_merge_request_tag_to_tip,
+    try_merge_request_from_tag,
 )
 from pygittools.tasks import EMPTY_TREE_OID_HEX, _ensure_empty_tree
 
@@ -163,3 +166,56 @@ def test_refresh_from_theirs_advances_mr_head(repo: Repository) -> None:
     new_oid = mr.refresh_from_theirs(repo)
     assert new_oid is not None
     assert peel_mr_head_to_commit_oid(repo, mr.target) == b
+
+
+def test_try_merge_request_from_tag_roundtrip(repo: Repository) -> None:
+    c = _commit(repo)
+    ref = f"{MR_REF_PREFIX}trytrytrytrytry1"
+    mr = MergeRequest(c, SIG, ours="refs/heads/main", title="try", name=ref)
+    oid = mr.write(repo)
+    tag = repo[oid]
+    assert isinstance(tag, Tag)
+    loaded = try_merge_request_from_tag(repo, tag)
+    assert loaded is not None
+    assert loaded.title == "try"
+    assert loaded.name == ref
+
+
+def test_try_merge_request_from_tag_rejects_plain_tag(repo: Repository) -> None:
+    c = _commit(repo)
+    oid = repo.create_tag("plain", c, repo[c].type, SIG, "not an mr")
+    tag = repo[oid]
+    assert isinstance(tag, Tag)
+    assert try_merge_request_from_tag(repo, tag) is None
+
+
+def test_resolve_merge_request_tag_to_tip_follows_rewritten_ref(repo: Repository) -> None:
+    _a, b = _linear_ab(repo)
+    ref = f"{MR_REF_PREFIX}tipfollow000001"
+    mr = MergeRequest(b, SIG, ours="refs/heads/main", title="v1", name=ref)
+    old_oid = mr.write(repo)
+    old_tag = repo[old_oid]
+    assert isinstance(old_tag, Tag)
+
+    mr.title = "v2"
+    mr.update_message()
+    new_oid = mr.write(repo)
+    assert new_oid != old_oid
+
+    resolved = resolve_merge_request_tag_to_tip(repo, old_tag)
+    assert resolved is not None
+    tip_tag, tip_mr = resolved
+    assert tip_tag.id == new_oid
+    assert tip_mr.title == "v2"
+
+
+def test_merge_request_status_fields_can_fast_forward(repo: Repository) -> None:
+    _a, b = _linear_ab(repo)
+    ref = f"{MR_REF_PREFIX}statusfields0001"
+    mr = MergeRequest(b, SIG, ours="refs/heads/main", title="ff", name=ref)
+    mr.write(repo)
+    fields = merge_request_status_fields(repo, mr)
+    assert fields["status_code"] == MergeRequestStatus.CAN_FAST_FORWARD.value
+    assert fields["status_label"] == "Can fast-forward"
+    assert fields["show_merge_button"] is True
+    assert fields["status_detail"] == ""
diff --git a/pygittools/src/pygittools/tui/projects.py b/pygittools/src/pygittools/tui/projects.py
index 9e30f1a..df729ca 100644
--- a/pygittools/src/pygittools/tui/projects.py
+++ b/pygittools/src/pygittools/tui/projects.py
@@ -7,10 +7,11 @@ from dataclasses import dataclass
 from pathlib import Path
 from urllib.parse import unquote
 
-from pygit2 import GitError, Repository, discover_repository
+from pygit2 import GitError, Repository
 
 from pygittools.tui.branches import current_branch_name
 from pygittools.tui.pygitweb_layout import PygitwebLayout
+from pygittools.validation import check_export_ok
 
 
 @dataclass(frozen=True, slots=True)
@@ -48,9 +49,14 @@ def _branch_for_worktree(worktree: Path) -> str:
     return current_branch_name(repo)
 
 
+def _export_filter(layout: PygitwebLayout) -> str:
+    return "" if layout.list_all else layout.export_ok
+
+
 def _find_projects_in_dir(layout: PygitwebLayout) -> list[tuple[str, Path]]:
     root = layout.projects_list.resolve()
     projectroot = layout.projectroot.resolve()
+    export_ok = _export_filter(layout)
     result: list[tuple[str, Path]] = []
     for dirpath, dirnames, _ in os.walk(root, topdown=True):
         rel = os.path.relpath(dirpath, root)
@@ -67,12 +73,9 @@ def _find_projects_in_dir(layout: PygitwebLayout) -> list[tuple[str, Path]]:
                     continue
             except OSError:
                 continue
-            discovered = discover_repository(str(path))
-            if discovered is None:
+            if not check_export_ok(str(path), export_ok):
                 continue
             project_path = os.path.relpath(path, projectroot).replace("\\", "/")
-            if not layout.list_all and not _export_ok(path, layout.export_ok):
-                continue
             worktree = path.resolve()
             result.append((project_path, worktree))
             dirnames.remove(name)
@@ -81,6 +84,7 @@ def _find_projects_in_dir(layout: PygitwebLayout) -> list[tuple[str, Path]]:
 
 def _projects_from_file(layout: PygitwebLayout) -> list[tuple[str, Path]]:
     projectroot = layout.projectroot.resolve()
+    export_ok = _export_filter(layout)
     result: list[tuple[str, Path]] = []
     try:
         lines = layout.projects_list.read_text(encoding="utf-8").splitlines()
@@ -95,17 +99,7 @@ def _projects_from_file(layout: PygitwebLayout) -> list[tuple[str, Path]]:
         if not project_path:
             continue
         worktree = (projectroot / project_path).resolve()
-        if not worktree.is_dir():
-            continue
-        if discover_repository(str(worktree)) is None:
-            continue
-        if not layout.list_all and not _export_ok(worktree, layout.export_ok):
+        if not check_export_ok(str(worktree), export_ok):
             continue
         result.append((project_path, worktree))
     return result
-
-
-def _export_ok(worktree: Path, export_ok: str) -> bool:
-    if export_ok and not (worktree / export_ok).is_file():
-        return False
-    return discover_repository(str(worktree)) is not None
diff --git a/pygittools/src/pygittools/tui/projects_test.py b/pygittools/src/pygittools/tui/projects_test.py
index 3f69286..75f24c5 100644
--- a/pygittools/src/pygittools/tui/projects_test.py
+++ b/pygittools/src/pygittools/tui/projects_test.py
@@ -7,12 +7,12 @@ from pygit2 import init_repository
 
 from pygittools.tui.projects import (
     _branch_for_worktree,
-    _export_ok,
     _find_projects_in_dir,
     _projects_from_file,
     list_projects,
 )
 from pygittools.tui.pygitweb_layout import PygitwebLayout
+from pygittools.validation import check_export_ok
 
 
 def _layout(tmp_path: Path, **overrides: object) -> PygitwebLayout:
@@ -121,7 +121,7 @@ def test_find_projects_respects_maxdepth(tmp_path: Path) -> None:
 def test_export_ok_requires_repository(tmp_path: Path) -> None:
     path = tmp_path / "not-a-repo"
     path.mkdir()
-    assert _export_ok(path, "") is False
+    assert check_export_ok(str(path), "") is False
 
 
 def test_projects_from_file_read_error(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
diff --git a/pygittools/src/pygittools/validation.py b/pygittools/src/pygittools/validation.py
new file mode 100644
index 0000000..169c245
--- /dev/null
+++ b/pygittools/src/pygittools/validation.py
@@ -0,0 +1,75 @@
+"""
+Validation: pathname, ref format, project; repo discovery via pygit2.
+Ported from gitweb/gitweb.perl (is_valid_pathname, is_valid_ref_format,
+is_valid_project). Repo check uses pygit2.discover_repository.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+from collections.abc import Callable
+
+import pygit2
+
+
+def is_valid_pathname(input_path: str | None) -> bool:
+    """No '.', '..' as path elements, no null, no doubled slashes. Port of is_valid_pathname."""
+    if input_path is None:
+        return False
+    if "\0" in input_path:
+        return False
+    parts = input_path.strip("/").split("/")
+    return all(p not in ("", ".", "..") for p in parts)
+
+
+def is_valid_ref_format(input_ref: str | None) -> bool:
+    """Git-check-ref-format rules: no /., no .., no control/space/special at start/end. Port of is_valid_ref_format."""
+    if input_ref is None:
+        return False
+    if "/." in input_ref or input_ref.startswith(".") or ".." in input_ref:
+        return False
+    if input_ref.endswith("/") or input_ref.endswith(".lock"):
+        return False
+    # No ASCII control, space, ~^:?*[
+    return not re.search(r"[\x00-\x20\x7f ~^:?*\[\]]", input_ref)
+
+
+def check_export_ok(
+    git_dir: str,
+    export_ok: str = "",
+    export_auth_hook: Callable[[str], bool] | None = None,
+    across_fs: bool = False,
+) -> bool:
+    """True if path is a git repo (via pygit2.discover_repository) and optional export_ok file / auth hook pass."""
+    if not os.path.isdir(git_dir):
+        return False
+    try:
+        discovered = pygit2.discover_repository(git_dir, across_fs)
+        if not discovered:
+            return False
+    except (KeyError, pygit2.GitError, OSError):
+        return False
+    if export_ok and not os.path.isfile(os.path.join(git_dir, export_ok)):
+        return False
+    return not export_auth_hook or export_auth_hook(git_dir)
+
+
+def is_valid_project(
+    project: str | None,
+    projectroot: str,
+    export_ok: str,
+    strict_export: bool,
+    project_in_list: Callable[[str], bool],
+) -> bool:
+    """Pathname valid, dir exists, export_ok, and (if strict) in project list. Port of is_valid_project."""
+    if project is None:
+        return False
+    if not is_valid_pathname(project):
+        return False
+    full = os.path.join(projectroot, project)
+    if not os.path.isdir(full):
+        return False
+    if not check_export_ok(full, export_ok):
+        return False
+    return not strict_export or project_in_list(project)
diff --git a/pygittools/src/pygittools/validation_test.py b/pygittools/src/pygittools/validation_test.py
new file mode 100644
index 0000000..9b0dbe8
--- /dev/null
+++ b/pygittools/src/pygittools/validation_test.py
@@ -0,0 +1,167 @@
+from __future__ import annotations
+
+import os
+import stat
+import sys
+from collections.abc import Generator
+from pathlib import Path
+
+import pygit2
+import pytest
+
+from pygittools.validation import check_export_ok, is_valid_pathname, is_valid_project, is_valid_ref_format
+
+
+class TestValidation:
+    @pytest.fixture(scope="class")
+    def project_env(self, tmp_path_factory: pytest.TempPathFactory) -> Generator[dict[str, str], None, None]:
+        projectroot: Path = tmp_path_factory.mktemp("validation-projects")
+        valid_dir: Path = projectroot / "valid"
+        invalid_dir: Path = projectroot / "invalid"
+        unreachable_dir: Path = projectroot / "unreachable"
+
+        valid_dir.mkdir()
+        invalid_dir.mkdir()
+        unreachable_dir.mkdir()
+
+        pygit2.init_repository(str(valid_dir), bare=True)
+        (valid_dir / "git-daemon-export-ok").write_text("", encoding="utf-8")
+
+        pygit2.init_repository(str(unreachable_dir), bare=True)
+        (unreachable_dir / "git-daemon-export-ok").write_text("", encoding="utf-8")
+        original_mode: int = stat.S_IMODE(unreachable_dir.stat().st_mode)
+        os.chmod(unreachable_dir, 0)
+
+        try:
+            yield {
+                "projectroot": str(projectroot),
+                "valid_name": "valid",
+                "invalid_name": "invalid",
+                "unreachable_name": "unreachable",
+                "valid_dir": str(valid_dir),
+                "invalid_dir": str(invalid_dir),
+                "unreachable_dir": str(unreachable_dir),
+                "export_ok": "git-daemon-export-ok",
+            }
+        finally:
+            os.chmod(unreachable_dir, original_mode)
+
+    class TestPathname:
+        def test_is_valid_pathname_success(self) -> None:
+            assert is_valid_pathname("group/project.git") is True
+
+        @pytest.mark.parametrize("input_path", [None, "a//b", "a/./b", "a/../b", "a\0b"])
+        def test_is_valid_pathname_failure_modes(self, input_path: str | None) -> None:
+            assert is_valid_pathname(input_path) is False
+
+    class TestRefFormat:
+        def test_is_valid_ref_format_success(self) -> None:
+            assert is_valid_ref_format("refs/heads/main") is True
+
+        @pytest.mark.parametrize(
+            "input_ref",
+            [None, ".bad", "refs/heads/../x", "refs/heads/x.lock", "refs/heads/x ", "refs/heads/x?y"],
+        )
+        def test_is_valid_ref_format_failure_modes(self, input_ref: str | None) -> None:
+            assert is_valid_ref_format(input_ref) is False
+
+    class TestCheckExportOk:
+        def test_check_export_ok_success(self, project_env: dict[str, str]) -> None:
+            assert check_export_ok(project_env["valid_dir"], project_env["export_ok"]) is True
+
+        def test_check_export_ok_fails_for_missing_directory(self, project_env: dict[str, str]) -> None:
+            missing_dir: str = str(Path(project_env["projectroot"]) / "missing")
+            assert check_export_ok(missing_dir, project_env["export_ok"]) is False
+
+        def test_check_export_ok_fails_for_non_repo_directory(self, project_env: dict[str, str]) -> None:
+            assert check_export_ok(project_env["invalid_dir"], project_env["export_ok"]) is False
+
+        def test_check_export_ok_fails_for_missing_export_file(self, project_env: dict[str, str]) -> None:
+            assert check_export_ok(project_env["valid_dir"], "missing-export-ok") is False
+
+        def test_check_export_ok_fails_when_hook_rejects(self, project_env: dict[str, str]) -> None:
+            assert (
+                check_export_ok(
+                    project_env["valid_dir"],
+                    project_env["export_ok"],
+                    export_auth_hook=lambda _path: False,
+                )
+                is False
+            )
+
+        @pytest.mark.skipif(sys.platform == "win32", reason="chmod-based unreachable directory check is not portable")
+        def test_check_export_ok_fails_for_unreachable_repo(self, project_env: dict[str, str]) -> None:
+            assert check_export_ok(project_env["unreachable_dir"], project_env["export_ok"]) is False
+
+    class TestValidProject:
+        def test_is_valid_project_success(self, project_env: dict[str, str]) -> None:
+            assert (
+                is_valid_project(
+                    project=project_env["valid_name"],
+                    projectroot=project_env["projectroot"],
+                    export_ok=project_env["export_ok"],
+                    strict_export=False,
+                    project_in_list=lambda _project: True,
+                )
+                is True
+            )
+
+        def test_is_valid_project_fails_for_none(self, project_env: dict[str, str]) -> None:
+            assert (
+                is_valid_project(
+                    project=None,
+                    projectroot=project_env["projectroot"],
+                    export_ok=project_env["export_ok"],
+                    strict_export=False,
+                    project_in_list=lambda _project: True,
+                )
+                is False
+            )
+
+        def test_is_valid_project_fails_for_invalid_pathname(self, project_env: dict[str, str]) -> None:
+            assert (
+                is_valid_project(
+                    project="bad/../name",
+                    projectroot=project_env["projectroot"],
+                    export_ok=project_env["export_ok"],
+                    strict_export=False,
+                    project_in_list=lambda _project: True,
+                )
+                is False
+            )
+
+        def test_is_valid_project_fails_for_missing_directory(self, project_env: dict[str, str]) -> None:
+            assert (
+                is_valid_project(
+                    project="missing",
+                    projectroot=project_env["projectroot"],
+                    export_ok=project_env["export_ok"],
+                    strict_export=False,
+                    project_in_list=lambda _project: True,
+                )
+                is False
+            )
+
+        def test_is_valid_project_fails_when_export_check_fails(self, project_env: dict[str, str]) -> None:
+            assert (
+                is_valid_project(
+                    project=project_env["invalid_name"],
+                    projectroot=project_env["projectroot"],
+                    export_ok=project_env["export_ok"],
+                    strict_export=False,
+                    project_in_list=lambda _project: True,
+                )
+                is False
+            )
+
+        def test_is_valid_project_fails_strict_export_when_not_listed(self, project_env: dict[str, str]) -> None:
+            assert (
+                is_valid_project(
+                    project=project_env["valid_name"],
+                    projectroot=project_env["projectroot"],
+                    export_ok=project_env["export_ok"],
+                    strict_export=True,
+                    project_in_list=lambda _project: False,
+                )
+                is False
+            )
diff --git a/pygitweb/src/pygitweb/actions.py b/pygitweb/src/pygitweb/actions.py
index 20a56bf..6965368 100644
--- a/pygitweb/src/pygitweb/actions.py
+++ b/pygitweb/src/pygitweb/actions.py
@@ -6,7 +6,6 @@ Ported from gitweb/gitweb.perl action handlers. Invoked by main.dispatch.
 from __future__ import annotations
 
 import base64
-import json
 import mimetypes
 import os
 from datetime import UTC, datetime, timedelta, timezone
@@ -17,7 +16,14 @@ from urllib.parse import parse_qsl, quote, urlencode
 import pygit2
 from fastapi import HTTPException, Request
 from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse, Response
-from pygittools.merge import MR_REF_PREFIX, MergeRequestStatus, get_merge_request_by_oid
+from pygittools.merge import (
+    MR_REF_PREFIX,
+    MergeRequestStatus,
+    merge_request_status_fields,
+    resolve_merge_request_tag_to_tip,
+    try_merge_request_from_tag,
+)
+from pygittools.validation import is_valid_pathname, is_valid_ref_format
 from python_ripgrep import PySortMode, PySortModeKind
 from python_ripgrep import search as rg_search
 
@@ -42,11 +48,7 @@ from pygitweb.git_helpers import (
     parse_tag,
 )
 from pygitweb.hooks_install import list_installable_hooks, list_installed_hooks
-from pygitweb.merge_requests import (
-    merge_request_status_fields,
-    merge_request_tag_response,
-    resolve_merge_request_tag_to_tip,
-)
+from pygitweb.merge_requests import merge_request_tag_response
 from pygitweb.projects import git_get_project_owner
 from pygitweb.templates_env import (
     Cell,
@@ -57,7 +59,6 @@ from pygitweb.templates_env import (
     render_page,
     trusted_html,
 )
-from pygitweb.validation import is_valid_pathname, is_valid_ref_format
 
 
 class BlameHunkInfo(TypedDict):
@@ -612,10 +613,12 @@ def _open_merge_requests(project: str) -> list[dict[str, str]]:
         try:
             oid = repo.references[ref_name].resolve().target
             obj = repo[oid]
-            if not isinstance(obj, pygit2.Tag):
-                continue
-            mr = get_merge_request_by_oid(repo, pygit2.Oid(hex=str(oid)))
-        except (ValueError, KeyError, TypeError, json.JSONDecodeError, pygit2.GitError):
+        except (KeyError, pygit2.GitError):
+            continue
+        if not isinstance(obj, pygit2.Tag):
+            continue
+        mr = try_merge_request_from_tag(repo, obj)
+        if mr is None:
             continue
         try:
             status = mr.check_status(repo)
diff --git a/pygitweb/src/pygitweb/board_refs.py b/pygitweb/src/pygitweb/board_refs.py
index ce129ca..b5f0828 100644
--- a/pygitweb/src/pygitweb/board_refs.py
+++ b/pygitweb/src/pygitweb/board_refs.py
@@ -7,8 +7,8 @@ from datetime import UTC, datetime, timedelta, timezone
 from typing import TypedDict
 from urllib.parse import quote
 
-from pygit2 import Oid, Repository, Tag
-from pygittools.merge import MR_REF_PREFIX, get_merge_request_by_oid
+from pygit2 import Repository, Tag
+from pygittools.merge import MR_REF_PREFIX, try_merge_request_from_tag
 
 from pygitweb.config import settings
 from pygitweb.git_helpers import get_commit_history
@@ -122,10 +122,12 @@ def _manifest_merge_requests(project: str) -> list[BoardRefManifestEntry]:
         try:
             oid = repo.references[ref_name].resolve().target
             obj = repo[oid]
-            if not isinstance(obj, Tag):
-                continue
-            mr = get_merge_request_by_oid(repo, Oid(hex=str(oid)))
-        except (ValueError, KeyError, TypeError):
+        except (KeyError, TypeError):
+            continue
+        if not isinstance(obj, Tag):
+            continue
+        mr = try_merge_request_from_tag(repo, obj)
+        if mr is None:
             continue
         slug = ref_name.removeprefix(MR_REF_PREFIX)
         title = mr.title or slug
diff --git a/pygitweb/src/pygitweb/dependencies.py b/pygitweb/src/pygitweb/dependencies.py
index 2a47dae..2c9780b 100644
--- a/pygitweb/src/pygitweb/dependencies.py
+++ b/pygitweb/src/pygitweb/dependencies.py
@@ -8,6 +8,7 @@ import ipaddress
 from typing import Annotated, Any
 
 from fastapi import Depends, Form, HTTPException, Query, Request
+from pygittools.validation import is_valid_project
 
 from pygitweb.auth import (
     access_token_from_request,
@@ -16,7 +17,6 @@ from pygitweb.auth import (
 )
 from pygitweb.config import settings
 from pygitweb.projects import git_get_projects_list, project_in_list
-from pygitweb.validation import is_valid_project
 
 
 def _visible_projects_list() -> list[dict[str, Any]]:
diff --git a/pygitweb/src/pygitweb/main.py b/pygitweb/src/pygitweb/main.py
index 93110dd..f7e5a34 100644
--- a/pygitweb/src/pygitweb/main.py
+++ b/pygitweb/src/pygitweb/main.py
@@ -18,6 +18,7 @@ import pygit2
 from fastapi import Depends, FastAPI, File, Form, HTTPException, Query, Request, UploadFile
 from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse, Response
 from fastapi.staticfiles import StaticFiles
+from pygittools.validation import is_valid_pathname
 
 from pygitweb import __meta__
 from pygitweb.actions import (
@@ -127,7 +128,6 @@ from pygitweb.tasks import (
 from pygitweb.team import team_router
 from pygitweb.templates_env import Cell, html_join, html_link, jinja_escape, render_page, trusted_html
 from pygitweb.timeline_cache import clear_timeline_cache_async, get_all_timeline_events, warm_timeline_cache_async
-from pygitweb.validation import is_valid_pathname
 
 UPDATES_SUPPORTED_ACTIONS: frozenset[str] = frozenset({"history", "log", "shortlog", "heads", "tags"})
 
diff --git a/pygitweb/src/pygitweb/merge_requests.py b/pygitweb/src/pygitweb/merge_requests.py
index de02916..7217b98 100644
--- a/pygitweb/src/pygitweb/merge_requests.py
+++ b/pygitweb/src/pygitweb/merge_requests.py
@@ -1,22 +1,25 @@
-"""Merge request tag view helpers and fast-forward merge endpoint."""
+"""Merge request create / fast-forward HTTP endpoints and tag view response."""
 
 from __future__ import annotations
 
-import json
-from typing import Annotated, Any
+from typing import Annotated
 from urllib.parse import quote, urlencode
 
 import pygit2
 from fastapi import APIRouter, Depends, Form, HTTPException, Query
 from fastapi.responses import HTMLResponse, RedirectResponse
-from pygittools.merge import MergeRequest, MergeRequestStatus, get_merge_request_by_oid
+from pygittools.merge import (
+    MergeRequest,
+    merge_request_status_fields,
+    resolve_merge_request_tag_to_tip,
+)
+from pygittools.validation import is_valid_ref_format
 
 from pygitweb.auth import access_token_from_request, principal_from_session, require_permission
 from pygitweb.dependencies import ValidatedReadableFormProject, ValidatedReadableQueryProject
 from pygitweb.git_helpers import open_repo, resolve_mr_opener
 from pygitweb.permissions import Permission
 from pygitweb.templates_env import render_page
-from pygitweb.validation import is_valid_ref_format
 
 
 def _branch_short_name(ref: str) -> str:
@@ -25,61 +28,6 @@ def _branch_short_name(ref: str) -> str:
 
 merge_router = APIRouter(tags=["merge_requests"])
 
-_STATUS_LABELS: dict[MergeRequestStatus, str] = {
-    MergeRequestStatus.CAN_FAST_FORWARD: "Can fast-forward",
-    MergeRequestStatus.UP_TO_DATE: "Already up to date",
-    MergeRequestStatus.BLOCKED_DIVERGED: "Blocked: histories diverged (not fast-forward)",
-    MergeRequestStatus.OURS_REF_MISSING: "Blocked: ours ref is missing",
-    MergeRequestStatus.MR_HEAD_MISSING: "Blocked: merge request head is missing from the repository",
-}
-
-
-def try_merge_request_from_tag(repo: pygit2.Repository, tag: pygit2.Tag) -> MergeRequest | None:
-    try:
-        return get_merge_request_by_oid(repo, tag.id)
-    except (ValueError, json.JSONDecodeError, TypeError, KeyError):
-        return None
-
-
-def resolve_merge_request_tag_to_tip(
-    repo: pygit2.Repository, obj: pygit2.Tag
-) -> tuple[pygit2.Tag, MergeRequest] | None:
-    """If ``obj`` is an MR tag and ``refs/tags/mr/…`` points to a newer tag object, return that tip."""
-    mr = try_merge_request_from_tag(repo, obj)
-    if mr is None:
-        return None
-    if mr.name not in repo.references:
-        return obj, mr
-    tip_id = repo.references[mr.name].resolve().target
-    if tip_id == obj.id:
-        return obj, mr
-    tip_obj = repo[tip_id]
-    if not isinstance(tip_obj, pygit2.Tag):
-        return obj, mr
-    mr_tip = try_merge_request_from_tag(repo, tip_obj)
-    if mr_tip is None:
-        return obj, mr
-    return tip_obj, mr_tip
-
-
-def merge_request_status_fields(repo: pygit2.Repository, mr: MergeRequest) -> dict[str, Any]:
-    try:
-        status = mr.check_status(repo)
-    except ValueError as exc:
-        return {
-            "status_code": "error",
-            "status_label": "Unable to analyze merge",
-            "status_detail": str(exc),
-            "show_merge_button": False,
-        }
-    label = _STATUS_LABELS.get(status, status.value)
-    return {
-        "status_code": status.value,
-        "status_label": label,
-        "status_detail": "",
-        "show_merge_button": status == MergeRequestStatus.CAN_FAST_FORWARD,
-    }
-
 
 def _head_view_url(project: str, repo: pygit2.Repository, head_hex: str) -> tuple[str, str]:
     try:
diff --git a/pygitweb/src/pygitweb/projects.py b/pygitweb/src/pygitweb/projects.py
index d107820..0178ec3 100644
--- a/pygitweb/src/pygitweb/projects.py
+++ b/pygitweb/src/pygitweb/projects.py
@@ -12,9 +12,9 @@ from collections.abc import Callable
 from typing import Any
 
 import pygit2
+from pygittools.validation import check_export_ok
 
 from pygitweb.config import settings
-from pygitweb.validation import check_export_ok
 
 
 def project_in_list(
diff --git a/pygitweb/src/pygitweb/smart_http.py b/pygitweb/src/pygitweb/smart_http.py
index f255722..0276d26 100644
--- a/pygitweb/src/pygitweb/smart_http.py
+++ b/pygitweb/src/pygitweb/smart_http.py
@@ -15,6 +15,7 @@ from typing import Annotated, Literal
 import pygit2
 from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
 from fastapi.responses import Response
+from pygittools.validation import is_valid_project
 
 from pygitweb.auth import (
     ACCESS_TOKEN_COOKIE_NAME,
@@ -29,7 +30,6 @@ from pygitweb.auth import (
 )
 from pygitweb.config import settings
 from pygitweb.dependencies import project_visible_in_list, require_valid_project
-from pygitweb.validation import is_valid_project
 
 SmartHttpService = Literal["git-upload-pack", "git-receive-pack"]
 
diff --git a/pygitweb/src/pygitweb/validation.py b/pygitweb/src/pygitweb/validation.py
deleted file mode 100644
index 328ba5e..0000000
--- a/pygitweb/src/pygitweb/validation.py
+++ /dev/null
@@ -1,75 +0,0 @@
-"""
-Validation: pathname, ref format, refname, project, action; repo discovery via pygit2.
-Ported from gitweb/gitweb.perl (is_valid_pathname, is_valid_ref_format, is_valid_refname,
-is_valid_project, is_valid_action). Repo check uses pygit2.discover_repository..
-"""
-
-from __future__ import annotations
-
-import os
-import re
-from collections.abc import Callable
-
-import pygit2
-
-
-def is_valid_pathname(input_path: str | None) -> bool:
-    """No '.', '..' as path elements, no null, no doubled slashes. Port of is_valid_pathname."""
-    if input_path is None:
-        return False
-    if "\0" in input_path:
-        return False
-    parts = input_path.strip("/").split("/")
-    return all(p not in ("", ".", "..") for p in parts)
-
-
-def is_valid_ref_format(input_ref: str | None) -> bool:
-    """Git-check-ref-format rules: no /., no .., no control/space/special at start/end. Port of is_valid_ref_format."""
-    if input_ref is None:
-        return False
-    if "/." in input_ref or input_ref.startswith(".") or ".." in input_ref:
-        return False
-    if input_ref.endswith("/") or input_ref.endswith(".lock"):
-        return False
-    # No ASCII control, space, ~^:?*[
-    return not re.search(r"[\x00-\x20\x7f ~^:?*\[\]]", input_ref)
-
-
-def check_export_ok(
-    git_dir: str,
-    export_ok: str = "",
-    export_auth_hook: Callable[[str], bool] | None = None,
-    across_fs: bool = False,
-) -> bool:
-    """True if path is a git repo (via pygit2.discover_repository) and optional export_ok file / auth hook pass."""
-    if not os.path.isdir(git_dir):
-        return False
-    try:
-        discovered = pygit2.discover_repository(git_dir, across_fs)
-        if not discovered:
-            return False
-    except (KeyError, pygit2.GitError, OSError):
-        return False
-    if export_ok and not os.path.isfile(os.path.join(git_dir, export_ok)):
-        return False
-    return not export_auth_hook or export_auth_hook(git_dir)
-
-
-def is_valid_project(
-    project: str | None,
-    projectroot: str,
-    export_ok: str,
-    strict_export: bool,
-    project_in_list: Callable[[str], bool],
-) -> bool:
-    """Pathname valid, dir exists, export_ok, and (if strict) in project list. Port of is_valid_project."""
-    if project is None:
-        return False
-    if not is_valid_pathname(project):
-        return False
-    full = os.path.join(projectroot, project)
-    if not os.path.isdir(full):
-        return False
-    if not check_export_ok(full, export_ok):
-        return False
-    return not strict_export or project_in_list(project)
diff --git a/pygitweb/src/pygitweb/validation_test.py b/pygitweb/src/pygitweb/validation_test.py
deleted file mode 100644
index f812f41..0000000
--- a/pygitweb/src/pygitweb/validation_test.py
+++ /dev/null
@@ -1,167 +0,0 @@
-from __future__ import annotations
-
-import os
-import stat
-import sys
-from collections.abc import Generator
-from pathlib import Path
-
-import pygit2
-import pytest
-
-from pygitweb.validation import check_export_ok, is_valid_pathname, is_valid_project, is_valid_ref_format
-
-
-class TestValidation:
-    @pytest.fixture(scope="class")
-    def project_env(self, tmp_path_factory: pytest.TempPathFactory) -> Generator[dict[str, str], None, None]:
-        projectroot: Path = tmp_path_factory.mktemp("validation-projects")
-        valid_dir: Path = projectroot / "valid"
-        invalid_dir: Path = projectroot / "invalid"
-        unreachable_dir: Path = projectroot / "unreachable"
-
-        valid_dir.mkdir()
-        invalid_dir.mkdir()
-        unreachable_dir.mkdir()
-
-        pygit2.init_repository(str(valid_dir), bare=True)
-        (valid_dir / "git-daemon-export-ok").write_text("", encoding="utf-8")
-
-        pygit2.init_repository(str(unreachable_dir), bare=True)
-        (unreachable_dir / "git-daemon-export-ok").write_text("", encoding="utf-8")
-        original_mode: int = stat.S_IMODE(unreachable_dir.stat().st_mode)
-        os.chmod(unreachable_dir, 0)
-
-        try:
-            yield {
-                "projectroot": str(projectroot),
-                "valid_name": "valid",
-                "invalid_name": "invalid",
-                "unreachable_name": "unreachable",
-                "valid_dir": str(valid_dir),
-                "invalid_dir": str(invalid_dir),
-                "unreachable_dir": str(unreachable_dir),
-                "export_ok": "git-daemon-export-ok",
-            }
-        finally:
-            os.chmod(unreachable_dir, original_mode)
-
-    class TestPathname:
-        def test_is_valid_pathname_success(self) -> None:
-            assert is_valid_pathname("group/project.git") is True
-
-        @pytest.mark.parametrize("input_path", [None, "a//b", "a/./b", "a/../b", "a\0b"])
-        def test_is_valid_pathname_failure_modes(self, input_path: str | None) -> None:
-            assert is_valid_pathname(input_path) is False
-
-    class TestRefFormat:
-        def test_is_valid_ref_format_success(self) -> None:
-            assert is_valid_ref_format("refs/heads/main") is True
-
-        @pytest.mark.parametrize(
-            "input_ref",
-            [None, ".bad", "refs/heads/../x", "refs/heads/x.lock", "refs/heads/x ", "refs/heads/x?y"],
-        )
-        def test_is_valid_ref_format_failure_modes(self, input_ref: str | None) -> None:
-            assert is_valid_ref_format(input_ref) is False
-
-    class TestCheckExportOk:
-        def test_check_export_ok_success(self, project_env: dict[str, str]) -> None:
-            assert check_export_ok(project_env["valid_dir"], project_env["export_ok"]) is True
-
-        def test_check_export_ok_fails_for_missing_directory(self, project_env: dict[str, str]) -> None:
-            missing_dir: str = str(Path(project_env["projectroot"]) / "missing")
-            assert check_export_ok(missing_dir, project_env["export_ok"]) is False
-
-        def test_check_export_ok_fails_for_non_repo_directory(self, project_env: dict[str, str]) -> None:
-            assert check_export_ok(project_env["invalid_dir"], project_env["export_ok"]) is False
-
-        def test_check_export_ok_fails_for_missing_export_file(self, project_env: dict[str, str]) -> None:
-            assert check_export_ok(project_env["valid_dir"], "missing-export-ok") is False
-
-        def test_check_export_ok_fails_when_hook_rejects(self, project_env: dict[str, str]) -> None:
-            assert (
-                check_export_ok(
-                    project_env["valid_dir"],
-                    project_env["export_ok"],
-                    export_auth_hook=lambda _path: False,
-                )
-                is False
-            )
-
-        @pytest.mark.skipif(sys.platform == "win32", reason="chmod-based unreachable directory check is not portable")
-        def test_check_export_ok_fails_for_unreachable_repo(self, project_env: dict[str, str]) -> None:
-            assert check_export_ok(project_env["unreachable_dir"], project_env["export_ok"]) is False
-
-    class TestValidProject:
-        def test_is_valid_project_success(self, project_env: dict[str, str]) -> None:
-            assert (
-                is_valid_project(
-                    project=project_env["valid_name"],
-                    projectroot=project_env["projectroot"],
-                    export_ok=project_env["export_ok"],
-                    strict_export=False,
-                    project_in_list=lambda _project: True,
-                )
-                is True
-            )
-
-        def test_is_valid_project_fails_for_none(self, project_env: dict[str, str]) -> None:
-            assert (
-                is_valid_project(
-                    project=None,
-                    projectroot=project_env["projectroot"],
-                    export_ok=project_env["export_ok"],
-                    strict_export=False,
-                    project_in_list=lambda _project: True,
-                )
-                is False
-            )
-
-        def test_is_valid_project_fails_for_invalid_pathname(self, project_env: dict[str, str]) -> None:
-            assert (
-                is_valid_project(
-                    project="bad/../name",
-                    projectroot=project_env["projectroot"],
-                    export_ok=project_env["export_ok"],
-                    strict_export=False,
-                    project_in_list=lambda _project: True,
-                )
-                is False
-            )
-
-        def test_is_valid_project_fails_for_missing_directory(self, project_env: dict[str, str]) -> None:
-            assert (
-                is_valid_project(
-                    project="missing",
-                    projectroot=project_env["projectroot"],
-                    export_ok=project_env["export_ok"],
-                    strict_export=False,
-                    project_in_list=lambda _project: True,
-                )
-                is False
-            )
-
-        def test_is_valid_project_fails_when_export_check_fails(self, project_env: dict[str, str]) -> None:
-            assert (
-                is_valid_project(
-                    project=project_env["invalid_name"],
-                    projectroot=project_env["projectroot"],
-                    export_ok=project_env["export_ok"],
-                    strict_export=False,
-                    project_in_list=lambda _project: True,
-                )
-                is False
-            )
-
-        def test_is_valid_project_fails_strict_export_when_not_listed(self, project_env: dict[str, str]) -> None:
-            assert (
-                is_valid_project(
-                    project=project_env["valid_name"],
-                    projectroot=project_env["projectroot"],
-                    export_ok=project_env["export_ok"],
-                    strict_export=True,
-                    project_in_list=lambda _project: False,
-                )
-                is False
-            )
