diff --git a/distgit/auth.py b/distgit/auth.py
index 456ad18..7143464 100644
--- a/distgit/auth.py
+++ b/distgit/auth.py
@@ -1,6 +1,7 @@
 """
 Root auth provider: base settings and interface common to all auth methods.
 """
+
 from __future__ import annotations
 
 import hmac
@@ -10,64 +11,62 @@ from datetime import timedelta
 
 
 class RootAuthProvider:
-    """
-    Base auth provider with admin credentials, session timeout, and session create/validate.
-    Subclass or use as-is for simple admin user/password auth.
-    """
+	"""
+	Base auth provider with admin credentials, session timeout, and session create/validate.
+	Subclass or use as-is for simple admin user/password auth.
+	"""
 
-    def __init__(
-        self,
-        *,
-        admin_user: bytes | None = None,
-        admin_password: bytes | None = None,
-        session_timeout: timedelta | float | None = None,  # duration; None = no expiry
-    ) -> None:
-        """
-        admin_user: optional admin username (bytes). If None, no admin login is accepted.
-        admin_password: optional admin password (bytes). If None, no admin login is accepted.
-        session_timeout: optional duration in seconds (float) or timedelta. If None, sessions do not expire.
-        """
-        self.admin_user = admin_user
-        self.admin_password = admin_password
-        if session_timeout is not None and hasattr(session_timeout, "total_seconds"):
-            self._timeout_seconds = session_timeout.total_seconds()
-        else:
-            self._timeout_seconds = session_timeout  # float or None
-        self._sessions: dict[str, float] = {}  # token -> created_at (monotonic or epoch)
+	def __init__(
+		self,
+		*,
+		admin_user: bytes | None = None,
+		admin_password: bytes | None = None,
+		session_timeout: timedelta | float | None = None,  # duration; None = no expiry
+	) -> None:
+		"""
+		admin_user: optional admin username (bytes). If None, no admin login is accepted.
+		admin_password: optional admin password (bytes). If None, no admin login is accepted.
+		session_timeout: optional duration in seconds (float) or timedelta. If None, sessions do not expire.
+		"""
+		self.admin_user = admin_user
+		self.admin_password = admin_password
+		if session_timeout is not None and hasattr(session_timeout, "total_seconds"):
+			self._timeout_seconds = session_timeout.total_seconds()
+		else:
+			self._timeout_seconds = session_timeout  # float or None
+		self._sessions: dict[str, float] = {}  # token -> created_at (monotonic or epoch)
 
-    def create_session(
-        self,
-        *,
-        user: bytes | None = None,
-        password: bytes | None = None,
-    ) -> str | None:
-        """
-        Authenticate with user/password and create a session if valid.
-        Returns a session token or None if credentials are missing or invalid.
-        """
-        if self.admin_user is None or self.admin_password is None:
-            return None
-        if user is None or password is None:
-            return None
-        if not hmac.compare_digest(user, self.admin_user) or not hmac.compare_digest(
-            password, self.admin_password
-        ):
-            return None
-        token = secrets.token_urlsafe(32)
-        self._sessions[token] = time.monotonic()
-        return token
+	def create_session(
+		self,
+		*,
+		user: bytes | None = None,
+		password: bytes | None = None,
+	) -> str | None:
+		"""
+		Authenticate with user/password and create a session if valid.
+		Returns a session token or None if credentials are missing or invalid.
+		"""
+		if self.admin_user is None or self.admin_password is None:
+			return None
+		if user is None or password is None:
+			return None
+		if not hmac.compare_digest(user, self.admin_user) or not hmac.compare_digest(password, self.admin_password):
+			return None
+		token = secrets.token_urlsafe(32)
+		self._sessions[token] = time.monotonic()
+		return token
 
-    def validate_session(self, session_token: str) -> bool:
-        """
-        Return True if the session token exists and (when session_timeout is set) is not expired.
-        """
-        if not session_token:
-            return False
-        created = self._sessions.get(session_token)
-        if created is None:
-            return False
-        if self._timeout_seconds is not None:
-            if time.monotonic() - created > self._timeout_seconds:
-                del self._sessions[session_token]
-                return False
-        return True
+	def validate_session(self, session_token: str) -> bool:
+		"""
+		Return True if the session token exists and (when session_timeout is set) is not expired.
+		"""
+		if not session_token:
+			return False
+		created = self._sessions.get(session_token)
+		if created is None:
+			return False
+		if self._timeout_seconds is not None:
+			if time.monotonic() - created > self._timeout_seconds:
+				del self._sessions[session_token]
+				return False
+		return True
diff --git a/distgit/hooks.py b/distgit/hooks.py
index d057117..6a2fbee 100644
--- a/distgit/hooks.py
+++ b/distgit/hooks.py
@@ -1,16 +1,16 @@
 from enum import Enum
-from typing import Optional
+
 import pygit2
 
 
 class HookResult(Enum):
-    SUCCESS = 0
-    FAILURE = 1
+	SUCCESS = 0
+	FAILURE = 1
 
 
-class Hook(object):
-    def __init__(self, repo: pygit2.Repository):
-        self.repo = repo
+class Hook:
+	def __init__(self, repo: pygit2.Repository):
+		self.repo = repo
 
 
 """
@@ -21,12 +21,14 @@ Use this hook to:
 - Run tests, lints, security checks, etc.
 This can be bypassed with --no-verify by the user.
 """
+
+
 class PreCommit(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(self) -> HookResult:
-        return HookResult.SUCCESS
+	def run(self) -> HookResult:
+		return HookResult.SUCCESS
 
 
 """
@@ -38,17 +40,19 @@ Use this hook to:
 - Add Signed-off-by from a template
 Takes 1–3 parameters: message file path, source (message|template|merge|squash|commit), and optionally commit hash for amend.
 """
+
+
 class PrepareCommitMsg(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(
-        self,
-        message_file: str,
-        source: str = "",
-        commit_hash: Optional[str] = None,
-    ) -> HookResult:
-        return HookResult.SUCCESS
+	def run(
+		self,
+		message_file: str,
+		source: str = "",
+		commit_hash: str | None = None,
+	) -> HookResult:
+		return HookResult.SUCCESS
 
 
 """
@@ -60,12 +64,14 @@ Use this hook to:
 - Reject the commit (e.g. duplicate Signed-off-by, missing ticket reference)
 Takes one parameter: the path to the file holding the proposed commit log message.
 """
+
+
 class CommitMsg(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(self, message_file: str) -> HookResult:
-        return HookResult.SUCCESS
+	def run(self, message_file: str) -> HookResult:
+		return HookResult.SUCCESS
 
 
 """
@@ -76,12 +82,14 @@ Use this hook to:
 - Run post-commit checks or backups
 - Update external metadata or caches
 """
+
+
 class PostCommit(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(self) -> HookResult:
-        return HookResult.SUCCESS
+	def run(self) -> HookResult:
+		return HookResult.SUCCESS
 
 
 """
@@ -93,12 +101,14 @@ Use this hook to:
 - Abort the merge commit if checks fail
 Takes no parameters.
 """
+
+
 class PreMergeCommit(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(self) -> HookResult:
-        return HookResult.SUCCESS
+	def run(self) -> HookResult:
+		return HookResult.SUCCESS
 
 
 """
@@ -109,12 +119,14 @@ Use this hook to:
 - Run checks before rewriting history
 Takes one or two parameters: upstream ref, and optionally the branch being rebased (absent when rebasing the current branch).
 """
+
+
 class PreRebase(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(self, upstream: str, branch: Optional[str] = None) -> HookResult:
-        return HookResult.SUCCESS
+	def run(self, upstream: str, branch: str | None = None) -> HookResult:
+		return HookResult.SUCCESS
 
 
 """
@@ -126,12 +138,14 @@ Use this hook to:
 - Run repository validity checks or refresh generated files
 Takes three parameters: previous HEAD ref, new HEAD ref, and a flag (1 = branch checkout, 0 = file checkout).
 """
+
+
 class PostCheckout(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(self, prev_head: str, new_head: str, branch_checkout: str) -> HookResult:
-        return HookResult.SUCCESS
+	def run(self, prev_head: str, new_head: str, branch_checkout: str) -> HookResult:
+		return HookResult.SUCCESS
 
 
 """
@@ -142,12 +156,16 @@ Use this hook to:
 - Run post-merge checks or notifications
 Takes one parameter: a status flag indicating whether the merge was a squash merge.
 """
+
+
 class PostMerge(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
+
+	def run(self, squash: str) -> HookResult:
+		return HookResult.SUCCESS
+
 
-    def run(self, squash: str) -> HookResult:
-        return HookResult.SUCCESS
 """
 Pre-Push Hook (Client-side)
 Called by git push; can be used to prevent a push.
@@ -157,12 +175,14 @@ Use this hook to:
 - Validate commits being pushed
 Takes two parameters: remote name and remote URL. Ref updates are provided on stdin.
 """
+
+
 class PrePush(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(self, remote_name: str, remote_url: str) -> HookResult:
-        return HookResult.SUCCESS
+	def run(self, remote_name: str, remote_url: str) -> HookResult:
+		return HookResult.SUCCESS
 
 
 # -----------------------------------------------------------------------------
@@ -178,12 +198,14 @@ Use this hook to:
 - Log or validate old → new for specific refs
 Takes three parameters: ref name, old object name, new object name.
 """
+
+
 class Update(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(self, ref_name: str, old_oid: str, new_oid: str) -> HookResult:
-        return HookResult.SUCCESS
+	def run(self, ref_name: str, old_oid: str, new_oid: str) -> HookResult:
+		return HookResult.SUCCESS
 
 
 """
@@ -195,12 +217,14 @@ Use this hook to:
 - Update caches or derived data
 Takes a variable number of parameters: the name of each ref that was updated.
 """
+
+
 class PostUpdate(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(self, *ref_names: str) -> HookResult:
-        return HookResult.SUCCESS
+	def run(self, *ref_names: str) -> HookResult:
+		return HookResult.SUCCESS
 
 
 """
@@ -212,12 +236,14 @@ Use this hook to:
 - Refuse the push by exiting non-zero (without modifying index or worktree)
 Takes one parameter: the commit object name the tip of the current branch will be updated to.
 """
+
+
 class PushToCheckout(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(self, new_commit: str) -> HookResult:
-        return HookResult.SUCCESS
+	def run(self, new_commit: str) -> HookResult:
+		return HookResult.SUCCESS
 
 
 """
@@ -229,12 +255,14 @@ Use this hook to:
 - Notify or log that auto-gc is about to run
 Takes no parameters. Exiting with non-zero status prevents gc from running.
 """
+
+
 class PreAutoGc(Hook):
-    def __init__(self, repo: pygit2.Repository):
-        super().__init__(repo)
+	def __init__(self, repo: pygit2.Repository):
+		super().__init__(repo)
 
-    def run(self) -> HookResult:
-        return HookResult.SUCCESS
+	def run(self) -> HookResult:
+		return HookResult.SUCCESS
 
 
 # -----------------------------------------------------------------------------
diff --git a/distgit/hooks_patterns.py b/distgit/hooks_patterns.py
index 73c2abe..b88b90c 100644
--- a/distgit/hooks_patterns.py
+++ b/distgit/hooks_patterns.py
@@ -1,24 +1,29 @@
-from distgit.hooks import Hook, HookResult, CommitMsg, Update
 import re
 from sys import stderr
+
 import pygit2
 
+from distgit.hooks import CommitMsg, Hook, HookResult
+
 """
 Commit-Msg Pattern Hook (Client-side)
 Validates the commit message against a regular expression.
 """
+
+
 class CommitMsgPattern(CommitMsg):
-    def __init__(self, repo: pygit2.Repository, pattern: str):
-        super().__init__(repo)
-        self.exp = re.compile(pattern)
-
-    def run(self, message_file: str) -> HookResult:
-        with open(message_file, "r") as f:
-            message = f.read()
-        if not self.exp.match(message):
-            stderr.write(f"Commit message does not match pattern: {self.exp.pattern}\n")
-            return HookResult.FAILURE
-        return HookResult.SUCCESS
+	def __init__(self, repo: pygit2.Repository, pattern: str):
+		super().__init__(repo)
+		self.exp = re.compile(pattern)
+
+	def run(self, message_file: str) -> HookResult:
+		with open(message_file) as f:
+			message = f.read()
+		if not self.exp.match(message):
+			stderr.write(f"Commit message does not match pattern: {self.exp.pattern}\n")
+			return HookResult.FAILURE
+		return HookResult.SUCCESS
+
 
 """
 Update Pattern Hook (Server-side)
@@ -29,16 +34,18 @@ Use this hook to:
 - Log or validate old → new for specific refs
 Takes three parameters: ref name, old object name, new object name.
 """
+
+
 class UpdatePattern(Hook):
-    def __init__(self, repo: pygit2.Repository, ref_pattern: str, msg_pattern: str):
-        super().__init__(repo)
-        self.ref_exp = re.compile(ref_pattern)
-        self.msg_exp = re.compile(msg_pattern)
-
-    def run(self, ref_name: str, old_oid: str, new_oid: str) -> HookResult:
-        if self.ref_exp.match(ref_name):
-            commit = self.repo.revparse_single(new_oid).peel(pygit2.Commit)
-            if not self.msg_exp.match(commit.message):
-                stderr.write(f"Commit message does not match pattern: {self.msg_exp.pattern}\n")
-                return HookResult.FAILURE
-        return HookResult.SUCCESS
+	def __init__(self, repo: pygit2.Repository, ref_pattern: str, msg_pattern: str):
+		super().__init__(repo)
+		self.ref_exp = re.compile(ref_pattern)
+		self.msg_exp = re.compile(msg_pattern)
+
+	def run(self, ref_name: str, old_oid: str, new_oid: str) -> HookResult:
+		if self.ref_exp.match(ref_name):
+			commit = self.repo.revparse_single(new_oid).peel(pygit2.Commit)
+			if not self.msg_exp.match(commit.message):
+				stderr.write(f"Commit message does not match pattern: {self.msg_exp.pattern}\n")
+				return HookResult.FAILURE
+		return HookResult.SUCCESS
diff --git a/distgit/tasks.py b/distgit/tasks.py
index fb367c0..e6026c5 100644
--- a/distgit/tasks.py
+++ b/distgit/tasks.py
@@ -1,10 +1,11 @@
 import json
-from random import randint
-from pygit2 import Signature, Tag, Oid, reference_is_valid_name, Repository
 from datetime import datetime
+from enum import Enum
 from json import JSONEncoder
+from random import randint
 from typing import Any
-from enum import Enum
+
+from pygit2 import Oid, Repository, Signature, Tag, reference_is_valid_name
 
 # GIT_OBJECT_TAG = 4, GIT_OBJECT_TREE = 2
 GIT_OBJECT_TAG = 4
@@ -13,59 +14,53 @@ EMPTY_TREE_OID_HEX = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
 
 
 def _ensure_empty_tree(repo: Repository) -> None:
-    """Ensure the well-known empty tree object exists in the repo ODB (so tags can point to it)."""
-    try:
-        repo[Oid(hex=EMPTY_TREE_OID_HEX)]
-    except KeyError:
-        repo.odb.write(GIT_OBJECT_TREE, b"tree 0\0")
+	"""Ensure the well-known empty tree object exists in the repo ODB (so tags can point to it)."""
+	try:
+		repo[Oid(hex=EMPTY_TREE_OID_HEX)]
+	except KeyError:
+		repo.odb.write(GIT_OBJECT_TREE, b"tree 0\0")
 
 
 class _DateTimeEncoder(JSONEncoder):
-    def default(self, o: Any) -> Any:
-        if hasattr(o, "isoformat"):
-            return o.isoformat()
-        if isinstance(o, Enum):
-            return o.value
-        return super().default(o)
+	def default(self, o: Any) -> Any:
+		if hasattr(o, "isoformat"):
+			return o.isoformat()
+		if isinstance(o, Enum):
+			return o.value
+		return super().default(o)
 
 
 def _tagger_str(tagger: str | Signature | None) -> str:
-    """Produce a tagger line for git tag object (name <email> timestamp +tz). Accepts str or pygit2.Signature."""
-    if isinstance(tagger, Signature):
-        return f"{tagger.name} <{tagger.email}> {tagger.time} {tagger.offset}"
-    if isinstance(tagger, str):
-        if tagger and " <" in tagger and ">" in tagger:
-            return tagger + " 0 +0000"
-        return (tagger or "distgit <distgit@local>") + " 0 +0000"
-    return "distgit <distgit@local> 0 +0000"
+	"""Produce a tagger line for git tag object (name <email> timestamp +tz). Accepts str or pygit2.Signature."""
+	if isinstance(tagger, Signature):
+		return f"{tagger.name} <{tagger.email}> {tagger.time} {tagger.offset}"
+	if isinstance(tagger, str):
+		if tagger and " <" in tagger and ">" in tagger:
+			return tagger + " 0 +0000"
+		return (tagger or "distgit <distgit@local>") + " 0 +0000"
+	return "distgit <distgit@local> 0 +0000"
 
 
 def _build_tag_raw(
-    target: Oid,
-    name: str,
-    tagger: str,
-    message: str,
-    object_type: str = "commit",
+	target: Oid,
+	name: str,
+	tagger: str,
+	message: str,
+	object_type: str = "commit",
 ) -> bytes:
-    """Build tag object body (content only) for odb.write(4, data). libgit2 prepends 'tag <len>\\0'. object_type must match the target (e.g. 'commit' or 'tree')."""
-    content = (
-        f"object {str(target)}\n"
-        f"type {object_type}\n"
-        f"tag {name}\n"
-        f"tagger {_tagger_str(tagger)}\n"
-        f"\n{message}"
-    )
-    return content.encode("utf-8")
+	"""Build tag object body (content only) for odb.write(4, data). libgit2 prepends 'tag <len>\\0'. object_type must match the target (e.g. 'commit' or 'tree')."""
+	content = f"object {str(target)}\ntype {object_type}\ntag {name}\ntagger {_tagger_str(tagger)}\n\n{message}"
+	return content.encode("utf-8")
 
 
 def _read_tag_raw(repo: Repository, oid: Oid) -> tuple[Oid, str, str, str]:
-    """Read a tag from ODB; return (target, name, tagger_str, message)."""
-    obj = repo[oid]
-    if not isinstance(obj, Tag):
-        raise ValueError(f"Not a tag: {oid}")
-    tagger = obj.tagger
-    tagger_str = f"{tagger.name} <{tagger.email}> {tagger.time} {tagger.offset}" if tagger else ""
-    return (obj.target, obj.name, tagger_str, obj.message)
+	"""Read a tag from ODB; return (target, name, tagger_str, message)."""
+	obj = repo[oid]
+	if not isinstance(obj, Tag):
+		raise ValueError(f"Not a tag: {oid}")
+	tagger = obj.tagger
+	tagger_str = f"{tagger.name} <{tagger.email}> {tagger.time} {tagger.offset}" if tagger else ""
+	return (obj.target, obj.name, tagger_str, obj.message)
 
 
 """
@@ -78,131 +73,133 @@ Because the ODB is immutable, the tag will always point to the latest version of
 
 
 class Task:
-    class Status(Enum):
-        TODO = "TODO"
-        IN_PROGRESS = "IN_PROGRESS"
-        IN_REVIEW = "IN_REVIEW"
-        DONE = "DONE"
-        CANCELLED = "CANCELLED"
-
-    class Priority(Enum):
-        LOW = "LOW"
-        MEDIUM = "MEDIUM"
-        HIGH = "HIGH"
-        CRITICAL = "CRITICAL"
-
-    def __init__(
-        self,
-        target : Oid,
-        name : str,
-        tagger : str,
-        title : str,
-        description : str = "",
-        status : Status | None = None,
-        priority : Priority | None = None,
-        assignee : str | None = None,
-        due_date : datetime | None = None
-    ):
-        if name is None:
-            name = f"tasks/{title.lower().replace(' ', '_')}"
-
-        if not reference_is_valid_name(name):
-            raise ValueError(f"Invalid task backend name: '{name}'")
-
-        self.target = target
-        self.tagger = tagger or ""
-        self.name = name
-        self.title = title
-        self.description = description
-        self.status = status
-        self.priority = priority
-        self.assignee = assignee
-        self.due_date = due_date
-        self.created_at = datetime.now()
-        self.comments = [] # Comment OIDs
-        self.update_message()
-
-    def update_message(self) -> None:
-        self.updated_at = datetime.now()
-        self.message = _DateTimeEncoder().encode(
-            {k: v for k, v in self.__dict__.items() if k not in ["target", "tagger", "name", "message"]}
-        )
-
-    """
+	class Status(Enum):
+		TODO = "TODO"
+		IN_PROGRESS = "IN_PROGRESS"
+		IN_REVIEW = "IN_REVIEW"
+		DONE = "DONE"
+		CANCELLED = "CANCELLED"
+
+	class Priority(Enum):
+		LOW = "LOW"
+		MEDIUM = "MEDIUM"
+		HIGH = "HIGH"
+		CRITICAL = "CRITICAL"
+
+	def __init__(
+		self,
+		target: Oid,
+		name: str,
+		tagger: str,
+		title: str,
+		description: str = "",
+		status: Status | None = None,
+		priority: Priority | None = None,
+		assignee: str | None = None,
+		due_date: datetime | None = None,
+	):
+		if name is None:
+			name = f"tasks/{title.lower().replace(' ', '_')}"
+
+		if not reference_is_valid_name(name):
+			raise ValueError(f"Invalid task backend name: '{name}'")
+
+		self.target = target
+		self.tagger = tagger or ""
+		self.name = name
+		self.title = title
+		self.description = description
+		self.status = status
+		self.priority = priority
+		self.assignee = assignee
+		self.due_date = due_date
+		self.created_at = datetime.now()
+		self.comments = []  # Comment OIDs
+		self.update_message()
+
+	def update_message(self) -> None:
+		self.updated_at = datetime.now()
+		self.message = _DateTimeEncoder().encode(
+			{k: v for k, v in self.__dict__.items() if k not in ["target", "tagger", "name", "message"]}
+		)
+
+	"""
     Write the task to the repository.
     @param repo: The repository to write to.
     @return: The OID of the written task.
     """
-    def write(self, repo: Repository) -> Oid:
-        try:
-            obj = repo[self.target]
-            object_type = (getattr(obj, "type_str", None) or "commit").lower()
-        except KeyError:
-            if str(self.target) == EMPTY_TREE_OID_HEX:
-                _ensure_empty_tree(repo)
-                object_type = "tree"
-            else:
-                object_type = "commit"
-        raw = _build_tag_raw(self.target, self.name, self.tagger, self.message, object_type=object_type)
-        oid = repo.odb.write(GIT_OBJECT_TAG, raw)
-        repo.references.create(self.name, oid, force=True)
-        return oid
+
+	def write(self, repo: Repository) -> Oid:
+		try:
+			obj = repo[self.target]
+			object_type = (getattr(obj, "type_str", None) or "commit").lower()
+		except KeyError:
+			if str(self.target) == EMPTY_TREE_OID_HEX:
+				_ensure_empty_tree(repo)
+				object_type = "tree"
+			else:
+				object_type = "commit"
+		raw = _build_tag_raw(self.target, self.name, self.tagger, self.message, object_type=object_type)
+		oid = repo.odb.write(GIT_OBJECT_TAG, raw)
+		repo.references.create(self.name, oid, force=True)
+		return oid
+
 
 def _parse_dt(s):
-    if not s:
-        return None
-    if hasattr(s, "isoformat"):
-        return s
-    return datetime.fromisoformat(str(s).replace("Z", "+00:00")) if s else None
+	if not s:
+		return None
+	if hasattr(s, "isoformat"):
+		return s
+	return datetime.fromisoformat(str(s).replace("Z", "+00:00")) if s else None
 
 
 def get_task(repo: Repository, ref: str) -> Task | None:
-    tag = repo.revparse_single(ref)
-    if not isinstance(tag, Tag):
-        raise ValueError(f"Requested task is not a tag: {ref}")
-    j = json.loads(tag.message)
-    status = Task.Status(j["status"]) if j.get("status") else None
-    priority = Task.Priority(j["priority"]) if j.get("priority") else None
-    t = Task(
-        tag.target,
-        tag.name,
-        tag.tagger,
-        j.get("title", "Untitled"),
-        description=j.get("description", ""),
-        status=status,
-        priority=priority,
-        assignee=j.get("assignee"),
-        due_date=_parse_dt(j.get("due_date")),
-    )
-    t.comments = j.get("comments", [])
-    t.created_at = _parse_dt(j.get("created_at")) or t.created_at
-    t.updated_at = _parse_dt(j.get("updated_at")) or t.updated_at
-    return t
+	tag = repo.revparse_single(ref)
+	if not isinstance(tag, Tag):
+		raise ValueError(f"Requested task is not a tag: {ref}")
+	j = json.loads(tag.message)
+	status = Task.Status(j["status"]) if j.get("status") else None
+	priority = Task.Priority(j["priority"]) if j.get("priority") else None
+	t = Task(
+		tag.target,
+		tag.name,
+		tag.tagger,
+		j.get("title", "Untitled"),
+		description=j.get("description", ""),
+		status=status,
+		priority=priority,
+		assignee=j.get("assignee"),
+		due_date=_parse_dt(j.get("due_date")),
+	)
+	t.comments = j.get("comments", [])
+	t.created_at = _parse_dt(j.get("created_at")) or t.created_at
+	t.updated_at = _parse_dt(j.get("updated_at")) or t.updated_at
+	return t
 
 
 def get_task_by_oid(repo: Repository, oid: Oid) -> Task | None:
-    """Load a task by its tag OID (e.g. from board.tasks)."""
-    obj = repo[oid]
-    if not isinstance(obj, Tag):
-        raise ValueError(f"Object is not a task tag: {oid}")
-    j = json.loads(obj.message)
-    status = Task.Status(j["status"]) if j.get("status") else None
-    priority = Task.Priority(j["priority"]) if j.get("priority") else None
-    t = Task(
-        obj.target,
-        obj.name,
-        obj.tagger,
-        j.get("title", "Untitled"),
-        description=j.get("description", ""),
-        status=status,
-        priority=priority,
-        assignee=j.get("assignee"),
-        due_date=_parse_dt(j.get("due_date")),
-    )
-    t.comments = j.get("comments", [])
-    t.created_at = _parse_dt(j.get("created_at")) or t.created_at
-    t.updated_at = _parse_dt(j.get("updated_at")) or t.updated_at
-    return t
+	"""Load a task by its tag OID (e.g. from board.tasks)."""
+	obj = repo[oid]
+	if not isinstance(obj, Tag):
+		raise ValueError(f"Object is not a task tag: {oid}")
+	j = json.loads(obj.message)
+	status = Task.Status(j["status"]) if j.get("status") else None
+	priority = Task.Priority(j["priority"]) if j.get("priority") else None
+	t = Task(
+		obj.target,
+		obj.name,
+		obj.tagger,
+		j.get("title", "Untitled"),
+		description=j.get("description", ""),
+		status=status,
+		priority=priority,
+		assignee=j.get("assignee"),
+		due_date=_parse_dt(j.get("due_date")),
+	)
+	t.comments = j.get("comments", [])
+	t.created_at = _parse_dt(j.get("created_at")) or t.created_at
+	t.updated_at = _parse_dt(j.get("updated_at")) or t.updated_at
+	return t
 
 
 """
@@ -213,57 +210,59 @@ Todo: this will not play nice with gc...
 
 
 class Comment:
-    def __init__(
-        self,
-        target: Oid,  # Task or parent comment OID
-        tagger: str,  # Author of the comment
-        content: str,
-        name: str | None = None,  # "comments/<comment-id>" - Note this will NOT get a ref by default
-    ):
-        if name is None:
-            name = f"comments/{randint(1, 2147483647)}"
-
-        self.target = target
-        self.tagger = tagger or ""
-        self.name = name
-        self.content = content
-        self.created_at = datetime.now()
-        self.edited_at = None
-        self.update_message()
-
-    def update_message(self) -> None:
-        self.updated_at = datetime.now()
-        self.message = _DateTimeEncoder().encode(
-            {k: v for k, v in self.__dict__.items() if k not in ["target", "tagger", "name", "message"]}
-        )
-
-    """
+	def __init__(
+		self,
+		target: Oid,  # Task or parent comment OID
+		tagger: str,  # Author of the comment
+		content: str,
+		name: str | None = None,  # "comments/<comment-id>" - Note this will NOT get a ref by default
+	):
+		if name is None:
+			name = f"comments/{randint(1, 2147483647)}"
+
+		self.target = target
+		self.tagger = tagger or ""
+		self.name = name
+		self.content = content
+		self.created_at = datetime.now()
+		self.edited_at = None
+		self.update_message()
+
+	def update_message(self) -> None:
+		self.updated_at = datetime.now()
+		self.message = _DateTimeEncoder().encode(
+			{k: v for k, v in self.__dict__.items() if k not in ["target", "tagger", "name", "message"]}
+		)
+
+	"""
     Write the comment to the repository.
     @param repo: The repository to write to.
     @return: The OID of the written comment.
     """
-    def write(self, repo: Repository) -> Oid:
-        try:
-            obj = repo[self.target]
-            object_type = (getattr(obj, "type_str", None) or "commit").lower()
-        except KeyError:
-            if str(self.target) == EMPTY_TREE_OID_HEX:
-                _ensure_empty_tree(repo)
-                object_type = "tree"
-            else:
-                object_type = "commit"
-        raw = _build_tag_raw(self.target, self.name, self.tagger, self.message, object_type=object_type)
-        return repo.odb.write(GIT_OBJECT_TAG, raw)
+
+	def write(self, repo: Repository) -> Oid:
+		try:
+			obj = repo[self.target]
+			object_type = (getattr(obj, "type_str", None) or "commit").lower()
+		except KeyError:
+			if str(self.target) == EMPTY_TREE_OID_HEX:
+				_ensure_empty_tree(repo)
+				object_type = "tree"
+			else:
+				object_type = "commit"
+		raw = _build_tag_raw(self.target, self.name, self.tagger, self.message, object_type=object_type)
+		return repo.odb.write(GIT_OBJECT_TAG, raw)
+
 
 def get_comment(repo: Repository, oid: Oid) -> Comment | None:
-    obj = repo[oid]
-    if not isinstance(obj, Tag):
-        raise ValueError(f"Requested comment is not a tag: {oid}")
-    j = json.loads(obj.message)
-    c = Comment(obj.target, obj.tagger, j["content"], obj.name)
-    c.created_at = _parse_dt(j.get("created_at")) or c.created_at
-    c.edited_at = _parse_dt(j.get("edited_at"))
-    return c
+	obj = repo[oid]
+	if not isinstance(obj, Tag):
+		raise ValueError(f"Requested comment is not a tag: {oid}")
+	j = json.loads(obj.message)
+	c = Comment(obj.target, obj.tagger, j["content"], obj.name)
+	c.created_at = _parse_dt(j.get("created_at")) or c.created_at
+	c.edited_at = _parse_dt(j.get("edited_at"))
+	return c
 
 
 """
@@ -274,50 +273,49 @@ The "tasks" field is an array of task OIDs.
 
 
 class Board:
-    def __init__(
-        self,
-        target: Oid,
-        name: str,
-        tagger: str,
-        description: str = "",
-    ):
-        self.target = target
-        self.name = name
-        self.tagger = tagger or ""
-        self.description = description
-        self.created_at = datetime.now()
-        self.updated_at = datetime.now()
-        self.tasks = []  # Task OIDs
-        self.update_message()
-
-    def update_message(self) -> None:
-        self.updated_at = datetime.now()
-        self.message = _DateTimeEncoder().encode(
-            {k: v for k, v in self.__dict__.items() if k not in ["target", "name", "tagger", "message"]}
-        )
-
-    def write(self, repo: Repository) -> Oid:
-        if str(self.target) == EMPTY_TREE_OID_HEX:
-            _ensure_empty_tree(repo)
-        try:
-            obj = repo[self.target]
-            object_type = (getattr(obj, "type_str", None) or "commit").lower()
-        except KeyError:
-            object_type = "tree" if str(self.target) == EMPTY_TREE_OID_HEX else "commit"
-        raw = _build_tag_raw(self.target, self.name, self.tagger, self.message, object_type=object_type)
-        oid = repo.odb.write(GIT_OBJECT_TAG, raw)
-        repo.references.create(self.name, oid, force=True)
-        return oid
+	def __init__(
+		self,
+		target: Oid,
+		name: str,
+		tagger: str,
+		description: str = "",
+	):
+		self.target = target
+		self.name = name
+		self.tagger = tagger or ""
+		self.description = description
+		self.created_at = datetime.now()
+		self.updated_at = datetime.now()
+		self.tasks = []  # Task OIDs
+		self.update_message()
+
+	def update_message(self) -> None:
+		self.updated_at = datetime.now()
+		self.message = _DateTimeEncoder().encode(
+			{k: v for k, v in self.__dict__.items() if k not in ["target", "name", "tagger", "message"]}
+		)
+
+	def write(self, repo: Repository) -> Oid:
+		if str(self.target) == EMPTY_TREE_OID_HEX:
+			_ensure_empty_tree(repo)
+		try:
+			obj = repo[self.target]
+			object_type = (getattr(obj, "type_str", None) or "commit").lower()
+		except KeyError:
+			object_type = "tree" if str(self.target) == EMPTY_TREE_OID_HEX else "commit"
+		raw = _build_tag_raw(self.target, self.name, self.tagger, self.message, object_type=object_type)
+		oid = repo.odb.write(GIT_OBJECT_TAG, raw)
+		repo.references.create(self.name, oid, force=True)
+		return oid
 
 
 def get_board(repo: Repository, ref: str) -> Board | None:
-    tag = repo.revparse_single(ref)
-    if not isinstance(tag, Tag):
-        return None
-    j = json.loads(tag.message)
-    b = Board(tag.target, tag.name, tag.tagger, j.get("description", ""))
-    b.tasks = j.get("tasks", [])
-    b.created_at = _parse_dt(j.get("created_at")) or b.created_at
-    b.updated_at = _parse_dt(j.get("updated_at")) or b.updated_at
-    return b
-
+	tag = repo.revparse_single(ref)
+	if not isinstance(tag, Tag):
+		return None
+	j = json.loads(tag.message)
+	b = Board(tag.target, tag.name, tag.tagger, j.get("description", ""))
+	b.tasks = j.get("tasks", [])
+	b.created_at = _parse_dt(j.get("created_at")) or b.created_at
+	b.updated_at = _parse_dt(j.get("updated_at")) or b.updated_at
+	return b
diff --git a/postgit/__init__.py b/postgit/__init__.py
index b9e2a30..567b9b4 100644
--- a/postgit/__init__.py
+++ b/postgit/__init__.py
@@ -1,31 +1,31 @@
 from .adapters import (
-    GitObjectType,
-    OidBinaryDumper,
-    OidDumper,
-    object_data_from_pygit2,
-    object_type_from_pygit2,
-    objects_row_insert,
-    oid_from_db,
-    postgit_dict_row,
-    postgit_tuple_row,
-    ref_storage_columns,
-    register_adapters,
-    register_adapters_async,
-    regtype_git_object_type,
+	GitObjectType,
+	OidBinaryDumper,
+	OidDumper,
+	object_data_from_pygit2,
+	object_type_from_pygit2,
+	objects_row_insert,
+	oid_from_db,
+	postgit_dict_row,
+	postgit_tuple_row,
+	ref_storage_columns,
+	register_adapters,
+	register_adapters_async,
+	regtype_git_object_type,
 )
 
 __all__ = [
-    "GitObjectType",
-    "OidBinaryDumper",
-    "OidDumper",
-    "object_data_from_pygit2",
-    "object_type_from_pygit2",
-    "objects_row_insert",
-    "oid_from_db",
-    "postgit_dict_row",
-    "postgit_tuple_row",
-    "ref_storage_columns",
-    "register_adapters",
-    "register_adapters_async",
-    "regtype_git_object_type",
+	"GitObjectType",
+	"OidBinaryDumper",
+	"OidDumper",
+	"object_data_from_pygit2",
+	"object_type_from_pygit2",
+	"objects_row_insert",
+	"oid_from_db",
+	"postgit_dict_row",
+	"postgit_tuple_row",
+	"ref_storage_columns",
+	"register_adapters",
+	"register_adapters_async",
+	"regtype_git_object_type",
 ]
diff --git a/postgit/adapters.py b/postgit/adapters.py
index 8df8831..3fb30ab 100644
--- a/postgit/adapters.py
+++ b/postgit/adapters.py
@@ -10,8 +10,9 @@ Psycopg 3 adapters and row factories for PostGit tables (see schema.sql).
 
 from __future__ import annotations
 
+from collections.abc import Sequence
 from enum import IntEnum
-from typing import Any, Sequence
+from typing import Any
 
 import pygit2
 from psycopg import _oids
@@ -33,186 +34,184 @@ _OID_COLUMN_NAMES = frozenset({"oid", "target_oid"})
 
 
 class GitObjectType(IntEnum):
-    """Matches ``git_object_type`` in PostgreSQL and ``pygit2.GIT_OBJECT_*``."""
+	"""Matches ``git_object_type`` in PostgreSQL and ``pygit2.GIT_OBJECT_*``."""
 
-    COMMIT = 1
-    TREE = 2
-    BLOB = 3
-    TAG = 4
+	COMMIT = 1
+	TREE = 2
+	BLOB = 3
+	TAG = 4
 
 
 _GIT_OBJECT_TYPE_PG_MAP = {
-    GitObjectType.COMMIT: "commit",
-    GitObjectType.TREE: "tree",
-    GitObjectType.BLOB: "blob",
-    GitObjectType.TAG: "tag",
+	GitObjectType.COMMIT: "commit",
+	GitObjectType.TREE: "tree",
+	GitObjectType.BLOB: "blob",
+	GitObjectType.TAG: "tag",
 }
 
 
 class OidDumper(Dumper):
-    """Encode ``pygit2.Oid`` as PostgreSQL ``bytea`` (text parameter format)."""
+	"""Encode ``pygit2.Oid`` as PostgreSQL ``bytea`` (text parameter format)."""
 
-    oid = _oids.BYTEA_OID
+	oid = _oids.BYTEA_OID
 
-    def __init__(self, cls: type, context: AdaptContext | None = None):
-        super().__init__(cls, context)
-        self._bytes = BytesDumper(bytes, context)
+	def __init__(self, cls: type, context: AdaptContext | None = None):
+		super().__init__(cls, context)
+		self._bytes = BytesDumper(bytes, context)
 
-    def dump(self, obj: pygit2.Oid) -> Buffer | None:
-        return self._bytes.dump(obj.raw)
+	def dump(self, obj: pygit2.Oid) -> Buffer | None:
+		return self._bytes.dump(obj.raw)
 
 
 class OidBinaryDumper(Dumper):
-    """Encode ``pygit2.Oid`` as PostgreSQL ``bytea`` (binary parameter format)."""
+	"""Encode ``pygit2.Oid`` as PostgreSQL ``bytea`` (binary parameter format)."""
 
-    format = Format.BINARY
-    oid = _oids.BYTEA_OID
+	format = Format.BINARY
+	oid = _oids.BYTEA_OID
 
-    def dump(self, obj: pygit2.Oid) -> Buffer | None:
-        return obj.raw
+	def dump(self, obj: pygit2.Oid) -> Buffer | None:
+		return obj.raw
 
 
 def register_adapters(
-    conn: Connection[Any],
-    *,
-    git_object_type_regtype: str = "git_object_type",
+	conn: Connection[Any],
+	*,
+	git_object_type_regtype: str = "git_object_type",
 ) -> None:
-    """
-    Register dumpers/loaders on ``conn`` for ``pygit2.Oid`` and ``git_object_type``.
-
-    Call once per sync connection after the schema exists (enum type present).
-
-    :param git_object_type_regtype: argument to :meth:`EnumInfo.fetch` (e.g.
-        :func:`regtype_git_object_type` if the type is not on ``search_path``).
-    """
-    info = EnumInfo.fetch(conn, git_object_type_regtype)
-    if info is None:
-        raise LookupError(
-            "PostgreSQL type git_object_type not found; apply schema.sql first "
-            "(use the regtype your search_path resolves, e.g. public.git_object_type)."
-        )
-    register_enum(
-        info,
-        conn,
-        enum=GitObjectType,
-        mapping=_GIT_OBJECT_TYPE_PG_MAP,
-    )
-    conn.adapters.register_dumper(pygit2.Oid, OidDumper)
-    conn.adapters.register_dumper(pygit2.Oid, OidBinaryDumper)
+	"""
+	Register dumpers/loaders on ``conn`` for ``pygit2.Oid`` and ``git_object_type``.
+
+	Call once per sync connection after the schema exists (enum type present).
+
+	:param git_object_type_regtype: argument to :meth:`EnumInfo.fetch` (e.g.
+	    :func:`regtype_git_object_type` if the type is not on ``search_path``).
+	"""
+	info = EnumInfo.fetch(conn, git_object_type_regtype)
+	if info is None:
+		raise LookupError(
+			"PostgreSQL type git_object_type not found; apply schema.sql first "
+			"(use the regtype your search_path resolves, e.g. public.git_object_type)."
+		)
+	register_enum(
+		info,
+		conn,
+		enum=GitObjectType,
+		mapping=_GIT_OBJECT_TYPE_PG_MAP,
+	)
+	conn.adapters.register_dumper(pygit2.Oid, OidDumper)
+	conn.adapters.register_dumper(pygit2.Oid, OidBinaryDumper)
 
 
 async def register_adapters_async(
-    conn: AsyncConnection[Any],
-    *,
-    git_object_type_regtype: str = "git_object_type",
+	conn: AsyncConnection[Any],
+	*,
+	git_object_type_regtype: str = "git_object_type",
 ) -> None:
-    """Async variant of :func:`register_adapters`."""
-    info = await EnumInfo.fetch(conn, git_object_type_regtype)
-    if info is None:
-        raise LookupError(
-            "PostgreSQL type git_object_type not found; apply schema.sql first."
-        )
-    register_enum(
-        info,
-        conn,
-        enum=GitObjectType,
-        mapping=_GIT_OBJECT_TYPE_PG_MAP,
-    )
-    conn.adapters.register_dumper(pygit2.Oid, OidDumper)
-    conn.adapters.register_dumper(pygit2.Oid, OidBinaryDumper)
+	"""Async variant of :func:`register_adapters`."""
+	info = await EnumInfo.fetch(conn, git_object_type_regtype)
+	if info is None:
+		raise LookupError("PostgreSQL type git_object_type not found; apply schema.sql first.")
+	register_enum(
+		info,
+		conn,
+		enum=GitObjectType,
+		mapping=_GIT_OBJECT_TYPE_PG_MAP,
+	)
+	conn.adapters.register_dumper(pygit2.Oid, OidDumper)
+	conn.adapters.register_dumper(pygit2.Oid, OidBinaryDumper)
 
 
 def oid_from_db(value: bytes | memoryview) -> pygit2.Oid:
-    """Build ``Oid`` from a ``bytea`` value (e.g. manual handling without a row factory)."""
-    return pygit2.Oid(raw=bytes(value))
+	"""Build ``Oid`` from a ``bytea`` value (e.g. manual handling without a row factory)."""
+	return pygit2.Oid(raw=bytes(value))
 
 
 def postgit_tuple_row(cursor: BaseCursor[Any, Any]) -> RowMaker[tuple[Any, ...]]:
-    """
-    Like :func:`psycopg.rows.tuple_row`, but turns ``oid`` and ``target_oid``
-    columns into ``pygit2.Oid``.
-    """
-    if not cursor.description:
-        return tuple_row(cursor)
-    names = [d.name for d in cursor.description]
-    idxs = [i for i, n in enumerate(names) if n in _OID_COLUMN_NAMES]
-    if not idxs:
-        return tuple_row(cursor)
-
-    def rowmaker(values: Sequence[Any]) -> tuple[Any, ...]:
-        out = list(values)
-        for i in idxs:
-            v = out[i]
-            if isinstance(v, (bytes, memoryview)):
-                out[i] = oid_from_db(v)
-        return tuple(out)
-
-    return rowmaker
+	"""
+	Like :func:`psycopg.rows.tuple_row`, but turns ``oid`` and ``target_oid``
+	columns into ``pygit2.Oid``.
+	"""
+	if not cursor.description:
+		return tuple_row(cursor)
+	names = [d.name for d in cursor.description]
+	idxs = [i for i, n in enumerate(names) if n in _OID_COLUMN_NAMES]
+	if not idxs:
+		return tuple_row(cursor)
+
+	def rowmaker(values: Sequence[Any]) -> tuple[Any, ...]:
+		out = list(values)
+		for i in idxs:
+			v = out[i]
+			if isinstance(v, (bytes, memoryview)):
+				out[i] = oid_from_db(v)
+		return tuple(out)
+
+	return rowmaker
 
 
 def postgit_dict_row(cursor: BaseCursor[Any, Any]) -> RowMaker[dict[str, Any]]:
-    """
-    Like :func:`psycopg.rows.dict_row`, but turns ``oid`` and ``target_oid``
-    values into ``pygit2.Oid``.
-    """
-    inner = dict_row(cursor)
-    if cursor.description is None:
-        return inner
-    names = [d.name for d in cursor.description]
-    idxs = [i for i, n in enumerate(names) if n in _OID_COLUMN_NAMES]
-    if not idxs:
-        return inner
-
-    def rowmaker(values: Sequence[Any]) -> dict[str, Any]:
-        d = dict(zip(names, values))
-        for k in _OID_COLUMN_NAMES & d.keys():
-            v = d[k]
-            if isinstance(v, (bytes, memoryview)):
-                d[k] = oid_from_db(v)
-        return d
-
-    return rowmaker
+	"""
+	Like :func:`psycopg.rows.dict_row`, but turns ``oid`` and ``target_oid``
+	values into ``pygit2.Oid``.
+	"""
+	inner = dict_row(cursor)
+	if cursor.description is None:
+		return inner
+	names = [d.name for d in cursor.description]
+	idxs = [i for i, n in enumerate(names) if n in _OID_COLUMN_NAMES]
+	if not idxs:
+		return inner
+
+	def rowmaker(values: Sequence[Any]) -> dict[str, Any]:
+		d = dict(zip(names, values))
+		for k in _OID_COLUMN_NAMES & d.keys():
+			v = d[k]
+			if isinstance(v, (bytes, memoryview)):
+				d[k] = oid_from_db(v)
+		return d
+
+	return rowmaker
 
 
 def ref_storage_columns(
-    ref: pygit2.Reference,
+	ref: pygit2.Reference,
 ) -> tuple[pygit2.Oid | None, str | None]:
-    """
-    Values for ``(target_oid, symbolic_target)`` on the ``refs`` table.
-
-    Matches :func:`register_adapters` expectations: direct refs supply ``Oid``;
-    symbolic refs supply the target ref name as ``str``.
-    """
-    if ref.type == _GIT_REFERENCE_SYMBOLIC:
-        t = ref.target
-        if not isinstance(t, str):
-            t = str(t)
-        return None, t
-    if ref.type == _GIT_REFERENCE_OID:
-        tgt = ref.target
-        if not isinstance(tgt, pygit2.Oid):
-            tgt = pygit2.Oid(hex=str(tgt))
-        return tgt, None
-    raise TypeError(f"unsupported reference type: {ref.type!r}")
+	"""
+	Values for ``(target_oid, symbolic_target)`` on the ``refs`` table.
+
+	Matches :func:`register_adapters` expectations: direct refs supply ``Oid``;
+	symbolic refs supply the target ref name as ``str``.
+	"""
+	if ref.type == _GIT_REFERENCE_SYMBOLIC:
+		t = ref.target
+		if not isinstance(t, str):
+			t = str(t)
+		return None, t
+	if ref.type == _GIT_REFERENCE_OID:
+		tgt = ref.target
+		if not isinstance(tgt, pygit2.Oid):
+			tgt = pygit2.Oid(hex=str(tgt))
+		return tgt, None
+	raise TypeError(f"unsupported reference type: {ref.type!r}")
 
 
 def object_type_from_pygit2(obj: pygit2.Object) -> GitObjectType:
-    """Map ``Object.type`` (libgit2 kind int) to :class:`GitObjectType`."""
-    return GitObjectType(obj.type)
+	"""Map ``Object.type`` (libgit2 kind int) to :class:`GitObjectType`."""
+	return GitObjectType(obj.type)
 
 
 def object_data_from_pygit2(obj: pygit2.Object) -> bytes:
-    """Object payload for ``objects.object_data`` (same as ``Object.read_raw()``)."""
-    return obj.read_raw()
+	"""Object payload for ``objects.object_data`` (same as ``Object.read_raw()``)."""
+	return obj.read_raw()
 
 
 def objects_row_insert(
-    obj: pygit2.Object,
+	obj: pygit2.Object,
 ) -> tuple[pygit2.Oid, GitObjectType, bytes]:
-    """``(oid, object_type, object_data)`` for inserting into ``objects``."""
-    return obj.id, object_type_from_pygit2(obj), object_data_from_pygit2(obj)
+	"""``(oid, object_type, object_data)`` for inserting into ``objects``."""
+	return obj.id, object_type_from_pygit2(obj), object_data_from_pygit2(obj)
 
 
 def regtype_git_object_type(schema: str = "public") -> str:
-    """Qualified type name for :func:`EnumInfo.fetch` if ``search_path`` is not set."""
-    return f"{schema}.git_object_type"
+	"""Qualified type name for :func:`EnumInfo.fetch` if ``search_path`` is not set."""
+	return f"{schema}.git_object_type"
diff --git a/pygitweb/__init__.py b/pygitweb/__init__.py
index f4c0c68..2eabd0a 100644
--- a/pygitweb/__init__.py
+++ b/pygitweb/__init__.py
@@ -1,5 +1,5 @@
-from pygitweb.main import app
 import pygitweb.__meta__
+from pygitweb.main import app
 
 """
 Git Repo Browser built with FastAPI + Pygit2
diff --git a/pygitweb/__meta__.py b/pygitweb/__meta__.py
index 8e34511..08dcd7b 100644
--- a/pygitweb/__meta__.py
+++ b/pygitweb/__meta__.py
@@ -1,8 +1,9 @@
 """
 Metadata for PyGitWeb - this is the canonical source of all information below.
 """
+
 __version__ = "1.0.0"
 __author__ = "Will Bowers"
-__license__ = "Apache 2.0" # This may change before being distributed.
+__license__ = "Apache 2.0"  # This may change before being distributed.
 __description__ = "FastAPI + Pygit2 Repo Browser"
 __url__ = "https://github.com/willbowers/pygitweb"
diff --git a/pygitweb/actions.py b/pygitweb/actions.py
index 4e90471..141f0c6 100644
--- a/pygitweb/actions.py
+++ b/pygitweb/actions.py
@@ -2,17 +2,17 @@
 Gitweb action handlers (git_*): summary, tree, blob, log, commit, tag, etc.
 Ported from gitweb/gitweb.perl action handlers. Invoked by main.dispatch.
 """
+
 from __future__ import annotations
 
 import base64
 import mimetypes
 import os
-from datetime import datetime, timedelta, timezone
+from datetime import UTC, datetime, timedelta, timezone
 from typing import Any
 from urllib.parse import quote, urlencode
 
 import pygit2
-
 from fastapi import HTTPException, Request
 from fastapi.responses import HTMLResponse, PlainTextResponse, Response
 
@@ -20,773 +20,778 @@ from pygitweb import config
 from pygitweb.config import BLOB_LANG
 from pygitweb.formatting import age_string, esc_html, sanitize, to_utf8
 from pygitweb.git_helpers import (
-    get_blob_at_ref_path,
-    get_blob_unified_diff,
-    get_commit_history,
-    get_commit_unified_diff,
-    get_commits_in_range,
-    get_readme_at_ref_path,
-    get_tree_at_ref_path,
-    git_get_head_hash,
-    git_get_project_description,
-    git_get_remotes_info,
-    git_get_tags_list,
-    git_get_type,
-    git_get_heads_list,
-    open_repo,
-    parse_commit,
-    parse_tag,
+	get_blob_at_ref_path,
+	get_blob_unified_diff,
+	get_commit_history,
+	get_commit_unified_diff,
+	get_commits_in_range,
+	get_readme_at_ref_path,
+	get_tree_at_ref_path,
+	git_get_head_hash,
+	git_get_heads_list,
+	git_get_project_description,
+	git_get_remotes_info,
+	git_get_tags_list,
+	git_get_type,
+	open_repo,
+	parse_commit,
+	parse_tag,
 )
 from pygitweb.projects import git_get_project_owner
-from pygitweb.templates_env import PREAMBLE, POSTAMBLE, env
+from pygitweb.templates_env import POSTAMBLE, PREAMBLE, env
 from pygitweb.validation import is_valid_pathname, is_valid_ref_format
 
 
 def _pagination_url(request: Request, page: int, pagecount: int) -> str:
-    """Build URL for a pagination page, preserving path and other query params."""
-    params = dict(request.query_params)
-    params["page"] = str(page)
-    params["pagecount"] = str(pagecount)
-    return f"{request.url.path}?{urlencode(params)}"
+	"""Build URL for a pagination page, preserving path and other query params."""
+	params = dict(request.query_params)
+	params["page"] = str(page)
+	params["pagecount"] = str(pagecount)
+	return f"{request.url.path}?{urlencode(params)}"
 
 
 def parse_pagination(
-    page: str | None,
-    pagecount: str | None,
+	page: str | None,
+	pagecount: str | None,
 ) -> tuple[int, int]:
-    """Parse page and pagecount; default 1 and 25; raise if pagecount > 50."""
-    p = 1
-    pc = 25
-    if page is not None:
-        try:
-            p = int(page)
-        except ValueError:
-            raise HTTPException(status_code=400, detail="page must be an integer")
-        if p < 1:
-            raise HTTPException(status_code=400, detail="page must be at least 1")
-    if pagecount is not None:
-        try:
-            pc = int(pagecount)
-        except ValueError:
-            raise HTTPException(status_code=400, detail="pagecount must be an integer")
-        if pc < 1:
-            raise HTTPException(status_code=400, detail="pagecount must be at least 1")
-        if pc > 50:
-            raise HTTPException(status_code=400, detail="pagecount must not exceed 50")
-    return p, pc
+	"""Parse page and pagecount; default 1 and 25; raise if pagecount > 50."""
+	p = 1
+	pc = 25
+	if page is not None:
+		try:
+			p = int(page)
+		except ValueError:
+			raise HTTPException(status_code=400, detail="page must be an integer")
+		if p < 1:
+			raise HTTPException(status_code=400, detail="page must be at least 1")
+	if pagecount is not None:
+		try:
+			pc = int(pagecount)
+		except ValueError:
+			raise HTTPException(status_code=400, detail="pagecount must be an integer")
+		if pc < 1:
+			raise HTTPException(status_code=400, detail="pagecount must be at least 1")
+		if pc > 50:
+			raise HTTPException(status_code=400, detail="pagecount must not exceed 50")
+	return p, pc
 
 
 def _tree_url(project: str, h: str, f: str | None, a: str = "tree") -> str:
-    """Build URL for tree or blob: /project/{project}?a=a&h=h&f=f with f quoted."""
-    q = f"a={a}&h={quote(h, safe='')}"
-    if f:
-        q += f"&f={quote(f, safe='/')}"
-    return f"/project/{project}?{q}"
+	"""Build URL for tree or blob: /project/{project}?a=a&h=h&f=f with f quoted."""
+	q = f"a={a}&h={quote(h, safe='')}"
+	if f:
+		q += f"&f={quote(f, safe='/')}"
+	return f"/project/{project}?{q}"
 
 
 def _format_date(epoch: int | None, tz_offset: int | None = None) -> str:
-    """Format epoch timestamp to readable date string.
-    tz_offset is in minutes (as returned by pygit2).
-    """
-    if epoch is None:
-        return ""
-    try:
-        if tz_offset is not None:
-            tz = timezone(timedelta(seconds=tz_offset * 60))
-        else:
-            tz = timezone.utc
-        dt = datetime.fromtimestamp(epoch, tz=tz)
-        return dt.strftime("%Y-%m-%d %H:%M:%S")
-    except (ValueError, OSError):
-        return ""
+	"""Format epoch timestamp to readable date string.
+	tz_offset is in minutes (as returned by pygit2).
+	"""
+	if epoch is None:
+		return ""
+	try:
+		if tz_offset is not None:
+			tz = timezone(timedelta(seconds=tz_offset * 60))
+		else:
+			tz = UTC
+		dt = datetime.fromtimestamp(epoch, tz=tz)
+		return dt.strftime("%Y-%m-%d %H:%M:%S")
+	except (ValueError, OSError):
+		return ""
 
 
 def _format_commit_table_row(project: str, commit: dict[str, Any], short: bool = False) -> list:
-    """Format a single commit as a table row."""
-    oid = commit.get("oid", "")
-    oid_short = oid[:7] if oid else ""
-    subject = commit.get("subject", "")
-    author = commit.get("author", "")
-    author_epoch = commit.get("author_epoch")
-
-    date_str = _format_date(author_epoch, commit.get("author_tz"))
-    age_sec = None
-    if author_epoch:
-        age_sec = datetime.now(timezone.utc).timestamp() - author_epoch
-        age_str = age_string(age_sec) if age_sec > 0 else "right now"
-    else:
-        age_str = ""
-
-    commit_link = f"/project/{project}?a=commit&h={quote(oid, safe='')}"
-    diff_link = f"/project/{project}?a=commitdiff&h={quote(oid, safe='')}"
-    author_display = esc_html(author) or "unknown"
-
-    if short:
-        return [
-            f'<a href="{commit_link}">{esc_html(oid_short)}</a>',
-            esc_html(subject),
-            author_display,
-            esc_html(age_str),
-            f'<a href="{diff_link}">diff</a>',
-        ]
-    return [
-        f'<a href="{commit_link}">{esc_html(oid_short)}</a>',
-        esc_html(subject),
-        author_display,
-        esc_html(date_str),
-        esc_html(age_str),
-        f'<a href="{diff_link}">diff</a>',
-    ]
+	"""Format a single commit as a table row."""
+	oid = commit.get("oid", "")
+	oid_short = oid[:7] if oid else ""
+	subject = commit.get("subject", "")
+	author = commit.get("author", "")
+	author_epoch = commit.get("author_epoch")
+
+	date_str = _format_date(author_epoch, commit.get("author_tz"))
+	age_sec = None
+	if author_epoch:
+		age_sec = datetime.now(UTC).timestamp() - author_epoch
+		age_str = age_string(age_sec) if age_sec > 0 else "right now"
+	else:
+		age_str = ""
+
+	commit_link = f"/project/{project}?a=commit&h={quote(oid, safe='')}"
+	diff_link = f"/project/{project}?a=commitdiff&h={quote(oid, safe='')}"
+	author_display = esc_html(author) or "unknown"
+
+	if short:
+		return [
+			f'<a href="{commit_link}">{esc_html(oid_short)}</a>',
+			esc_html(subject),
+			author_display,
+			esc_html(age_str),
+			f'<a href="{diff_link}">diff</a>',
+		]
+	return [
+		f'<a href="{commit_link}">{esc_html(oid_short)}</a>',
+		esc_html(subject),
+		author_display,
+		esc_html(date_str),
+		esc_html(age_str),
+		f'<a href="{diff_link}">diff</a>',
+	]
 
 
 def _render_pagination(
-    request: Request,
-    page: int,
-    pagecount: int,
-    has_prev: bool,
-    has_next: bool,
-    total_pages: int | None = None,
+	request: Request,
+	page: int,
+	pagecount: int,
+	has_prev: bool,
+	has_next: bool,
+	total_pages: int | None = None,
 ) -> str:
-    """Render Tabler pagination HTML."""
-    prev_url = _pagination_url(request, page - 1, pagecount) if has_prev else ""
-    next_url = _pagination_url(request, page + 1, pagecount) if has_next else ""
-    page_links = None
-    if total_pages is not None and total_pages <= 20:
-        page_links = [(p, _pagination_url(request, p, pagecount)) for p in range(1, total_pages + 1)]
-    elif total_pages is not None:
-        start = max(1, page - 2)
-        end = min(total_pages, page + 2)
-        page_links = [(p, _pagination_url(request, p, pagecount)) for p in range(start, end + 1)]
-        if start > 1:
-            page_links = [(1, _pagination_url(request, 1, pagecount))] + [(-1, "")] + page_links
-        if end < total_pages:
-            page_links = page_links + [(-1, "")] + [(total_pages, _pagination_url(request, total_pages, pagecount))]
-    return env.get_template("pagination.html").render(
-        current_page=page,
-        pagecount=pagecount,
-        has_prev=has_prev,
-        has_next=has_next,
-        prev_url=prev_url,
-        next_url=next_url,
-        total_pages=total_pages,
-        page_links=page_links,
-    )
+	"""Render Tabler pagination HTML."""
+	prev_url = _pagination_url(request, page - 1, pagecount) if has_prev else ""
+	next_url = _pagination_url(request, page + 1, pagecount) if has_next else ""
+	page_links = None
+	if total_pages is not None and total_pages <= 20:
+		page_links = [(p, _pagination_url(request, p, pagecount)) for p in range(1, total_pages + 1)]
+	elif total_pages is not None:
+		start = max(1, page - 2)
+		end = min(total_pages, page + 2)
+		page_links = [(p, _pagination_url(request, p, pagecount)) for p in range(start, end + 1)]
+		if start > 1:
+			page_links = [(1, _pagination_url(request, 1, pagecount))] + [(-1, "")] + page_links
+		if end < total_pages:
+			page_links = page_links + [(-1, "")] + [(total_pages, _pagination_url(request, total_pages, pagecount))]
+	return env.get_template("pagination.html").render(
+		current_page=page,
+		pagecount=pagecount,
+		has_prev=has_prev,
+		has_next=has_next,
+		prev_url=prev_url,
+		next_url=next_url,
+		total_pages=total_pages,
+		page_links=page_links,
+	)
 
 
 def _render_readme_card(
-    project: str,
-    ref_oid: str,
-    readme_filename: str,
-    readme_content: str,
-    blob_dir: str = "",
+	project: str,
+	ref_oid: str,
+	readme_filename: str,
+	readme_content: str,
+	blob_dir: str = "",
 ) -> str:
-    """Return HTML for the README card (and script for markdown). blob_dir is the current tree path for relative links."""
-    is_markdown = readme_filename.lower().endswith(".md")
-    blob_base = f"/project/{project}?a=blob&h={quote(ref_oid, safe='')}&f="
-    blob_base_attr = blob_base.replace("&", "&amp;").replace('"', "&quot;")
-    blob_dir_attr = blob_dir.replace("&", "&amp;").replace('"', "&quot;") if blob_dir else ""
-    if is_markdown:
-        readme_b64 = base64.b64encode(readme_content.encode("utf-8")).decode("ascii")
-        card = (
-            '<div class="card mt-3"><div class="card-header"><h2 class="card-title">README</h2></div>'
-            '<div class="card-body"><div id="readme-container" class="markdown-body" data-readme-b64="'
-            + readme_b64
-            + '" data-blob-base="'
-            + blob_base_attr
-            + '"'
-        )
-        if blob_dir_attr:
-            card += ' data-blob-dir="' + blob_dir_attr + '"'
-        card += '></div></div></div><script src="/static/readme-render.js"></script>'
-        return card
-    return (
-        '<div class="card mt-3"><div class="card-header"><h2 class="card-title">README</h2></div>'
-        '<div class="card-body"><pre class="readme-plain"><code>'
-        + esc_html(sanitize(readme_content) or "")
-        + "</code></pre></div></div>"
-    )
+	"""Return HTML for the README card (and script for markdown). blob_dir is the current tree path for relative links."""
+	is_markdown = readme_filename.lower().endswith(".md")
+	blob_base = f"/project/{project}?a=blob&h={quote(ref_oid, safe='')}&f="
+	blob_base_attr = blob_base.replace("&", "&amp;").replace('"', "&quot;")
+	blob_dir_attr = blob_dir.replace("&", "&amp;").replace('"', "&quot;") if blob_dir else ""
+	if is_markdown:
+		readme_b64 = base64.b64encode(readme_content.encode("utf-8")).decode("ascii")
+		card = (
+			'<div class="card mt-3"><div class="card-header"><h2 class="card-title">README</h2></div>'
+			'<div class="card-body"><div id="readme-container" class="markdown-body" data-readme-b64="'
+			+ readme_b64
+			+ '" data-blob-base="'
+			+ blob_base_attr
+			+ '"'
+		)
+		if blob_dir_attr:
+			card += ' data-blob-dir="' + blob_dir_attr + '"'
+		card += '></div></div></div><script src="/static/readme-render.js"></script>'
+		return card
+	return (
+		'<div class="card mt-3"><div class="card-header"><h2 class="card-title">README</h2></div>'
+		'<div class="card-body"><pre class="readme-plain"><code>'
+		+ esc_html(sanitize(readme_content) or "")
+		+ "</code></pre></div></div>"
+	)
 
 
 def _commit_unified_diff_or_raise(project: str, h: str) -> tuple[str, str]:
-    """Return (unified_diff_text, oid_short) for commit h. Raises HTTPException on error."""
-    if not h:
-        raise HTTPException(status_code=400, detail="Commit hash (h) required")
-    if not is_valid_ref_format(h):
-        raise HTTPException(status_code=400, detail="Invalid ref or hash")
-    try:
-        return get_commit_unified_diff(project, h)
-    except (KeyError, pygit2.GitError, OSError, ValueError):
-        raise HTTPException(status_code=404, detail="Commit not found")
+	"""Return (unified_diff_text, oid_short) for commit h. Raises HTTPException on error."""
+	if not h:
+		raise HTTPException(status_code=400, detail="Commit hash (h) required")
+	if not is_valid_ref_format(h):
+		raise HTTPException(status_code=400, detail="Invalid ref or hash")
+	try:
+		return get_commit_unified_diff(project, h)
+	except (KeyError, pygit2.GitError, OSError, ValueError):
+		raise HTTPException(status_code=404, detail="Commit not found")
 
 
 # ---------- Action handlers ----------
 
 
 def git_object(project: str, h: str | None) -> Response:
-    """Show object by type: commit, tree, tag, or blob. Dispatches to the appropriate view."""
-    if not h:
-        raise HTTPException(status_code=400, detail="Object hash (h) required")
-    if not is_valid_ref_format(h):
-        raise HTTPException(status_code=400, detail="Invalid ref or hash")
-    obj_type = git_get_type(project, h)
-    if not obj_type:
-        raise HTTPException(status_code=404, detail="Object not found")
-    if obj_type == "commit":
-        return git_commit(project, h)
-    if obj_type == "tree":
-        return git_tree(project, h, None)
-    if obj_type == "tag":
-        return git_tag(project, h)
-    if obj_type == "blob":
-        try:
-            repo = open_repo(project)
-            obj = repo.revparse_single(h)
-        except (KeyError, pygit2.GitError, OSError):
-            raise HTTPException(status_code=404, detail="Object not found")
-        if not isinstance(obj, pygit2.Blob):
-            raise HTTPException(status_code=404, detail="Not a blob")
-        data = obj.data
-        text = to_utf8(data) or ""
-        body = esc_html(sanitize(text) or "") or ""
-        lines = text.split("\n")
-        num_lines = max(1, len(lines))
-        line_nums = "\n".join(str(i) for i in range(1, num_lines + 1))
-        oid_short = str(obj.id)[:7]
-        blob_html = (
-            f"<div class=\"blob-view\">"
-            f"<div class=\"blob-line-nums\" aria-hidden=\"true\">{esc_html(line_nums)}</div>"
-            f"<pre class=\"blob-content\"><code class=\"hljs\">{body}</code></pre>"
-            f"</div>"
-        )
-        pre = PREAMBLE.render(
-            title=f"Blob {oid_short} - {esc_html(project)}",
-            site_name=config.SITE_NAME,
-        )
-        return HTMLResponse(f"{pre}<h1>Blob {esc_html(oid_short)}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{blob_html}{POSTAMBLE}")
-    raise HTTPException(status_code=404, detail="Unknown object type")
+	"""Show object by type: commit, tree, tag, or blob. Dispatches to the appropriate view."""
+	if not h:
+		raise HTTPException(status_code=400, detail="Object hash (h) required")
+	if not is_valid_ref_format(h):
+		raise HTTPException(status_code=400, detail="Invalid ref or hash")
+	obj_type = git_get_type(project, h)
+	if not obj_type:
+		raise HTTPException(status_code=404, detail="Object not found")
+	if obj_type == "commit":
+		return git_commit(project, h)
+	if obj_type == "tree":
+		return git_tree(project, h, None)
+	if obj_type == "tag":
+		return git_tag(project, h)
+	if obj_type == "blob":
+		try:
+			repo = open_repo(project)
+			obj = repo.revparse_single(h)
+		except (KeyError, pygit2.GitError, OSError):
+			raise HTTPException(status_code=404, detail="Object not found")
+		if not isinstance(obj, pygit2.Blob):
+			raise HTTPException(status_code=404, detail="Not a blob")
+		data = obj.data
+		text = to_utf8(data) or ""
+		body = esc_html(sanitize(text) or "") or ""
+		lines = text.split("\n")
+		num_lines = max(1, len(lines))
+		line_nums = "\n".join(str(i) for i in range(1, num_lines + 1))
+		oid_short = str(obj.id)[:7]
+		blob_html = (
+			f'<div class="blob-view">'
+			f'<div class="blob-line-nums" aria-hidden="true">{esc_html(line_nums)}</div>'
+			f'<pre class="blob-content"><code class="hljs">{body}</code></pre>'
+			f"</div>"
+		)
+		pre = PREAMBLE.render(
+			title=f"Blob {oid_short} - {esc_html(project)}",
+			site_name=config.SITE_NAME,
+		)
+		return HTMLResponse(
+			f"{pre}<h1>Blob {esc_html(oid_short)}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{blob_html}{POSTAMBLE}"
+		)
+	raise HTTPException(status_code=404, detail="Unknown object type")
 
 
 def git_summary(project: str) -> HTMLResponse:
-    """Project summary page. Port of git_summary."""
-    descr = git_get_project_description(project) or "none"
-    owner = git_get_project_owner(project) or ""
-    head = git_get_head_hash(project)
-    head_short = head[:7] if head else ""
-
-    table = env.get_template("table.html").render(
-        cols=["Field", "Value"],
-        rows=[
-            ["Description", esc_html(descr)],
-            ["Owner", esc_html(owner)],
-            [esc_html("HEAD"), f"<a href='/project/{project}?a=commit&h={quote(head or '', safe='')}'>{head_short or 'N/A'}</a>"],
-            [esc_html("tree"), f"<a href='/project/{project}?a=tree&h={head or ''}'>browse</a>"],
-            ["Log", f"<a href='/project/{project}?a=log&h={head or ''}'>view log</a>"],
-            ["Shortlog", f"<a href='/project/{project}?a=shortlog&h={head or ''}'>view shortlog</a>"],
-            ["Heads", f"<a href='/project/{project}?a=heads'>view heads</a>"],
-            ["Tags", f"<a href='/project/{project}?a=tags'>view tags</a>"],
-            ["Remotes", f"<a href='/project/{project}?a=remotes'>view remotes</a>"],
-        ],
-    )
-    pre = PREAMBLE.render(title=f"{esc_html(config.SITE_NAME)} - {project}", site_name=config.SITE_NAME)
-    body_parts = [f"{pre}<h1>{esc_html(project)}</h1>{table}"]
-
-    readme = get_readme_at_ref_path(project, head, "")
-    if readme:
-        readme_filename, readme_content = readme
-        body_parts.append(_render_readme_card(project, head or "", readme_filename, readme_content, ""))
-
-    body_parts.append(POSTAMBLE)
-    return HTMLResponse("".join(body_parts))
+	"""Project summary page. Port of git_summary."""
+	descr = git_get_project_description(project) or "none"
+	owner = git_get_project_owner(project) or ""
+	head = git_get_head_hash(project)
+	head_short = head[:7] if head else ""
+
+	table = env.get_template("table.html").render(
+		cols=["Field", "Value"],
+		rows=[
+			["Description", esc_html(descr)],
+			["Owner", esc_html(owner)],
+			[
+				esc_html("HEAD"),
+				f"<a href='/project/{project}?a=commit&h={quote(head or '', safe='')}'>{head_short or 'N/A'}</a>",
+			],
+			[
+				esc_html("tree"),
+				f"<a href='/project/{project}?a=tree&h={head or ''}'>browse</a>",
+			],
+			["Log", f"<a href='/project/{project}?a=log&h={head or ''}'>view log</a>"],
+			[
+				"Shortlog",
+				f"<a href='/project/{project}?a=shortlog&h={head or ''}'>view shortlog</a>",
+			],
+			["Heads", f"<a href='/project/{project}?a=heads'>view heads</a>"],
+			["Tags", f"<a href='/project/{project}?a=tags'>view tags</a>"],
+			["Remotes", f"<a href='/project/{project}?a=remotes'>view remotes</a>"],
+		],
+	)
+	pre = PREAMBLE.render(title=f"{esc_html(config.SITE_NAME)} - {project}", site_name=config.SITE_NAME)
+	body_parts = [f"{pre}<h1>{esc_html(project)}</h1>{table}"]
+
+	readme = get_readme_at_ref_path(project, head, "")
+	if readme:
+		readme_filename, readme_content = readme
+		body_parts.append(_render_readme_card(project, head or "", readme_filename, readme_content, ""))
+
+	body_parts.append(POSTAMBLE)
+	return HTMLResponse("".join(body_parts))
 
 
 def git_remotes(project: str) -> HTMLResponse:
-    """Remotes page: list configured remotes (name, url, push_url)."""
-    remotes = git_get_remotes_info(project)
-    if not remotes:
-        pre = PREAMBLE.render(title=f"Remotes - {esc_html(project)}", site_name=config.SITE_NAME)
-        return HTMLResponse(
-            f"{pre}<h1>Remotes</h1><p>No remotes configured.</p>{POSTAMBLE}"
-        )
-    rows = []
-    for r in remotes:
-        name = esc_html(r["name"])
-        url = esc_html(r["url"] or "—")
-        push_url = esc_html(r["push_url"] or "—")
-        rows.append([name, url, push_url])
-    table = env.get_template("table.html").render(
-        cols=["Name", "URL", "Push URL"],
-        rows=rows,
-    )
-    pre = PREAMBLE.render(title=f"Remotes - {esc_html(project)}", site_name=config.SITE_NAME)
-    return HTMLResponse(f"{pre}<h1>Remotes</h1>{table}{POSTAMBLE}")
+	"""Remotes page: list configured remotes (name, url, push_url)."""
+	remotes = git_get_remotes_info(project)
+	if not remotes:
+		pre = PREAMBLE.render(title=f"Remotes - {esc_html(project)}", site_name=config.SITE_NAME)
+		return HTMLResponse(f"{pre}<h1>Remotes</h1><p>No remotes configured.</p>{POSTAMBLE}")
+	rows = []
+	for r in remotes:
+		name = esc_html(r["name"])
+		url = esc_html(r["url"] or "—")
+		push_url = esc_html(r["push_url"] or "—")
+		rows.append([name, url, push_url])
+	table = env.get_template("table.html").render(
+		cols=["Name", "URL", "Push URL"],
+		rows=rows,
+	)
+	pre = PREAMBLE.render(title=f"Remotes - {esc_html(project)}", site_name=config.SITE_NAME)
+	return HTMLResponse(f"{pre}<h1>Remotes</h1>{table}{POSTAMBLE}")
 
 
 def git_blob(project: str, h: str | None, f: str | None, *, raw: bool = False) -> Response:
-    """Return blob content when f points to a file. Raw = bytes; else HTML with escaped content."""
-    if not f:
-        raise HTTPException(status_code=400, detail="File path (f) required")
-    if not is_valid_pathname(f):
-        raise HTTPException(status_code=400, detail="Invalid path")
-    result = get_blob_at_ref_path(project, h, f)
-    if not result:
-        raise HTTPException(status_code=404, detail="File not found")
-    blob, _ = result
-    data = blob.data
-    if raw:
-        media_type, _ = mimetypes.guess_type(f.split("/")[-1])
-        if media_type is None:
-            try:
-                data.decode("utf-8")
-                media_type = "text/plain; charset=utf-8"
-            except UnicodeDecodeError:
-                media_type = "application/octet-stream"
-        return Response(content=data, media_type=media_type)
-    text = to_utf8(data) or ""
-    body = esc_html(sanitize(text) or "") or ""
-    lines = text.split("\n")
-    num_lines = max(1, len(lines))
-    line_nums = "\n".join(str(i) for i in range(1, num_lines + 1))
-    ext = os.path.splitext(f.split("/")[-1])[1].lstrip(".").lower()
-    lang = BLOB_LANG.get(ext, "")
-    lang_attr = f" language-{lang}" if lang else ""
-    blob_html = (
-        f"<div class=\"blob-view\">"
-        f"<div class=\"blob-line-nums\" aria-hidden=\"true\">{esc_html(line_nums)}</div>"
-        f"<pre class=\"blob-content\"><code class=\"hljs{lang_attr}\">{body}</code></pre>"
-        f"</div>"
-    )
-    blob_script = '<script src="/static/blob-view.js"></script>'
-    pre = PREAMBLE.render(title=f"{esc_html(f)} - {esc_html(project)}", site_name=config.SITE_NAME)
-    return HTMLResponse(f"{pre}{blob_html}{blob_script}{POSTAMBLE}")
+	"""Return blob content when f points to a file. Raw = bytes; else HTML with escaped content."""
+	if not f:
+		raise HTTPException(status_code=400, detail="File path (f) required")
+	if not is_valid_pathname(f):
+		raise HTTPException(status_code=400, detail="Invalid path")
+	result = get_blob_at_ref_path(project, h, f)
+	if not result:
+		raise HTTPException(status_code=404, detail="File not found")
+	blob, _ = result
+	data = blob.data
+	if raw:
+		media_type, _ = mimetypes.guess_type(f.split("/")[-1])
+		if media_type is None:
+			try:
+				data.decode("utf-8")
+				media_type = "text/plain; charset=utf-8"
+			except UnicodeDecodeError:
+				media_type = "application/octet-stream"
+		return Response(content=data, media_type=media_type)
+	text = to_utf8(data) or ""
+	body = esc_html(sanitize(text) or "") or ""
+	lines = text.split("\n")
+	num_lines = max(1, len(lines))
+	line_nums = "\n".join(str(i) for i in range(1, num_lines + 1))
+	ext = os.path.splitext(f.split("/")[-1])[1].lstrip(".").lower()
+	lang = BLOB_LANG.get(ext, "")
+	lang_attr = f" language-{lang}" if lang else ""
+	blob_html = (
+		f'<div class="blob-view">'
+		f'<div class="blob-line-nums" aria-hidden="true">{esc_html(line_nums)}</div>'
+		f'<pre class="blob-content"><code class="hljs{lang_attr}">{body}</code></pre>'
+		f"</div>"
+	)
+	blob_script = '<script src="/static/blob-view.js"></script>'
+	pre = PREAMBLE.render(title=f"{esc_html(f)} - {esc_html(project)}", site_name=config.SITE_NAME)
+	return HTMLResponse(f"{pre}{blob_html}{blob_script}{POSTAMBLE}")
 
 
 def git_tree(project: str, h: str | None, f: str | None) -> HTMLResponse:
-    """Tree page: list files and directories; directories link to tree with f=path."""
-    if f is not None and not is_valid_pathname(f):
-        raise HTTPException(status_code=400, detail="Invalid path")
-    result = get_tree_at_ref_path(project, h, f)
-    if not result:
-        raise HTTPException(status_code=404, detail="Tree or path not found")
-    tree, ref_oid = result
-    base = f"/project/{project}"
-    breadcrumbs = [f'<a href="{base}">{esc_html(project)}</a>']
-    if f:
-        parts = f.strip("/").split("/")
-        for i, seg in enumerate(parts):
-            prefix = "/".join(parts[: i + 1])
-            breadcrumbs.append(
-                f' / <a href="{_tree_url(project, ref_oid, prefix)}">{esc_html(seg)}</a>'
-            )
-    breadcrumb_html = "".join(breadcrumbs)
-    entries = [(obj.name, obj.type_str, str(obj.id)) for obj in tree]
-    dirs = sorted((n, t, o) for n, t, o in entries if t == "tree")
-    blobs = sorted((n, t, o) for n, t, o in entries if t == "blob")
-    rows = []
-    for name, typ, _ in dirs:
-        sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
-        link = _tree_url(project, ref_oid, sub_path)
-        rows.append(
-            [f'<a href="{link}">{esc_html(name)}/</a>', "tree"]
-        )
-    for name, typ, _ in blobs:
-        sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
-        link = _tree_url(project, ref_oid, sub_path, a="blob")
-        rows.append(
-            [f'<a href="{link}">{esc_html(name)}</a>', "blob"]
-        )
-    title_path = f" / {f}" if f else ""
-    pre = PREAMBLE.render(title=f"{esc_html(project)}{esc_html(title_path)} - Tree", site_name=config.SITE_NAME)
-    table = env.get_template("table.html").render(
-        cols=["Name", "Type"],
-        rows=rows
-    )
-    body_parts = [
-        f"{pre}<p class='breadcrumb'>{breadcrumb_html}</p><h1>Tree{esc_html(title_path)}</h1>{table}"
-    ]
-    readme = get_readme_at_ref_path(project, ref_oid, f or "")
-    if readme:
-        readme_filename, readme_content = readme
-        body_parts.append(_render_readme_card(project, ref_oid, readme_filename, readme_content, f or ""))
-    body_parts.append(POSTAMBLE)
-    return HTMLResponse("".join(body_parts))
+	"""Tree page: list files and directories; directories link to tree with f=path."""
+	if f is not None and not is_valid_pathname(f):
+		raise HTTPException(status_code=400, detail="Invalid path")
+	result = get_tree_at_ref_path(project, h, f)
+	if not result:
+		raise HTTPException(status_code=404, detail="Tree or path not found")
+	tree, ref_oid = result
+	base = f"/project/{project}"
+	breadcrumbs = [f'<a href="{base}">{esc_html(project)}</a>']
+	if f:
+		parts = f.strip("/").split("/")
+		for i, seg in enumerate(parts):
+			prefix = "/".join(parts[: i + 1])
+			breadcrumbs.append(f' / <a href="{_tree_url(project, ref_oid, prefix)}">{esc_html(seg)}</a>')
+	breadcrumb_html = "".join(breadcrumbs)
+	entries = [(obj.name, obj.type_str, str(obj.id)) for obj in tree]
+	dirs = sorted((n, t, o) for n, t, o in entries if t == "tree")
+	blobs = sorted((n, t, o) for n, t, o in entries if t == "blob")
+	rows = []
+	for name, typ, _ in dirs:
+		sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
+		link = _tree_url(project, ref_oid, sub_path)
+		rows.append([f'<a href="{link}">{esc_html(name)}/</a>', "tree"])
+	for name, typ, _ in blobs:
+		sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
+		link = _tree_url(project, ref_oid, sub_path, a="blob")
+		rows.append([f'<a href="{link}">{esc_html(name)}</a>', "blob"])
+	title_path = f" / {f}" if f else ""
+	pre = PREAMBLE.render(
+		title=f"{esc_html(project)}{esc_html(title_path)} - Tree",
+		site_name=config.SITE_NAME,
+	)
+	table = env.get_template("table.html").render(cols=["Name", "Type"], rows=rows)
+	body_parts = [f"{pre}<p class='breadcrumb'>{breadcrumb_html}</p><h1>Tree{esc_html(title_path)}</h1>{table}"]
+	readme = get_readme_at_ref_path(project, ref_oid, f or "")
+	if readme:
+		readme_filename, readme_content = readme
+		body_parts.append(_render_readme_card(project, ref_oid, readme_filename, readme_content, f or ""))
+	body_parts.append(POSTAMBLE)
+	return HTMLResponse("".join(body_parts))
 
 
 def git_log(project: str, h: str | None, request: Request, page: int, pagecount: int) -> HTMLResponse:
-    """Commit log page. Port of git_log."""
-    skip = (page - 1) * pagecount
-    commits = get_commit_history(project, ref=h, max_count=pagecount + 1, skip=skip)
-    if not commits:
-        raise HTTPException(status_code=404, detail="No commits found")
-    has_next = len(commits) > pagecount
-    if has_next:
-        commits = commits[:pagecount]
-    rows = []
-    for commit in commits:
-        rows.append(_format_commit_table_row(project, commit, short=False))
-    ref_display = h[:7] if h else "HEAD"
-    title = f"Log - {esc_html(project)} @ {esc_html(ref_display)}"
-    pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
-    table = env.get_template("table.html").render(
-        cols=["Commit", "Subject", "Author", "Date", "Age", "Diff"],
-        rows=rows
-    )
-    pagination_html = _render_pagination(request, page, pagecount, page > 1, has_next)
-    return HTMLResponse(
-        f"{pre}<h1>{title}</h1>{table}{pagination_html}{POSTAMBLE}"
-    )
+	"""Commit log page. Port of git_log."""
+	skip = (page - 1) * pagecount
+	commits = get_commit_history(project, ref=h, max_count=pagecount + 1, skip=skip)
+	if not commits:
+		raise HTTPException(status_code=404, detail="No commits found")
+	has_next = len(commits) > pagecount
+	if has_next:
+		commits = commits[:pagecount]
+	rows = []
+	for commit in commits:
+		rows.append(_format_commit_table_row(project, commit, short=False))
+	ref_display = h[:7] if h else "HEAD"
+	title = f"Log - {esc_html(project)} @ {esc_html(ref_display)}"
+	pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
+	table = env.get_template("table.html").render(
+		cols=["Commit", "Subject", "Author", "Date", "Age", "Diff"], rows=rows
+	)
+	pagination_html = _render_pagination(request, page, pagecount, page > 1, has_next)
+	return HTMLResponse(f"{pre}<h1>{title}</h1>{table}{pagination_html}{POSTAMBLE}")
 
 
 def git_shortlog(project: str, h: str | None, request: Request, page: int, pagecount: int) -> HTMLResponse:
-    """Shortlog page. Port of git_shortlog."""
-    skip = (page - 1) * pagecount
-    commits = get_commit_history(project, ref=h, max_count=pagecount + 1, skip=skip)
-    if not commits:
-        raise HTTPException(status_code=404, detail="No commits found")
-    has_next = len(commits) > pagecount
-    if has_next:
-        commits = commits[:pagecount]
-    rows = []
-    for commit in commits:
-        rows.append(_format_commit_table_row(project, commit, short=True))
-    ref_display = h[:7] if h else "HEAD"
-    title = f"Shortlog - {esc_html(project)} @ {esc_html(ref_display)}"
-    pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
-    table = env.get_template("table.html").render(
-        cols=["Commit", "Subject", "Author", "Age", "Diff"],
-        rows=rows
-    )
-    pagination_html = _render_pagination(request, page, pagecount, page > 1, has_next)
-    return HTMLResponse(
-        f"{pre}<h1>{title}</h1>{table}{pagination_html}{POSTAMBLE}"
-    )
-
-
-def git_history(project: str, h: str | None, f: str | None, request: Request, page: int, pagecount: int) -> HTMLResponse:
-    """History page for a file or directory. Port of git_history."""
-    if not f:
-        raise HTTPException(status_code=400, detail="File path (f) required for history")
-    if not is_valid_pathname(f):
-        raise HTTPException(status_code=400, detail="Invalid path")
-    skip = (page - 1) * pagecount
-    commits = get_commit_history(project, ref=h, path=f, max_count=pagecount + 1, skip=skip)
-    if not commits:
-        raise HTTPException(status_code=404, detail="No history found for this path")
-    has_next = len(commits) > pagecount
-    if has_next:
-        commits = commits[:pagecount]
-    rows = []
-    for commit in commits:
-        rows.append(_format_commit_table_row(project, commit, short=False))
-    ref_display = h[:7] if h else "HEAD"
-    title = f"History - {esc_html(f)} - {esc_html(project)} @ {esc_html(ref_display)}"
-    pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
-    table = env.get_template("table.html").render(
-        cols=["Commit", "Subject", "Author", "Date", "Age", "Patch"],
-        rows=rows
-    )
-    pagination_html = _render_pagination(request, page, pagecount, page > 1, has_next)
-    return HTMLResponse(
-        f"{pre}<h1>{title}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{table}{pagination_html}{POSTAMBLE}"
-    )
+	"""Shortlog page. Port of git_shortlog."""
+	skip = (page - 1) * pagecount
+	commits = get_commit_history(project, ref=h, max_count=pagecount + 1, skip=skip)
+	if not commits:
+		raise HTTPException(status_code=404, detail="No commits found")
+	has_next = len(commits) > pagecount
+	if has_next:
+		commits = commits[:pagecount]
+	rows = []
+	for commit in commits:
+		rows.append(_format_commit_table_row(project, commit, short=True))
+	ref_display = h[:7] if h else "HEAD"
+	title = f"Shortlog - {esc_html(project)} @ {esc_html(ref_display)}"
+	pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
+	table = env.get_template("table.html").render(cols=["Commit", "Subject", "Author", "Age", "Diff"], rows=rows)
+	pagination_html = _render_pagination(request, page, pagecount, page > 1, has_next)
+	return HTMLResponse(f"{pre}<h1>{title}</h1>{table}{pagination_html}{POSTAMBLE}")
+
+
+def git_history(
+	project: str,
+	h: str | None,
+	f: str | None,
+	request: Request,
+	page: int,
+	pagecount: int,
+) -> HTMLResponse:
+	"""History page for a file or directory. Port of git_history."""
+	if not f:
+		raise HTTPException(status_code=400, detail="File path (f) required for history")
+	if not is_valid_pathname(f):
+		raise HTTPException(status_code=400, detail="Invalid path")
+	skip = (page - 1) * pagecount
+	commits = get_commit_history(project, ref=h, path=f, max_count=pagecount + 1, skip=skip)
+	if not commits:
+		raise HTTPException(status_code=404, detail="No history found for this path")
+	has_next = len(commits) > pagecount
+	if has_next:
+		commits = commits[:pagecount]
+	rows = []
+	for commit in commits:
+		rows.append(_format_commit_table_row(project, commit, short=False))
+	ref_display = h[:7] if h else "HEAD"
+	title = f"History - {esc_html(f)} - {esc_html(project)} @ {esc_html(ref_display)}"
+	pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
+	table = env.get_template("table.html").render(
+		cols=["Commit", "Subject", "Author", "Date", "Age", "Patch"], rows=rows
+	)
+	pagination_html = _render_pagination(request, page, pagecount, page > 1, has_next)
+	return HTMLResponse(
+		f"{pre}<h1>{title}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{table}{pagination_html}{POSTAMBLE}"
+	)
 
 
 def git_heads(project: str) -> HTMLResponse:
-    """Heads (branches) list page. Port of git_heads."""
-    heads_list = git_get_heads_list(project)
-    if not heads_list:
-        raise HTTPException(status_code=404, detail="No heads found")
-    rows = []
-    for name, _ref, oid in heads_list:
-        oid_short = oid[:7] if oid else ""
-        commit_link = f"/project/{project}?a=commit&h={quote(oid, safe='')}"
-        tree_link = f"/project/{project}?a=tree&h={quote(oid, safe='')}"
-        rows.append([
-            f'<a href="{commit_link}">{esc_html(name)}</a>',
-            f'<a href="{commit_link}">{esc_html(oid_short)}</a>',
-            f'<a href="{tree_link}">tree</a>',
-        ])
-    title = f"Heads - {esc_html(project)}"
-    pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
-    table = env.get_template("table.html").render(
-        cols=["Head", "Commit", ""],
-        rows=rows,
-    )
-    return HTMLResponse(
-        f"{pre}<h1>{title}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{table}{POSTAMBLE}"
-    )
+	"""Heads (branches) list page. Port of git_heads."""
+	heads_list = git_get_heads_list(project)
+	if not heads_list:
+		raise HTTPException(status_code=404, detail="No heads found")
+	rows = []
+	for name, _ref, oid in heads_list:
+		oid_short = oid[:7] if oid else ""
+		commit_link = f"/project/{project}?a=commit&h={quote(oid, safe='')}"
+		tree_link = f"/project/{project}?a=tree&h={quote(oid, safe='')}"
+		rows.append(
+			[
+				f'<a href="{commit_link}">{esc_html(name)}</a>',
+				f'<a href="{commit_link}">{esc_html(oid_short)}</a>',
+				f'<a href="{tree_link}">tree</a>',
+			]
+		)
+	title = f"Heads - {esc_html(project)}"
+	pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
+	table = env.get_template("table.html").render(
+		cols=["Head", "Commit", ""],
+		rows=rows,
+	)
+	return HTMLResponse(
+		f"{pre}<h1>{title}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{table}{POSTAMBLE}"
+	)
 
 
 def git_tags(project: str, request: Request, page: int, pagecount: int) -> HTMLResponse:
-    """Tags list page. Port of git_tags."""
-    tags_list = git_get_tags_list(project)
-    if not tags_list:
-        raise HTTPException(status_code=404, detail="No tags found")
-    total = len(tags_list)
-    total_pages = (total + pagecount - 1) // pagecount if pagecount else 1
-    if page > total_pages and total > 0:
-        raise HTTPException(status_code=404, detail="Page does not exist")
-    skip = (page - 1) * pagecount
-    tags_page = tags_list[skip : skip + pagecount]
-    try:
-        repo = open_repo(project)
-    except Exception:
-        raise HTTPException(status_code=404, detail="Repository not found")
-    rows = []
-    for name, _ref, oid in tags_page:
-        tag_link = f"/project/{project}?a=tag&h={quote(oid, safe='')}"
-        target_oid = oid
-        target_type = "commit"
-        try:
-            obj = repo.revparse_single(oid)
-            if isinstance(obj, pygit2.Tag):
-                target_oid = str(obj.target)
-                target_type = obj.type_str
-        except (KeyError, pygit2.GitError):
-            pass
-        target_short = target_oid[:7] if target_oid else ""
-        if target_type == "commit":
-            target_link = f"/project/{project}?a=commit&h={quote(target_oid, safe='')}"
-        elif target_type == "tree":
-            target_link = f"/project/{project}?a=tree&h={quote(target_oid, safe='')}"
-        else:
-            target_link = None
-        obj_cell = f'<a href="{target_link}">{esc_html(target_short)}</a>' if target_link else esc_html(target_short)
-        rows.append([
-            f'<a href="{tag_link}">{esc_html(name)}</a>',
-            obj_cell,
-        ])
-    title = f"Tags - {esc_html(project)}"
-    pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
-    table = env.get_template("table.html").render(
-        cols=["Tag", "Object"],
-        rows=rows,
-    )
-    pagination_html = _render_pagination(
-        request, page, pagecount, page > 1, page < total_pages, total_pages=total_pages
-    )
-    return HTMLResponse(
-        f"{pre}<h1>{title}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{table}{pagination_html}{POSTAMBLE}"
-    )
+	"""Tags list page. Port of git_tags."""
+	tags_list = git_get_tags_list(project)
+	if not tags_list:
+		raise HTTPException(status_code=404, detail="No tags found")
+	total = len(tags_list)
+	total_pages = (total + pagecount - 1) // pagecount if pagecount else 1
+	if page > total_pages and total > 0:
+		raise HTTPException(status_code=404, detail="Page does not exist")
+	skip = (page - 1) * pagecount
+	tags_page = tags_list[skip : skip + pagecount]
+	try:
+		repo = open_repo(project)
+	except Exception:
+		raise HTTPException(status_code=404, detail="Repository not found")
+	rows = []
+	for name, _ref, oid in tags_page:
+		tag_link = f"/project/{project}?a=tag&h={quote(oid, safe='')}"
+		target_oid = oid
+		target_type = "commit"
+		try:
+			obj = repo.revparse_single(oid)
+			if isinstance(obj, pygit2.Tag):
+				target_oid = str(obj.target)
+				target_type = obj.type_str
+		except (KeyError, pygit2.GitError):
+			pass
+		target_short = target_oid[:7] if target_oid else ""
+		if target_type == "commit":
+			target_link = f"/project/{project}?a=commit&h={quote(target_oid, safe='')}"
+		elif target_type == "tree":
+			target_link = f"/project/{project}?a=tree&h={quote(target_oid, safe='')}"
+		else:
+			target_link = None
+		obj_cell = f'<a href="{target_link}">{esc_html(target_short)}</a>' if target_link else esc_html(target_short)
+		rows.append(
+			[
+				f'<a href="{tag_link}">{esc_html(name)}</a>',
+				obj_cell,
+			]
+		)
+	title = f"Tags - {esc_html(project)}"
+	pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
+	table = env.get_template("table.html").render(
+		cols=["Tag", "Object"],
+		rows=rows,
+	)
+	pagination_html = _render_pagination(
+		request, page, pagecount, page > 1, page < total_pages, total_pages=total_pages
+	)
+	return HTMLResponse(
+		f"{pre}<h1>{title}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{table}{pagination_html}{POSTAMBLE}"
+	)
 
 
 def git_tag(project: str, h: str | None) -> HTMLResponse:
-    """Single tag view. Port of git_tag."""
-    if not h:
-        raise HTTPException(status_code=400, detail="Tag ref or hash (h) required")
-    if not is_valid_ref_format(h):
-        raise HTTPException(status_code=400, detail="Invalid ref or hash")
-    try:
-        repo = open_repo(project)
-        obj = repo.revparse_single(h)
-    except (KeyError, pygit2.GitError, OSError):
-        raise HTTPException(status_code=404, detail="Tag or object not found")
-    if not isinstance(obj, pygit2.Tag):
-        raise HTTPException(status_code=404, detail="Not a tag object")
-    tag_oid = str(obj.id)
-    tag_data = parse_tag(project, tag_oid)
-    if not tag_data:
-        raise HTTPException(status_code=404, detail="Tag not found")
-    target_oid = tag_data.get("object", "")
-    target_type = tag_data.get("type", "commit")
-    tagger = tag_data.get("tagger", "")
-    tagger_epoch = tag_data.get("tagger_epoch")
-    tagger_tz = tag_data.get("tagger_tz")
-    message = (tag_data.get("message") or "").strip()
-    tag_name = None
-    for name, _ref, oid in git_get_tags_list(project):
-        if oid == tag_oid:
-            tag_name = name
-            break
-    if tag_name is None:
-        tag_name = tag_oid[:7]
-    target_short = target_oid[:7] if target_oid else ""
-    if target_type == "commit":
-        object_link = f'<a href="/project/{project}?a=commit&h={quote(target_oid, safe="")}">{esc_html(target_short)}</a>'
-    elif target_type == "tree":
-        object_link = f'<a href="/project/{project}?a=tree&h={quote(target_oid, safe="")}">{esc_html(target_short)}</a>'
-    else:
-        object_link = esc_html(target_short)
-    table_rows = [
-        ["Tag", esc_html(tag_name)],
-        ["Object", f"{object_link} ({esc_html(target_type)})"],
-        ["Tagger", esc_html(tagger)],
-        ["Date", esc_html(_format_date(tagger_epoch, tagger_tz))],
-    ]
-    if message:
-        table_rows.append(["Message", f"<pre class='tag-message'>{esc_html(message)}</pre>"])
-    table = env.get_template("table.html").render(cols=["Field", "Value"], rows=table_rows)
-    title = f"Tag {esc_html(tag_name)} - {esc_html(project)}"
-    pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
-    return HTMLResponse(
-        f"{pre}<h1>Tag {esc_html(tag_name)}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{table}{POSTAMBLE}"
-    )
+	"""Single tag view. Port of git_tag."""
+	if not h:
+		raise HTTPException(status_code=400, detail="Tag ref or hash (h) required")
+	if not is_valid_ref_format(h):
+		raise HTTPException(status_code=400, detail="Invalid ref or hash")
+	try:
+		repo = open_repo(project)
+		obj = repo.revparse_single(h)
+	except (KeyError, pygit2.GitError, OSError):
+		raise HTTPException(status_code=404, detail="Tag or object not found")
+	if not isinstance(obj, pygit2.Tag):
+		raise HTTPException(status_code=404, detail="Not a tag object")
+	tag_oid = str(obj.id)
+	tag_data = parse_tag(project, tag_oid)
+	if not tag_data:
+		raise HTTPException(status_code=404, detail="Tag not found")
+	target_oid = tag_data.get("object", "")
+	target_type = tag_data.get("type", "commit")
+	tagger = tag_data.get("tagger", "")
+	tagger_epoch = tag_data.get("tagger_epoch")
+	tagger_tz = tag_data.get("tagger_tz")
+	message = (tag_data.get("message") or "").strip()
+	tag_name = None
+	for name, _ref, oid in git_get_tags_list(project):
+		if oid == tag_oid:
+			tag_name = name
+			break
+	if tag_name is None:
+		tag_name = tag_oid[:7]
+	target_short = target_oid[:7] if target_oid else ""
+	if target_type == "commit":
+		object_link = (
+			f'<a href="/project/{project}?a=commit&h={quote(target_oid, safe="")}">{esc_html(target_short)}</a>'
+		)
+	elif target_type == "tree":
+		object_link = f'<a href="/project/{project}?a=tree&h={quote(target_oid, safe="")}">{esc_html(target_short)}</a>'
+	else:
+		object_link = esc_html(target_short)
+	table_rows = [
+		["Tag", esc_html(tag_name)],
+		["Object", f"{object_link} ({esc_html(target_type)})"],
+		["Tagger", esc_html(tagger)],
+		["Date", esc_html(_format_date(tagger_epoch, tagger_tz))],
+	]
+	if message:
+		table_rows.append(["Message", f"<pre class='tag-message'>{esc_html(message)}</pre>"])
+	table = env.get_template("table.html").render(cols=["Field", "Value"], rows=table_rows)
+	title = f"Tag {esc_html(tag_name)} - {esc_html(project)}"
+	pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
+	return HTMLResponse(
+		f"{pre}<h1>Tag {esc_html(tag_name)}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{table}{POSTAMBLE}"
+	)
 
 
 def git_commit(project: str, h: str | None) -> HTMLResponse:
-    """Single commit view. Port of git_commit (git show)."""
-    if not h:
-        raise HTTPException(status_code=400, detail="Commit hash (h) required")
-    if not is_valid_ref_format(h):
-        raise HTTPException(status_code=400, detail="Invalid ref or hash")
-    try:
-        repo = open_repo(project)
-        obj = repo.revparse_single(h)
-    except (KeyError, pygit2.GitError, OSError):
-        raise HTTPException(status_code=404, detail="Commit not found")
-    if not isinstance(obj, pygit2.Commit):
-        raise HTTPException(status_code=404, detail="Not a commit object")
-    commit = obj
-    oid = str(commit.id)
-    oid_short = oid[:7]
-    data = parse_commit(project, oid)
-    author = data.get("author", "")
-    author_email = data.get("author_email", "")
-    author_date = _format_date(data.get("author_epoch"), data.get("author_tz"))
-    tree_oid = data.get("tree", "")
-    message = (data.get("body") or "").strip()
-    commit_link = f"/project/{project}?a=commit&h={quote(oid, safe='')}"
-    tree_link = f"/project/{project}?a=tree&h={quote(oid, safe='')}"
-    diff_link = f"/project/{project}?a=commitdiff&h={quote(oid, safe='')}"
-    patch_link = f"/project/{project}?a=patch&h={quote(oid, safe='')}"
-    table_rows = [
-        ["commit", f'<a href="{commit_link}">{esc_html(oid)}</a>'],
-        ["Author", f"{esc_html(author)} &lt;{esc_html(author_email)}&gt;"],
-        ["Date", esc_html(author_date)],
-        ["tree", f'<a href="{tree_link}">{esc_html(tree_oid[:7])}</a>'],
-    ]
-    for i, parent_id in enumerate(commit.parent_ids):
-        parent_oid = str(parent_id)
-        parent_short = parent_oid[:7]
-        parent_link = f"/project/{project}?a=commit&h={quote(parent_oid, safe='')}"
-        label = "parent" if len(commit.parent_ids) == 1 else f"parent ({i + 1})"
-        table_rows.append([label, f'<a href="{parent_link}">{esc_html(parent_short)}</a>'])
-    if message:
-        table_rows.append(["", f"<pre class='commit-message'>{esc_html(message)}</pre>"])
-    table_rows.append(["", f'<a href="{diff_link}">diff</a> · <a href="{patch_link}">patch</a>'])
-    table = env.get_template("table.html").render(cols=["Field", "Value"], rows=table_rows)
-    title = f"Commit {esc_html(oid_short)} - {esc_html(project)}"
-    pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
-    return HTMLResponse(
-        f"{pre}<h1>Commit {esc_html(oid_short)}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{table}{POSTAMBLE}"
-    )
+	"""Single commit view. Port of git_commit (git show)."""
+	if not h:
+		raise HTTPException(status_code=400, detail="Commit hash (h) required")
+	if not is_valid_ref_format(h):
+		raise HTTPException(status_code=400, detail="Invalid ref or hash")
+	try:
+		repo = open_repo(project)
+		obj = repo.revparse_single(h)
+	except (KeyError, pygit2.GitError, OSError):
+		raise HTTPException(status_code=404, detail="Commit not found")
+	if not isinstance(obj, pygit2.Commit):
+		raise HTTPException(status_code=404, detail="Not a commit object")
+	commit = obj
+	oid = str(commit.id)
+	oid_short = oid[:7]
+	data = parse_commit(project, oid)
+	author = data.get("author", "")
+	author_email = data.get("author_email", "")
+	author_date = _format_date(data.get("author_epoch"), data.get("author_tz"))
+	tree_oid = data.get("tree", "")
+	message = (data.get("body") or "").strip()
+	commit_link = f"/project/{project}?a=commit&h={quote(oid, safe='')}"
+	tree_link = f"/project/{project}?a=tree&h={quote(oid, safe='')}"
+	diff_link = f"/project/{project}?a=commitdiff&h={quote(oid, safe='')}"
+	patch_link = f"/project/{project}?a=patch&h={quote(oid, safe='')}"
+	table_rows = [
+		["commit", f'<a href="{commit_link}">{esc_html(oid)}</a>'],
+		["Author", f"{esc_html(author)} &lt;{esc_html(author_email)}&gt;"],
+		["Date", esc_html(author_date)],
+		["tree", f'<a href="{tree_link}">{esc_html(tree_oid[:7])}</a>'],
+	]
+	for i, parent_id in enumerate(commit.parent_ids):
+		parent_oid = str(parent_id)
+		parent_short = parent_oid[:7]
+		parent_link = f"/project/{project}?a=commit&h={quote(parent_oid, safe='')}"
+		label = "parent" if len(commit.parent_ids) == 1 else f"parent ({i + 1})"
+		table_rows.append([label, f'<a href="{parent_link}">{esc_html(parent_short)}</a>'])
+	if message:
+		table_rows.append(["", f"<pre class='commit-message'>{esc_html(message)}</pre>"])
+	table_rows.append(["", f'<a href="{diff_link}">diff</a> · <a href="{patch_link}">patch</a>'])
+	table = env.get_template("table.html").render(cols=["Field", "Value"], rows=table_rows)
+	title = f"Commit {esc_html(oid_short)} - {esc_html(project)}"
+	pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
+	return HTMLResponse(
+		f"{pre}<h1>Commit {esc_html(oid_short)}</h1><p>Project: <a href='/project/{project}'>{esc_html(project)}</a></p>{table}{POSTAMBLE}"
+	)
 
 
 def git_commitdiff(project: str, h: str | None) -> HTMLResponse:
-    """Commit diff page: unified diff rendered with diff2html (script id diff2html-script)."""
-    diff_text, oid_short = _commit_unified_diff_or_raise(project, h or "")
-    diff_b64 = base64.b64encode(diff_text.encode("utf-8")).decode("ascii")
-    title = f"Commit diff {oid_short} - {project}"
-    pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
-    body = env.get_template("commitdiff.html").render(
-        project=project,
-        oid_short=oid_short,
-        diff_b64=diff_b64,
-        commit_link=f"/project/{project}?a=commit&h={quote(h or '', safe='')}",
-    )
-    return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
+	"""Commit diff page: unified diff rendered with diff2html (script id diff2html-script)."""
+	diff_text, oid_short = _commit_unified_diff_or_raise(project, h or "")
+	diff_b64 = base64.b64encode(diff_text.encode("utf-8")).decode("ascii")
+	title = f"Commit diff {oid_short} - {project}"
+	pre = PREAMBLE.render(title=title, site_name=config.SITE_NAME)
+	body = env.get_template("commitdiff.html").render(
+		project=project,
+		oid_short=oid_short,
+		diff_b64=diff_b64,
+		commit_link=f"/project/{project}?a=commit&h={quote(h or '', safe='')}",
+	)
+	return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
 
 
 def git_patch(project: str, h: str | None) -> PlainTextResponse:
-    """Single-commit patch (plain text unified diff)."""
-    if not h:
-        raise HTTPException(status_code=400, detail="Commit hash (h) required")
-    body, oid_short = _commit_unified_diff_or_raise(project, h)
-    filename = f"{project}-{oid_short}.patch"
-    return PlainTextResponse(
-        body,
-        media_type="text/x-diff; charset=utf-8",
-        headers={"Content-Disposition": f'inline; filename="{filename}"'},
-    )
+	"""Single-commit patch (plain text unified diff)."""
+	if not h:
+		raise HTTPException(status_code=400, detail="Commit hash (h) required")
+	body, oid_short = _commit_unified_diff_or_raise(project, h)
+	filename = f"{project}-{oid_short}.patch"
+	return PlainTextResponse(
+		body,
+		media_type="text/x-diff; charset=utf-8",
+		headers={"Content-Disposition": f'inline; filename="{filename}"'},
+	)
 
 
 def git_patches(project: str, h: str | None, hb: str | None) -> PlainTextResponse:
-    """Multi-commit patches (plain text). Range hb..h if hb given, else single commit."""
-    if not h:
-        raise HTTPException(status_code=400, detail="Commit hash (h) required")
-    if hb:
-        oids = get_commits_in_range(project, h, hb)
-        if not oids:
-            raise HTTPException(status_code=404, detail="No commits in range")
-        parts = []
-        for oid in oids:
-            try:
-                diff, _ = get_commit_unified_diff(project, oid)
-                if diff:
-                    parts.append(diff)
-            except (KeyError, pygit2.GitError, OSError, ValueError):
-                pass
-        body = "\n".join(parts)
-        filename = f"{project}-{h[:7]}-patches.patch"
-    else:
-        body, oid_short = _commit_unified_diff_or_raise(project, h)
-        filename = f"{project}-{oid_short}.patch"
-    return PlainTextResponse(
-        body,
-        media_type="text/x-diff; charset=utf-8",
-        headers={"Content-Disposition": f'inline; filename="{filename}"'},
-    )
+	"""Multi-commit patches (plain text). Range hb..h if hb given, else single commit."""
+	if not h:
+		raise HTTPException(status_code=400, detail="Commit hash (h) required")
+	if hb:
+		oids = get_commits_in_range(project, h, hb)
+		if not oids:
+			raise HTTPException(status_code=404, detail="No commits in range")
+		parts = []
+		for oid in oids:
+			try:
+				diff, _ = get_commit_unified_diff(project, oid)
+				if diff:
+					parts.append(diff)
+			except (KeyError, pygit2.GitError, OSError, ValueError):
+				pass
+		body = "\n".join(parts)
+		filename = f"{project}-{h[:7]}-patches.patch"
+	else:
+		body, oid_short = _commit_unified_diff_or_raise(project, h)
+		filename = f"{project}-{oid_short}.patch"
+	return PlainTextResponse(
+		body,
+		media_type="text/x-diff; charset=utf-8",
+		headers={"Content-Disposition": f'inline; filename="{filename}"'},
+	)
 
 
 def git_blobdiff(
-    project: str,
-    h: str | None,
-    hb: str | None,
-    f: str | None,
-    fp: str | None,
+	project: str,
+	h: str | None,
+	hb: str | None,
+	f: str | None,
+	fp: str | None,
 ) -> HTMLResponse:
-    """Blob diff page: diff between two blob versions, rendered with diff2html."""
-    if not h or not hb:
-        raise HTTPException(status_code=400, detail="Both h and hb (refs) required for blob diff")
-    if not f:
-        raise HTTPException(status_code=400, detail="File path (f) required for blob diff")
-    if not is_valid_ref_format(h) or not is_valid_ref_format(hb):
-        raise HTTPException(status_code=400, detail="Invalid ref or hash")
-    path_new = f or ""
-    path_old = fp if fp is not None else path_new
-    if not is_valid_pathname(path_old) or not is_valid_pathname(path_new):
-        raise HTTPException(status_code=400, detail="Invalid path")
-    diff_text = get_blob_unified_diff(project, hb, path_old, h, path_new)
-    if diff_text is None:
-        raise HTTPException(status_code=404, detail="Blob not found")
-    diff_b64 = base64.b64encode(diff_text.encode("utf-8")).decode("ascii")
-    blob_link = f"/project/{project}?a=blob&h={quote(h or '', safe='')}&f={quote(path_new, safe='/')}"
-    pre = PREAMBLE.render(
-        title=f"Blob diff - {esc_html(path_new)} - {esc_html(project)}",
-        site_name=config.SITE_NAME,
-    )
-    body = env.get_template("blobdiff.html").render(
-        project=project,
-        path_new=path_new,
-        diff_b64=diff_b64,
-        blob_link=blob_link,
-    )
-    return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
+	"""Blob diff page: diff between two blob versions, rendered with diff2html."""
+	if not h or not hb:
+		raise HTTPException(status_code=400, detail="Both h and hb (refs) required for blob diff")
+	if not f:
+		raise HTTPException(status_code=400, detail="File path (f) required for blob diff")
+	if not is_valid_ref_format(h) or not is_valid_ref_format(hb):
+		raise HTTPException(status_code=400, detail="Invalid ref or hash")
+	path_new = f or ""
+	path_old = fp if fp is not None else path_new
+	if not is_valid_pathname(path_old) or not is_valid_pathname(path_new):
+		raise HTTPException(status_code=400, detail="Invalid path")
+	diff_text = get_blob_unified_diff(project, hb, path_old, h, path_new)
+	if diff_text is None:
+		raise HTTPException(status_code=404, detail="Blob not found")
+	diff_b64 = base64.b64encode(diff_text.encode("utf-8")).decode("ascii")
+	blob_link = f"/project/{project}?a=blob&h={quote(h or '', safe='')}&f={quote(path_new, safe='/')}"
+	pre = PREAMBLE.render(
+		title=f"Blob diff - {esc_html(path_new)} - {esc_html(project)}",
+		site_name=config.SITE_NAME,
+	)
+	body = env.get_template("blobdiff.html").render(
+		project=project,
+		path_new=path_new,
+		diff_b64=diff_b64,
+		blob_link=blob_link,
+	)
+	return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
 
 
 def git_blobpatch(
-    project: str,
-    h: str | None,
-    hb: str | None,
-    f: str | None,
-    fp: str | None,
+	project: str,
+	h: str | None,
+	hb: str | None,
+	f: str | None,
+	fp: str | None,
 ) -> PlainTextResponse:
-    """Blob diff as plain unified diff."""
-    if not h or not hb:
-        raise HTTPException(status_code=400, detail="Both h and hb (refs) required for blob diff")
-    if not f:
-        raise HTTPException(status_code=400, detail="File path (f) required for blob diff")
-    if not is_valid_ref_format(h) or not is_valid_ref_format(hb):
-        raise HTTPException(status_code=400, detail="Invalid ref or hash")
-    path_new = f or ""
-    path_old = fp if fp is not None else path_new
-    if not is_valid_pathname(path_old) or not is_valid_pathname(path_new):
-        raise HTTPException(status_code=400, detail="Invalid path")
-    body = get_blob_unified_diff(project, hb, path_old, h, path_new)
-    if body is None:
-        raise HTTPException(status_code=404, detail="Blob not found")
-    filename = f"{path_new.split('/')[-1] or 'blob'}.patch"
-    return PlainTextResponse(
-        body,
-        media_type="text/x-diff; charset=utf-8",
-        headers={"Content-Disposition": f'inline; filename="{filename}"'},
-    )
+	"""Blob diff as plain unified diff."""
+	if not h or not hb:
+		raise HTTPException(status_code=400, detail="Both h and hb (refs) required for blob diff")
+	if not f:
+		raise HTTPException(status_code=400, detail="File path (f) required for blob diff")
+	if not is_valid_ref_format(h) or not is_valid_ref_format(hb):
+		raise HTTPException(status_code=400, detail="Invalid ref or hash")
+	path_new = f or ""
+	path_old = fp if fp is not None else path_new
+	if not is_valid_pathname(path_old) or not is_valid_pathname(path_new):
+		raise HTTPException(status_code=400, detail="Invalid path")
+	body = get_blob_unified_diff(project, hb, path_old, h, path_new)
+	if body is None:
+		raise HTTPException(status_code=404, detail="Blob not found")
+	filename = f"{path_new.split('/')[-1] or 'blob'}.patch"
+	return PlainTextResponse(
+		body,
+		media_type="text/x-diff; charset=utf-8",
+		headers={"Content-Disposition": f'inline; filename="{filename}"'},
+	)
diff --git a/pygitweb/config.py b/pygitweb/config.py
index 7ebb827..f7a8257 100644
--- a/pygitweb/config.py
+++ b/pygitweb/config.py
@@ -4,6 +4,7 @@ Ported from gitweb/gitweb.perl (evaluate_gitweb_config, read_config_file, get_lo
 check_loadavg, known_snapshot_formats, feature_*, gitweb_get_feature, gitweb_check_feature,
 filter_snapshot_fmts, filter_and_validate_refs, configure_gitweb_features, get_branch_refs).
 """
+
 from __future__ import annotations
 
 import os
@@ -13,45 +14,74 @@ from typing import Any
 
 # Allowed actions (from %actions in gitweb.perl)
 ACTIONS = {
-    "blame",
-    "blame_incremental",
-    "blame_data",
-    "blobdiff",
-    "blobpatch",
-    "blob",
-    "blob_plain",
-    "commitdiff",
-    "commit",
-    "heads",
-    "history",
-    "log",
-    "patch",
-    "patches",
-    "remotes",
-    "rss",
-    "atom",
-    "search",
-    "search_help",
-    "shortlog",
-    "summary",
-    "tag",
-    "tags",
-    "tree",
-    "snapshot",
-    "object",
-    "opml",
-    "project_list",
-    "project_index",
+	"blame",
+	"blame_incremental",
+	"blame_data",
+	"blobdiff",
+	"blobpatch",
+	"blob",
+	"blob_plain",
+	"commitdiff",
+	"commit",
+	"heads",
+	"history",
+	"log",
+	"patch",
+	"patches",
+	"remotes",
+	"rss",
+	"atom",
+	"search",
+	"search_help",
+	"shortlog",
+	"summary",
+	"tag",
+	"tags",
+	"tree",
+	"snapshot",
+	"object",
+	"opml",
+	"project_list",
+	"project_index",
 }
 
 # Map file extension to highlight.js language (class name)
 BLOB_LANG = {
-    "py": "python", "js": "javascript", "ts": "typescript", "jsx": "javascript", "tsx": "typescript",
-    "html": "html", "htm": "html", "css": "css", "scss": "scss", "json": "json", "md": "markdown",
-    "sh": "bash", "bash": "bash", "yml": "yaml", "yaml": "yaml", "xml": "xml", "go": "go",
-    "rs": "rust", "java": "java", "c": "c", "h": "c", "cpp": "cpp", "cc": "cpp", "cxx": "cpp",
-    "sql": "sql", "r": "r", "rb": "ruby", "php": "php", "swift": "swift", "kt": "kotlin",
-    "vue": "xml", "toml": "toml", "ini": "ini", "cfg": "ini", "dockerfile": "dockerfile",
+	"py": "python",
+	"js": "javascript",
+	"ts": "typescript",
+	"jsx": "javascript",
+	"tsx": "typescript",
+	"html": "html",
+	"htm": "html",
+	"css": "css",
+	"scss": "scss",
+	"json": "json",
+	"md": "markdown",
+	"sh": "bash",
+	"bash": "bash",
+	"yml": "yaml",
+	"yaml": "yaml",
+	"xml": "xml",
+	"go": "go",
+	"rs": "rust",
+	"java": "java",
+	"c": "c",
+	"h": "c",
+	"cpp": "cpp",
+	"cc": "cpp",
+	"cxx": "cpp",
+	"sql": "sql",
+	"r": "r",
+	"rb": "ruby",
+	"php": "php",
+	"swift": "swift",
+	"kt": "kotlin",
+	"vue": "xml",
+	"toml": "toml",
+	"ini": "ini",
+	"cfg": "ini",
+	"dockerfile": "dockerfile",
 }
 
 # Defaults (equivalent to @GITWEB_*@ in gitweb.perl)
@@ -62,7 +92,11 @@ SITE_NAME = os.environ.get("GITWEB_SITENAME", "") or "DistGit"
 EXPORT_OK = os.environ.get("GITWEB_EXPORT_OK", "")
 # When True, list all directories under project root without repo/export_ok checks (default on for now).
 LIST_ALL = os.environ.get("GITWEB_LIST_ALL", "1").lower() in ("1", "true", "yes")
-STRICT_EXPORT = os.environ.get("GITWEB_STRICT_EXPORT", "0").lower() in ("1", "true", "yes")
+STRICT_EXPORT = os.environ.get("GITWEB_STRICT_EXPORT", "0").lower() in (
+	"1",
+	"true",
+	"yes",
+)
 GIT_BINDIR = os.environ.get("GIT_BINDIR", "")
 GIT = (GIT_BINDIR + "/git") if GIT_BINDIR else "git"
 MAXLOAD: float | None = None  # 300 in perl; None = disabled
@@ -88,46 +122,46 @@ GITWEB_CONFIG_COMMON = os.environ.get("GITWEB_CONFIG_COMMON", "")
 
 # Snapshot formats (from %known_snapshot_formats)
 KNOWN_SNAPSHOT_FORMATS: dict[str, dict[str, Any]] = {
-    "tgz": {
-        "display": "tar.gz",
-        "type": "application/x-gzip",
-        "suffix": ".tar.gz",
-        "format": "tar",
-        "compressor": ["gzip", "-n"],
-    },
-    "tbz2": {
-        "display": "tar.bz2",
-        "type": "application/x-bzip2",
-        "suffix": ".tar.bz2",
-        "format": "tar",
-        "compressor": ["bzip2"],
-    },
-    "txz": {
-        "display": "tar.xz",
-        "type": "application/x-xz",
-        "suffix": ".tar.xz",
-        "format": "tar",
-        "compressor": ["xz"],
-        "disabled": True,
-    },
-    "zip": {
-        "display": "zip",
-        "type": "application/zip",
-        "suffix": ".zip",
-        "format": "zip",
-    },
+	"tgz": {
+		"display": "tar.gz",
+		"type": "application/x-gzip",
+		"suffix": ".tar.gz",
+		"format": "tar",
+		"compressor": ["gzip", "-n"],
+	},
+	"tbz2": {
+		"display": "tar.bz2",
+		"type": "application/x-bzip2",
+		"suffix": ".tar.bz2",
+		"format": "tar",
+		"compressor": ["bzip2"],
+	},
+	"txz": {
+		"display": "tar.xz",
+		"type": "application/x-xz",
+		"suffix": ".tar.xz",
+		"format": "tar",
+		"compressor": ["xz"],
+		"disabled": True,
+	},
+	"zip": {
+		"display": "zip",
+		"type": "application/zip",
+		"suffix": ".zip",
+		"format": "zip",
+	},
 }
 
 KNOWN_SNAPSHOT_FORMAT_ALIASES: dict[str, str | None] = {
-    "gzip": "tgz",
-    "bzip2": "tbz2",
-    "xz": "txz",
-    "x-gzip": None,
-    "gz": None,
-    "x-bzip2": None,
-    "bz2": None,
-    "x-zip": None,
-    "": None,
+	"gzip": "tgz",
+	"bzip2": "tbz2",
+	"xz": "txz",
+	"x-gzip": None,
+	"gz": None,
+	"x-bzip2": None,
+	"bz2": None,
+	"x-zip": None,
+	"": None,
 }
 
 
@@ -138,154 +172,166 @@ _extra_branch_refs: list[str] = []
 
 
 def read_config_file(filename: str | None) -> bool:
-    """Load and execute a Python config file. Returns True on success. Port of read_config_file."""
-    if not filename or not os.path.exists(filename):
-        return False
-    try:
-        with open(filename) as f:
-            code = compile(f.read(), filename, "exec")
-            glob = {
-                "PROJECTROOT": PROJECTROOT,
-                "PROJECTS_LIST": PROJECTS_LIST,
-                "SITE_NAME": SITE_NAME,
-                "EXPORT_OK": EXPORT_OK,
-                "LIST_ALL": LIST_ALL,
-                "STRICT_EXPORT": STRICT_EXPORT,
-                "GIT": GIT,
-                "MAXLOAD": MAXLOAD,
-                "KNOWN_SNAPSHOT_FORMATS": KNOWN_SNAPSHOT_FORMATS,
-                "os": os,
-                "Path": Path,
-            }
-            exec(code, glob)
-            for k in ("PROJECTROOT", "PROJECTS_LIST", "SITE_NAME", "EXPORT_OK", "LIST_ALL", "STRICT_EXPORT", "GIT", "MAXLOAD", "KNOWN_SNAPSHOT_FORMATS"):
-                if k in glob:
-                    globals()[k] = glob[k]
-        return True
-    except Exception:
-        raise
+	"""Load and execute a Python config file. Returns True on success. Port of read_config_file."""
+	if not filename or not os.path.exists(filename):
+		return False
+	try:
+		with open(filename) as f:
+			code = compile(f.read(), filename, "exec")
+			glob = {
+				"PROJECTROOT": PROJECTROOT,
+				"PROJECTS_LIST": PROJECTS_LIST,
+				"SITE_NAME": SITE_NAME,
+				"EXPORT_OK": EXPORT_OK,
+				"LIST_ALL": LIST_ALL,
+				"STRICT_EXPORT": STRICT_EXPORT,
+				"GIT": GIT,
+				"MAXLOAD": MAXLOAD,
+				"KNOWN_SNAPSHOT_FORMATS": KNOWN_SNAPSHOT_FORMATS,
+				"os": os,
+				"Path": Path,
+			}
+			exec(code, glob)
+			for k in (
+				"PROJECTROOT",
+				"PROJECTS_LIST",
+				"SITE_NAME",
+				"EXPORT_OK",
+				"LIST_ALL",
+				"STRICT_EXPORT",
+				"GIT",
+				"MAXLOAD",
+				"KNOWN_SNAPSHOT_FORMATS",
+			):
+				if k in glob:
+					globals()[k] = glob[k]
+		return True
+	except Exception:
+		raise
 
 
 def evaluate_gitweb_config() -> None:
-    """Resolve config paths and load common + instance/system config. Port of evaluate_gitweb_config."""
-    global GITWEB_CONFIG, GITWEB_CONFIG_SYSTEM, GITWEB_CONFIG_COMMON
-    if not GITWEB_CONFIG:
-        GITWEB_CONFIG = os.environ.get("GITWEB_CONFIG", "")
-    if not GITWEB_CONFIG_SYSTEM:
-        GITWEB_CONFIG_SYSTEM = os.environ.get("GITWEB_CONFIG_SYSTEM", "")
-    if not GITWEB_CONFIG_COMMON:
-        GITWEB_CONFIG_COMMON = os.environ.get("GITWEB_CONFIG_COMMON", "")
-
-    if GITWEB_CONFIG == GITWEB_CONFIG_COMMON:
-        GITWEB_CONFIG = ""
-    if GITWEB_CONFIG_SYSTEM == GITWEB_CONFIG_COMMON:
-        GITWEB_CONFIG_SYSTEM = ""
-
-    if GITWEB_CONFIG_COMMON and os.path.exists(GITWEB_CONFIG_COMMON):
-        read_config_file(GITWEB_CONFIG_COMMON)
-    if GITWEB_CONFIG and os.path.exists(GITWEB_CONFIG):
-        read_config_file(GITWEB_CONFIG)
-        return
-    if GITWEB_CONFIG_SYSTEM and os.path.exists(GITWEB_CONFIG_SYSTEM):
-        read_config_file(GITWEB_CONFIG_SYSTEM)
+	"""Resolve config paths and load common + instance/system config. Port of evaluate_gitweb_config."""
+	global GITWEB_CONFIG, GITWEB_CONFIG_SYSTEM, GITWEB_CONFIG_COMMON
+	if not GITWEB_CONFIG:
+		GITWEB_CONFIG = os.environ.get("GITWEB_CONFIG", "")
+	if not GITWEB_CONFIG_SYSTEM:
+		GITWEB_CONFIG_SYSTEM = os.environ.get("GITWEB_CONFIG_SYSTEM", "")
+	if not GITWEB_CONFIG_COMMON:
+		GITWEB_CONFIG_COMMON = os.environ.get("GITWEB_CONFIG_COMMON", "")
+
+	if GITWEB_CONFIG == GITWEB_CONFIG_COMMON:
+		GITWEB_CONFIG = ""
+	if GITWEB_CONFIG_SYSTEM == GITWEB_CONFIG_COMMON:
+		GITWEB_CONFIG_SYSTEM = ""
+
+	if GITWEB_CONFIG_COMMON and os.path.exists(GITWEB_CONFIG_COMMON):
+		read_config_file(GITWEB_CONFIG_COMMON)
+	if GITWEB_CONFIG and os.path.exists(GITWEB_CONFIG):
+		read_config_file(GITWEB_CONFIG)
+		return
+	if GITWEB_CONFIG_SYSTEM and os.path.exists(GITWEB_CONFIG_SYSTEM):
+		read_config_file(GITWEB_CONFIG_SYSTEM)
 
 
 def get_loadavg() -> float:
-    """First element of load average, or 0 if unavailable. Port of get_loadavg."""
-    try:
-        return os.getloadavg()[0]
-    except (OSError, AttributeError):
-        pass
-    try:
-        with open("/proc/loadavg") as f:
-            return float(f.read().split()[0])
-    except (OSError, ValueError):
-        return 0.0
+	"""First element of load average, or 0 if unavailable. Port of get_loadavg."""
+	try:
+		return os.getloadavg()[0]
+	except (OSError, AttributeError):
+		pass
+	try:
+		with open("/proc/loadavg") as f:
+			return float(f.read().split()[0])
+	except (OSError, ValueError):
+		return 0.0
 
 
 def check_loadavg() -> None:
-    """Raise 503 if load exceeds maxload. Port of check_loadavg."""
-    if MAXLOAD is not None and get_loadavg() > MAXLOAD:
-        raise RuntimeError("503:The load average on the server is too high")
+	"""Raise 503 if load exceeds maxload. Port of check_loadavg."""
+	if MAXLOAD is not None and get_loadavg() > MAXLOAD:
+		raise RuntimeError("503:The load average on the server is too high")
 
 
 def gitweb_get_feature(
-    name: str,
-    git_dir: str | None = None,
-    get_project_config: Any = None,
+	name: str,
+	git_dir: str | None = None,
+	get_project_config: Any = None,
 ) -> list[Any]:
-    """Return feature value(s); project override when git_dir and get_project_config set. Port of gitweb_get_feature."""
-    if name == "snapshot":
-        defaults = _feature_snapshot_default
-        if git_dir and get_project_config:
-            val = get_project_config("snapshot") if callable(get_project_config) else None
-            if val:
-                defaults = [] if val.strip().lower() == "none" else [x.strip() for x in re.split(r"[\s,]+", val) if x.strip()]
-        return list(defaults)
-    if name == "avatar":
-        return ["gravatar"]  # default
-    if name == "extra-branch-refs":
-        if git_dir and get_project_config and callable(get_project_config):
-            val = get_project_config("extrabranchrefs")
-            if val:
-                parts = [val] if isinstance(val, str) else (val if isinstance(val, list) else [])
-                return [x for part in parts for x in str(part).split()]
-        return []
-    return []
+	"""Return feature value(s); project override when git_dir and get_project_config set. Port of gitweb_get_feature."""
+	if name == "snapshot":
+		defaults = _feature_snapshot_default
+		if git_dir and get_project_config:
+			val = get_project_config("snapshot") if callable(get_project_config) else None
+			if val:
+				defaults = (
+					[] if val.strip().lower() == "none" else [x.strip() for x in re.split(r"[\s,]+", val) if x.strip()]
+				)
+		return list(defaults)
+	if name == "avatar":
+		return ["gravatar"]  # default
+	if name == "extra-branch-refs":
+		if git_dir and get_project_config and callable(get_project_config):
+			val = get_project_config("extrabranchrefs")
+			if val:
+				parts = [val] if isinstance(val, str) else (val if isinstance(val, list) else [])
+				return [x for part in parts for x in str(part).split()]
+		return []
+	return []
 
 
 def gitweb_check_feature(name: str, git_dir: str | None = None, get_project_config: Any = None) -> bool | Any:
-    """First value of gitweb_get_feature. Port of gitweb_check_feature."""
-    vals = gitweb_get_feature(name, git_dir, get_project_config)
-    return vals[0] if vals else False
+	"""First value of gitweb_get_feature. Port of gitweb_check_feature."""
+	vals = gitweb_get_feature(name, git_dir, get_project_config)
+	return vals[0] if vals else False
 
 
 def filter_snapshot_fmts(fmts: list[str]) -> list[str]:
-    """Resolve aliases and drop unknown/disabled. Port of filter_snapshot_fmts."""
-    result = []
-    for f in fmts:
-        key = KNOWN_SNAPSHOT_FORMAT_ALIASES.get(f, f)
-        if key is None:
-            continue
-        if key not in KNOWN_SNAPSHOT_FORMATS:
-            continue
-        opt = KNOWN_SNAPSHOT_FORMATS[key]
-        if opt.get("disabled"):
-            continue
-        result.append(key)
-    return result
+	"""Resolve aliases and drop unknown/disabled. Port of filter_snapshot_fmts."""
+	result = []
+	for f in fmts:
+		key = KNOWN_SNAPSHOT_FORMAT_ALIASES.get(f, f)
+		if key is None:
+			continue
+		if key not in KNOWN_SNAPSHOT_FORMATS:
+			continue
+		opt = KNOWN_SNAPSHOT_FORMATS[key]
+		if opt.get("disabled"):
+			continue
+		result.append(key)
+	return result
 
 
 def filter_and_validate_refs(refs: list[str], is_valid_ref_format: Any) -> list[str]:
-    """Validate ref names and unique sort; 'heads' omitted (added in get_branch_refs). Port of filter_and_validate_refs."""
-    seen: set[str] = set()
-    for ref in refs:
-        if not is_valid_ref_format(ref):
-            raise ValueError(f"Invalid ref '{ref}' in 'extra-branch-refs' feature")
-        if ref != "heads":
-            seen.add(ref)
-    return sorted(seen)
+	"""Validate ref names and unique sort; 'heads' omitted (added in get_branch_refs). Port of filter_and_validate_refs."""
+	seen: set[str] = set()
+	for ref in refs:
+		if not is_valid_ref_format(ref):
+			raise ValueError(f"Invalid ref '{ref}' in 'extra-branch-refs' feature")
+		if ref != "heads":
+			seen.add(ref)
+	return sorted(seen)
 
 
 def configure_gitweb_features(
-    get_project_config: Any = None,
-    git_dir: str | None = None,
-    is_valid_ref_format: Any = None,
+	get_project_config: Any = None,
+	git_dir: str | None = None,
+	is_valid_ref_format: Any = None,
 ) -> None:
-    """Set snapshot_fmts and extra_branch_refs. Port of configure_gitweb_features."""
-    global _snapshot_fmts, _extra_branch_refs
-    _snapshot_fmts = filter_snapshot_fmts(gitweb_get_feature("snapshot", git_dir, get_project_config))
-    avatar = gitweb_get_feature("avatar", git_dir, get_project_config)
-    if avatar and avatar[0] not in ("gravatar", "picon"):
-        avatar = [""]
-    raw = gitweb_get_feature("extra-branch-refs", git_dir, get_project_config)
-    _extra_branch_refs = filter_and_validate_refs(raw, is_valid_ref_format) if is_valid_ref_format else []
+	"""Set snapshot_fmts and extra_branch_refs. Port of configure_gitweb_features."""
+	global _snapshot_fmts, _extra_branch_refs
+	_snapshot_fmts = filter_snapshot_fmts(gitweb_get_feature("snapshot", git_dir, get_project_config))
+	avatar = gitweb_get_feature("avatar", git_dir, get_project_config)
+	if avatar and avatar[0] not in ("gravatar", "picon"):
+		avatar = [""]
+	raw = gitweb_get_feature("extra-branch-refs", git_dir, get_project_config)
+	_extra_branch_refs = filter_and_validate_refs(raw, is_valid_ref_format) if is_valid_ref_format else []
 
 
 def get_branch_refs() -> list[str]:
-    """Return ['heads', ...extra_branch_refs]. Port of get_branch_refs."""
-    return ["heads"] + _extra_branch_refs
+	"""Return ['heads', ...extra_branch_refs]. Port of get_branch_refs."""
+	return ["heads"] + _extra_branch_refs
 
 
 def get_snapshot_fmts() -> list[str]:
-    return _snapshot_fmts
+	return _snapshot_fmts
diff --git a/pygitweb/formatting.py b/pygitweb/formatting.py
index 5334350..ddbca09 100644
--- a/pygitweb/formatting.py
+++ b/pygitweb/formatting.py
@@ -3,249 +3,259 @@ Formatting: escaping (esc_param, esc_path_info, esc_url, esc_attr, esc_html, esc
 quot_cec, quot_upr, unquote, untabify, to_utf8, chop_str, chop_and_escape_str, age_class, age_string.
 Ported from gitweb/gitweb.perl.
 """
+
 from __future__ import annotations
 
 import html
 import re
-from urllib.parse import quote, quote_plus, unquote as url_unquote
+from urllib.parse import quote, quote_plus
 
 # Fallback encoding when bytes are not valid UTF-8 (gitweb: $fallback_encoding)
 FALLBACK_ENCODING = "latin1"
 
 # Control character escape codes (CEC). Port of quot_cec.
 _CEC_MAP = {
-    "\t": r"\t",
-    "\n": r"\n",
-    "\r": r"\r",
-    "\f": r"\f",
-    "\b": r"\b",
-    "\a": r"\a",
-    "\x1b": r"\e",
-    "\v": r"\v",
-    "\0": r"\0",
+	"\t": r"\t",
+	"\n": r"\n",
+	"\r": r"\r",
+	"\f": r"\f",
+	"\b": r"\b",
+	"\a": r"\a",
+	"\x1b": r"\e",
+	"\v": r"\v",
+	"\0": r"\0",
 }
 
 
 def to_utf8(s: str | bytes | None) -> str | None:
-    """Decode to UTF-8 string; use fallback encoding if not valid UTF-8. Port of to_utf8."""
-    if s is None:
-        return None
-    if isinstance(s, str):
-        return s
-    try:
-        return s.decode("utf-8")
-    except UnicodeDecodeError:
-        return s.decode(FALLBACK_ENCODING, errors="replace")
+	"""Decode to UTF-8 string; use fallback encoding if not valid UTF-8. Port of to_utf8."""
+	if s is None:
+		return None
+	if isinstance(s, str):
+		return s
+	try:
+		return s.decode("utf-8")
+	except UnicodeDecodeError:
+		return s.decode(FALLBACK_ENCODING, errors="replace")
 
 
 def esc_param(s: str | None) -> str | None:
-    """URL-encode for query param; keep / and space as +. Port of esc_param."""
-    if s is None:
-        return None
-    return quote_plus(s, safe="")  # gitweb keeps -_.~()/@: and space→+
+	"""URL-encode for query param; keep / and space as +. Port of esc_param."""
+	if s is None:
+		return None
+	return quote_plus(s, safe="")  # gitweb keeps -_.~()/@: and space→+
 
 
 def esc_path_info(s: str | None) -> str | None:
-    """Path segment encoding; ? must be escaped. Port of esc_path_info."""
-    if s is None:
-        return None
-    # Safe: A-Za-z0-9\-_.~();/;:@&= +
-    return quote(s, safe="-_.~();/:@&= ")
+	"""Path segment encoding; ? must be escaped. Port of esc_path_info."""
+	if s is None:
+		return None
+	# Safe: A-Za-z0-9\-_.~();/;:@&= +
+	return quote(s, safe="-_.~();/:@&= ")
 
 
 def esc_url(s: str | None) -> str | None:
-    """URL encoding for href. Port of esc_url (same idea as esc_param)."""
-    if s is None:
-        return None
-    return quote(s, safe="-_.~()/:@!")
+	"""URL encoding for href. Port of esc_url (same idea as esc_param)."""
+	if s is None:
+		return None
+	return quote(s, safe="-_.~()/:@!")
 
 
 def esc_attr(s: str | None) -> str | None:
-    """Escape for HTML attribute. Port of esc_attr."""
-    if s is None:
-        return None
-    return html.escape(s, quote=True)
+	"""Escape for HTML attribute. Port of esc_attr."""
+	if s is None:
+		return None
+	return html.escape(s, quote=True)
 
 
 def esc_html(s: str | None) -> str | None:
-    """Escape for HTML body. Port of esc_html."""
-    if s is None:
-        return None
-    return html.escape(s, quote=False)
+	"""Escape for HTML body. Port of esc_html."""
+	if s is None:
+		return None
+	return html.escape(s, quote=False)
 
 
 def quot_cec(char: str, nohtml: bool = False) -> str:
-    """Printable representation of control char (CEC). Port of quot_cec."""
-    out = _CEC_MAP.get(char, f"\\{ord(char):02x}")
-    if nohtml:
-        return out
-    return f'<span class="cntrl">{out}</span>'
+	"""Printable representation of control char (CEC). Port of quot_cec."""
+	out = _CEC_MAP.get(char, f"\\{ord(char):02x}")
+	if nohtml:
+		return out
+	return f'<span class="cntrl">{out}</span>'
 
 
 def quot_upr(char: str, nohtml: bool = False) -> str:
-    """Unicode control pictures. Port of quot_upr."""
-    code = 0x2400 + ord(char)
-    out = f"&#{code};"
-    if nohtml:
-        return out
-    return f'<span class="cntrl">{out}</span>'
+	"""Unicode control pictures. Port of quot_upr."""
+	code = 0x2400 + ord(char)
+	out = f"&#{code};"
+	if nohtml:
+		return out
+	return f'<span class="cntrl">{out}</span>'
 
 
 def esc_path(s: str | None, nbsp: bool = False) -> str | None:
-    """UTF-8, HTML-escape, then control chars to quot_cec. Port of esc_path."""
-    if s is None:
-        return None
-    s = to_utf8(s) or s
-    s = html.escape(s, quote=False)
-    if nbsp:
-        s = s.replace(" ", "&nbsp;")
-    result = []
-    for c in s:
-        if ord(c) < 32 or ord(c) == 127:
-            result.append(quot_cec(c))
-        else:
-            result.append(c)
-    return "".join(result)
+	"""UTF-8, HTML-escape, then control chars to quot_cec. Port of esc_path."""
+	if s is None:
+		return None
+	s = to_utf8(s) or s
+	s = html.escape(s, quote=False)
+	if nbsp:
+		s = s.replace(" ", "&nbsp;")
+	result = []
+	for c in s:
+		if ord(c) < 32 or ord(c) == 127:
+			result.append(quot_cec(c))
+		else:
+			result.append(c)
+	return "".join(result)
 
 
 def sanitize(s: str | None) -> str | None:
-    """XHTML-safe: control chars to CEC except tab/lf/cr. Port of sanitize."""
-    if s is None:
-        return None
-    s = to_utf8(s) or s
-    result = []
-    for c in s:
-        if c in "\t\n\r":
-            result.append(c)
-        elif ord(c) < 32 or ord(c) == 127:
-            result.append(quot_cec(c, nohtml=True))
-        else:
-            result.append(c)
-    return "".join(result)
+	"""XHTML-safe: control chars to CEC except tab/lf/cr. Port of sanitize."""
+	if s is None:
+		return None
+	s = to_utf8(s) or s
+	result = []
+	for c in s:
+		if c in "\t\n\r":
+			result.append(c)
+		elif ord(c) < 32 or ord(c) == 127:
+			result.append(quot_cec(c, nohtml=True))
+		else:
+			result.append(c)
+	return "".join(result)
 
 
 def unquote(s: str | None) -> str:
-    """Unescape git-style quoted filenames (C and octal). Port of unquote."""
-    if s is None:
-        return ""
-
-    def unq(seq: str) -> str:
-        es = {"t": "\t", "n": "\n", "r": "\r", "f": "\f", "b": "\b", "a": "\a", "e": "\x1b", "v": "\v"}
-        if re.match(r"^[0-7]{1,3}$", seq):
-            return chr(int(seq, 8))
-        return es.get(seq, seq)
-
-    if len(s) >= 2 and s[0] == '"' and s[-1] == '"':
-        s = s[1:-1]
-        s = re.sub(r"\\([^0-7]|[0-7]{1,3})", lambda m: unq(m.group(1)), s)
-    return s
+	"""Unescape git-style quoted filenames (C and octal). Port of unquote."""
+	if s is None:
+		return ""
+
+	def unq(seq: str) -> str:
+		es = {
+			"t": "\t",
+			"n": "\n",
+			"r": "\r",
+			"f": "\f",
+			"b": "\b",
+			"a": "\a",
+			"e": "\x1b",
+			"v": "\v",
+		}
+		if re.match(r"^[0-7]{1,3}$", seq):
+			return chr(int(seq, 8))
+		return es.get(seq, seq)
+
+	if len(s) >= 2 and s[0] == '"' and s[-1] == '"':
+		s = s[1:-1]
+		s = re.sub(r"\\([^0-7]|[0-7]{1,3})", lambda m: unq(m.group(1)), s)
+	return s
 
 
 def untabify(line: str, tabwidth: int = 8) -> str:
-    """Expand tabs to spaces. Port of untabify."""
-    result = []
-    col = 0
-    for c in line:
-        if c == "\t":
-            n = tabwidth - (col % tabwidth)
-            result.append(" " * n)
-            col += n
-        else:
-            result.append(c)
-            col += 1
-    return "".join(result)
+	"""Expand tabs to spaces. Port of untabify."""
+	result = []
+	col = 0
+	for c in line:
+		if c == "\t":
+			n = tabwidth - (col % tabwidth)
+			result.append(" " * n)
+			col += n
+		else:
+			result.append(c)
+			col += 1
+	return "".join(result)
 
 
 def chop_str(
-    s: str,
-    length: int,
-    add_len: int = 10,
-    where: str = "right",
+	s: str,
+	length: int,
+	add_len: int = 10,
+	where: str = "right",
 ) -> str:
-    """Chop on word boundary between length and length+add_len. Port of chop_str."""
-    s = to_utf8(s) or ""
-    if where == "center":
-        if length + 5 >= len(s):
-            return s
-        half = length // 2
-        endre = re.compile(rf".{{{half}}}\w{{0,{add_len}}}", re.DOTALL)
-        begre = re.compile(rf"\w{{0,{add_len}}}.{{{half}}}$", re.DOTALL)
-        m1 = re.match(rf"^(.{{{half}}}\w{{0,{add_len}}})(.*)$", s, re.DOTALL)
-        if not m1:
-            return s[: length // 2] + " ... " + s[-(length // 2) :]
-        left, rest = m1.group(1), m1.group(2)
-        m2 = re.match(rf"^(.*?)(\w{{0,{add_len}}}.{{{half}}})$", rest, re.DOTALL)
-        if not m2:
-            return left + " ... " + rest[-half:]
-        mid, right = m2.group(1), m2.group(2)
-        if len(mid) > 5:
-            mid = " ... "
-        return left + mid + right
-    if length + 4 >= len(s):
-        return s
-    if where == "left":
-        begre = re.compile(rf"\w{{0,{add_len}}}.{{{length}}}$")
-        m = begre.search(s)
-        if m:
-            body = m.group(0)
-            lead = s[: m.start()]
-            if len(lead) > 4:
-                lead = " ..."
-            return lead + body
-        return s
-    # right
-    endre = re.compile(rf".{{{length}}}\w{{0,{add_len}}}")
-    m = endre.match(s)
-    if m:
-        body = m.group(0)
-        tail = s[m.end() :]
-        if len(tail) > 4:
-            tail = "... "
-        return body + tail
-    return s
+	"""Chop on word boundary between length and length+add_len. Port of chop_str."""
+	s = to_utf8(s) or ""
+	if where == "center":
+		if length + 5 >= len(s):
+			return s
+		half = length // 2
+		endre = re.compile(rf".{{{half}}}\w{{0,{add_len}}}", re.DOTALL)
+		begre = re.compile(rf"\w{{0,{add_len}}}.{{{half}}}$", re.DOTALL)
+		m1 = re.match(rf"^(.{{{half}}}\w{{0,{add_len}}})(.*)$", s, re.DOTALL)
+		if not m1:
+			return s[: length // 2] + " ... " + s[-(length // 2) :]
+		left, rest = m1.group(1), m1.group(2)
+		m2 = re.match(rf"^(.*?)(\w{{0,{add_len}}}.{{{half}}})$", rest, re.DOTALL)
+		if not m2:
+			return left + " ... " + rest[-half:]
+		mid, right = m2.group(1), m2.group(2)
+		if len(mid) > 5:
+			mid = " ... "
+		return left + mid + right
+	if length + 4 >= len(s):
+		return s
+	if where == "left":
+		begre = re.compile(rf"\w{{0,{add_len}}}.{{{length}}}$")
+		m = begre.search(s)
+		if m:
+			body = m.group(0)
+			lead = s[: m.start()]
+			if len(lead) > 4:
+				lead = " ..."
+			return lead + body
+		return s
+	# right
+	endre = re.compile(rf".{{{length}}}\w{{0,{add_len}}}")
+	m = endre.match(s)
+	if m:
+		body = m.group(0)
+		tail = s[m.end() :]
+		if len(tail) > 4:
+			tail = "... "
+		return body + tail
+	return s
 
 
 def chop_and_escape_str(
-    s: str,
-    length: int,
-    add_len: int = 10,
-    where: str = "right",
+	s: str,
+	length: int,
+	add_len: int = 10,
+	where: str = "right",
 ) -> str:
-    """Chop then HTML-escape; wrap in span with title if chopped. Port of chop_and_escape_str."""
-    chopped = chop_str(s, length, add_len, where)
-    s = to_utf8(s) or s
-    if chopped == s:
-        return esc_html(chopped) or ""
-    title = esc_attr(s.replace("\n", " ").replace("\r", "?"))
-    escaped = esc_html(chopped) or ""
-    return f'<span title="{title}">{escaped}</span>'
+	"""Chop then HTML-escape; wrap in span with title if chopped. Port of chop_and_escape_str."""
+	chopped = chop_str(s, length, add_len, where)
+	s = to_utf8(s) or s
+	if chopped == s:
+		return esc_html(chopped) or ""
+	title = esc_attr(s.replace("\n", " ").replace("\r", "?"))
+	escaped = esc_html(chopped) or ""
+	return f'<span title="{title}">{escaped}</span>'
 
 
 def age_class(age_seconds: float | None) -> str:
-    """CSS class for age. Port of age_class."""
-    if age_seconds is None:
-        return "noage"
-    if age_seconds < 2 * 3600:
-        return "age0"
-    if age_seconds < 2 * 86400:
-        return "age1"
-    return "age2"
+	"""CSS class for age. Port of age_class."""
+	if age_seconds is None:
+		return "noage"
+	if age_seconds < 2 * 3600:
+		return "age0"
+	if age_seconds < 2 * 86400:
+		return "age1"
+	return "age2"
 
 
 def age_string(age_seconds: float) -> str:
-    """Human-readable age. Port of age_string."""
-    if age_seconds > 2 * 365 * 86400:
-        return f"{int(age_seconds / 86400 / 365)} years ago"
-    if age_seconds > 2 * (365 / 12) * 86400:
-        return f"{int(age_seconds / 86400 / (365/12))} months ago"
-    if age_seconds > 2 * 7 * 86400:
-        return f"{int(age_seconds / 86400 / 7)} weeks ago"
-    if age_seconds > 2 * 86400:
-        return f"{int(age_seconds / 86400)} days ago"
-    if age_seconds > 2 * 3600:
-        return f"{int(age_seconds / 3600)} hours ago"
-    if age_seconds > 2 * 60:
-        return f"{int(age_seconds / 60)} min ago"
-    if age_seconds > 2:
-        return f"{int(age_seconds)} sec ago"
-    return "right now"
+	"""Human-readable age. Port of age_string."""
+	if age_seconds > 2 * 365 * 86400:
+		return f"{int(age_seconds / 86400 / 365)} years ago"
+	if age_seconds > 2 * (365 / 12) * 86400:
+		return f"{int(age_seconds / 86400 / (365 / 12))} months ago"
+	if age_seconds > 2 * 7 * 86400:
+		return f"{int(age_seconds / 86400 / 7)} weeks ago"
+	if age_seconds > 2 * 86400:
+		return f"{int(age_seconds / 86400)} days ago"
+	if age_seconds > 2 * 3600:
+		return f"{int(age_seconds / 3600)} hours ago"
+	if age_seconds > 2 * 60:
+		return f"{int(age_seconds / 60)} min ago"
+	if age_seconds > 2:
+		return f"{int(age_seconds)} sec ago"
+	return "right now"
diff --git a/pygitweb/git_helpers.py b/pygitweb/git_helpers.py
index ce29bef..59a33f1 100644
--- a/pygitweb/git_helpers.py
+++ b/pygitweb/git_helpers.py
@@ -6,17 +6,17 @@ git_get_hash_by_path, git_get_path_by_hash, git_get_file_or_project_config,
 git_get_project_description, git_get_project_category, git_get_references, git_get_heads_list,
 git_get_tags_list, git_get_remotes_list, parse_commit, parse_tag, etc.).
 """
+
 from __future__ import annotations
 
 import os
 import re
-from pathlib import Path
 from typing import Any
 
 import pygit2
 
 # From config
-from pygitweb.config import PROJECTROOT, GIT
+from pygitweb.config import PROJECTROOT
 from pygitweb.formatting import to_utf8
 
 # Common README filenames to look for (order matters: prefer README.md)
@@ -24,110 +24,110 @@ README_CANDIDATES = ["README.md", "README", "readme.md", "readme", "Readme.md"]
 
 
 def _repo_path(project: str) -> str:
-    return os.path.join(PROJECTROOT, project)
+	return os.path.join(PROJECTROOT, project)
 
 
 def open_repo(project: str):
-    """Open pygit2.Repository for project. Replaces git_cmd + --git-dir."""
-    path = _repo_path(project)
-    return pygit2.Repository(path)
+	"""Open pygit2.Repository for project. Replaces git_cmd + --git-dir."""
+	path = _repo_path(project)
+	return pygit2.Repository(path)
 
 
 def git_get_head_hash(project: str) -> str | None:
-    """HEAD commit OID. Port of git_get_head_hash (pygit2: repo.head.target)."""
-    try:
-        repo = open_repo(project)
-        return str(repo.head.target) if repo.head else None
-    except (pygit2.GitError, OSError):
-        return None
+	"""HEAD commit OID. Port of git_get_head_hash (pygit2: repo.head.target)."""
+	try:
+		repo = open_repo(project)
+		return str(repo.head.target) if repo.head else None
+	except (pygit2.GitError, OSError):
+		return None
 
 
 def git_get_full_hash(project: str, ref: str) -> str | None:
-    """Full OID for ref. Port of git_get_full_hash (pygit2 rev_parse)."""
-    return git_get_hash(project, ref)
+	"""Full OID for ref. Port of git_get_full_hash (pygit2 rev_parse)."""
+	return git_get_hash(project, ref)
 
 
 def git_get_short_hash(project: str, ref: str, length: int = 7) -> str | None:
-    """Short OID. Port of git_get_short_hash."""
-    full = git_get_hash(project, ref)
-    return full[:length] if full else None
+	"""Short OID. Port of git_get_short_hash."""
+	full = git_get_hash(project, ref)
+	return full[:length] if full else None
 
 
 def git_get_hash(project: str, ref: str) -> str | None:
-    """Resolve ref to full OID. Port of git_get_hash (pygit2 revparse_single)."""
-    try:
-        repo = open_repo(project)
-        obj = repo.revparse_single(ref)
-        return str(obj.id) if obj else None
-    except (KeyError, pygit2.GitError, OSError):
-        return None
+	"""Resolve ref to full OID. Port of git_get_hash (pygit2 revparse_single)."""
+	try:
+		repo = open_repo(project)
+		obj = repo.revparse_single(ref)
+		return str(obj.id) if obj else None
+	except (KeyError, pygit2.GitError, OSError):
+		return None
 
 
 def git_get_type(project: str, ref: str) -> str | None:
-    """Object type: commit, tree, blob, tag. Port of git_get_type (pygit2 obj.type)."""
-    try:
-        repo = open_repo(project)
-        obj = repo.revparse_single(ref)
-        return obj.type_str if obj else None
-    except (KeyError, pygit2.GitError, OSError):
-        return None
+	"""Object type: commit, tree, blob, tag. Port of git_get_type (pygit2 obj.type)."""
+	try:
+		repo = open_repo(project)
+		obj = repo.revparse_single(ref)
+		return obj.type_str if obj else None
+	except (KeyError, pygit2.GitError, OSError):
+		return None
 
 
 def hash_set_multi(d: dict[str, Any], key: str, value: Any) -> None:
-    """Store multi-value: first value direct, rest in list. Port of hash_set_multi."""
-    if key not in d:
-        d[key] = value
-    elif not isinstance(d[key], list):
-        d[key] = [d[key], value]
-    else:
-        d[key].append(value)
+	"""Store multi-value: first value direct, rest in list. Port of hash_set_multi."""
+	if key not in d:
+		d[key] = value
+	elif not isinstance(d[key], list):
+		d[key] = [d[key], value]
+	else:
+		d[key].append(value)
 
 
 def git_parse_project_config(project: str, section_regexp: str | None = None) -> dict[str, Any]:
-    """All config key/values; optionally filter by section. Port of git_parse_project_config."""
-    try:
-        repo = open_repo(project)
-        cfg = repo.config
-        result: dict[str, Any] = {}
-        for entry in cfg:
-            key = entry.name
-            if section_regexp and not re.search(rf"^(?:{section_regexp})\.", key):
-                continue
-            value = entry.value
-            hash_set_multi(result, key, value)
-        return result
-    except (pygit2.GitError, OSError):
-        return {}
+	"""All config key/values; optionally filter by section. Port of git_parse_project_config."""
+	try:
+		repo = open_repo(project)
+		cfg = repo.config
+		result: dict[str, Any] = {}
+		for entry in cfg:
+			key = entry.name
+			if section_regexp and not re.search(rf"^(?:{section_regexp})\.", key):
+				continue
+			value = entry.value
+			hash_set_multi(result, key, value)
+		return result
+	except (pygit2.GitError, OSError):
+		return {}
 
 
 def config_to_bool(val: str | None) -> bool:
-    """Config value to bool: true/yes/1. Port of config_to_bool."""
-    if val is None:
-        return True
-    val = (val or "").strip()
-    if re.match(r"^\d+$", val):
-        return int(val) != 0
-    return val.lower() in ("true", "yes")
+	"""Config value to bool: true/yes/1. Port of config_to_bool."""
+	if val is None:
+		return True
+	val = (val or "").strip()
+	if re.match(r"^\d+$", val):
+		return int(val) != 0
+	return val.lower() in ("true", "yes")
 
 
 def config_to_int(val: str | None) -> int | str:
-    """Config value to int; k/m/g suffix. Port of config_to_int."""
-    if val is None:
-        return 0
-    val = (val or "").strip()
-    m = re.match(r"^([0-9]*)([kmg])$", val, re.I)
-    if m:
-        num, unit = m.group(1), m.group(2).lower()
-        mult = {"k": 1024, "m": 1048576, "g": 1073741824}.get(unit, 1)
-        return int(num or 0) * mult
-    return val
+	"""Config value to int; k/m/g suffix. Port of config_to_int."""
+	if val is None:
+		return 0
+	val = (val or "").strip()
+	m = re.match(r"^([0-9]*)([kmg])$", val, re.I)
+	if m:
+		num, unit = m.group(1), m.group(2).lower()
+		mult = {"k": 1024, "m": 1048576, "g": 1073741824}.get(unit, 1)
+		return int(num or 0) * mult
+	return val
 
 
 def config_to_multi(val: Any) -> list[Any]:
-    """Config value to list. Port of config_to_multi."""
-    if isinstance(val, list):
-        return val
-    return [val] if val is not None else []
+	"""Config value to list. Port of config_to_multi."""
+	if isinstance(val, list):
+		return val
+	return [val] if val is not None else []
 
 
 # Per-repo cache for gitweb config (git_parse_project_config result)
@@ -135,421 +135,419 @@ _config_cache: dict[str, tuple[str, dict[str, Any]]] = {}
 
 
 def git_get_project_config(
-    project: str,
-    key: str,
-    config_type: str | None = None,
+	project: str,
+	key: str,
+	config_type: str | None = None,
 ) -> str | list[str] | bool | None:
-    """Single config value; gitweb.* section. Port of git_get_project_config."""
-    key = key.lower().replace("_", "")
-    if key.startswith("gitweb."):
-        key = key[7:]
-    if re.search(r"\W", key):
-        return None
-    full_key = f"gitweb.{key}"
-    git_dir = _repo_path(project)
-    cache_key = git_dir
-    if cache_key not in _config_cache or _config_cache[cache_key][0] != os.path.join(git_dir, "config"):
-        cfg = git_parse_project_config(project, "gitweb")
-        _config_cache[cache_key] = (os.path.join(git_dir, "config"), cfg)
-    _, cfg = _config_cache[cache_key]
-    raw = cfg.get(full_key)
-    if raw is None:
-        return None
-    if config_type == "bool" or config_type == "--bool":
-        return config_to_bool(raw[0] if isinstance(raw, list) else raw)
-    if config_type == "int" or config_type == "--int":
-        return config_to_int(raw[0] if isinstance(raw, list) else raw)
-    if isinstance(raw, list):
-        return raw[0] if len(raw) == 1 else raw
-    return raw
+	"""Single config value; gitweb.* section. Port of git_get_project_config."""
+	key = key.lower().replace("_", "")
+	if key.startswith("gitweb."):
+		key = key[7:]
+	if re.search(r"\W", key):
+		return None
+	full_key = f"gitweb.{key}"
+	git_dir = _repo_path(project)
+	cache_key = git_dir
+	if cache_key not in _config_cache or _config_cache[cache_key][0] != os.path.join(git_dir, "config"):
+		cfg = git_parse_project_config(project, "gitweb")
+		_config_cache[cache_key] = (os.path.join(git_dir, "config"), cfg)
+	_, cfg = _config_cache[cache_key]
+	raw = cfg.get(full_key)
+	if raw is None:
+		return None
+	if config_type == "bool" or config_type == "--bool":
+		return config_to_bool(raw[0] if isinstance(raw, list) else raw)
+	if config_type == "int" or config_type == "--int":
+		return config_to_int(raw[0] if isinstance(raw, list) else raw)
+	if isinstance(raw, list):
+		return raw[0] if len(raw) == 1 else raw
+	return raw
 
 
 def git_get_hash_by_path(project: str, base: str, path: str, obj_type: str | None = None) -> str | None:
-    """OID of path at base (tree-ish). Port of git_get_hash_by_path (pygit2 tree path lookup)."""
-    try:
-        repo = open_repo(project)
-        tree = repo.revparse_single(base).peel(pygit2.Tree)
-        path = path.rstrip("/")
-        entry = tree / path
-        if not entry:
-            return None
-        if obj_type and entry.type_str != obj_type:
-            return None
-        return str(entry.id)
-    except (KeyError, pygit2.GitError, OSError):
-        return None
-
-
-def get_tree_at_ref_path(
-    project: str, ref: str | None, path: str | None
-) -> tuple[pygit2.Tree, str] | None:
-    """
-    Resolve the tree at ref (commit or tree) and optional path.
-    Returns (tree, ref_oid) for listing, or None if not found.
-    ref_oid is the resolved OID to use in URLs (same revision).
-    """
-    try:
-        repo = open_repo(project)
-        base_ref = ref or (str(repo.head.target) if repo.head else None)
-        if not base_ref:
-            return None
-        obj = repo.revparse_single(base_ref)
-        ref_oid = str(obj.id)
-        base_tree = obj.peel(pygit2.Tree)
-        if not path or not path.strip("/"):
-            return (base_tree, ref_oid)
-        path_clean = path.strip("/")
-        entry = base_tree / path_clean
-        if not entry or entry.type_str != "tree":
-            return None
-        return (repo[entry.id].peel(pygit2.Tree), ref_oid)
-    except (KeyError, pygit2.GitError, OSError):
-        return None
+	"""OID of path at base (tree-ish). Port of git_get_hash_by_path (pygit2 tree path lookup)."""
+	try:
+		repo = open_repo(project)
+		tree = repo.revparse_single(base).peel(pygit2.Tree)
+		path = path.rstrip("/")
+		entry = tree / path
+		if not entry:
+			return None
+		if obj_type and entry.type_str != obj_type:
+			return None
+		return str(entry.id)
+	except (KeyError, pygit2.GitError, OSError):
+		return None
+
+
+def get_tree_at_ref_path(project: str, ref: str | None, path: str | None) -> tuple[pygit2.Tree, str] | None:
+	"""
+	Resolve the tree at ref (commit or tree) and optional path.
+	Returns (tree, ref_oid) for listing, or None if not found.
+	ref_oid is the resolved OID to use in URLs (same revision).
+	"""
+	try:
+		repo = open_repo(project)
+		base_ref = ref or (str(repo.head.target) if repo.head else None)
+		if not base_ref:
+			return None
+		obj = repo.revparse_single(base_ref)
+		ref_oid = str(obj.id)
+		base_tree = obj.peel(pygit2.Tree)
+		if not path or not path.strip("/"):
+			return (base_tree, ref_oid)
+		path_clean = path.strip("/")
+		entry = base_tree / path_clean
+		if not entry or entry.type_str != "tree":
+			return None
+		return (repo[entry.id].peel(pygit2.Tree), ref_oid)
+	except (KeyError, pygit2.GitError, OSError):
+		return None
 
 
 def get_blob_at_ref_path(project: str, ref: str | None, path: str | None) -> tuple[pygit2.Blob, str] | None:
-    """
-    Resolve the blob at ref (commit or tree) and path.
-    Returns (blob, ref_oid) or None if not found or not a blob.
-    """
-    if not path or not path.strip("/"):
-        return None
-    try:
-        repo = open_repo(project)
-        base_ref = ref or (str(repo.head.target) if repo.head else None)
-        if not base_ref:
-            return None
-        obj = repo.revparse_single(base_ref)
-        ref_oid = str(obj.id)
-        base_tree = obj.peel(pygit2.Tree)
-        path_clean = path.strip("/")
-        entry = base_tree / path_clean
-        if not entry or entry.type_str != "blob":
-            return None
-        return (repo[entry.id].peel(pygit2.Blob), ref_oid)
-    except (KeyError, pygit2.GitError, OSError):
-        return None
+	"""
+	Resolve the blob at ref (commit or tree) and path.
+	Returns (blob, ref_oid) or None if not found or not a blob.
+	"""
+	if not path or not path.strip("/"):
+		return None
+	try:
+		repo = open_repo(project)
+		base_ref = ref or (str(repo.head.target) if repo.head else None)
+		if not base_ref:
+			return None
+		obj = repo.revparse_single(base_ref)
+		ref_oid = str(obj.id)
+		base_tree = obj.peel(pygit2.Tree)
+		path_clean = path.strip("/")
+		entry = base_tree / path_clean
+		if not entry or entry.type_str != "blob":
+			return None
+		return (repo[entry.id].peel(pygit2.Blob), ref_oid)
+	except (KeyError, pygit2.GitError, OSError):
+		return None
 
 
 def git_get_path_by_hash(project: str, base: str, oid_str: str) -> str | None:
-    """Path of object with given OID in base tree. Port of git_get_path_by_hash."""
-    try:
-        repo = open_repo(project)
-        tree = repo.revparse_single(base).peel(pygit2.Tree)
-
-        def find_in_tree(t: pygit2.Tree, prefix: str) -> str | None:
-            for e in t:
-                p = f"{prefix}{e.name}" if prefix else e.name
-                if str(e.id) == oid_str:
-                    return p
-                if e.type_str == "tree":
-                    subtree = repo[e.id]
-                    if isinstance(subtree, pygit2.Tree):
-                        found = find_in_tree(subtree, p + "/")
-                        if found:
-                            return found
-            return None
-
-        return find_in_tree(tree, "")
-    except (KeyError, pygit2.GitError, OSError):
-        return None
+	"""Path of object with given OID in base tree. Port of git_get_path_by_hash."""
+	try:
+		repo = open_repo(project)
+		tree = repo.revparse_single(base).peel(pygit2.Tree)
+
+		def find_in_tree(t: pygit2.Tree, prefix: str) -> str | None:
+			for e in t:
+				p = f"{prefix}{e.name}" if prefix else e.name
+				if str(e.id) == oid_str:
+					return p
+				if e.type_str == "tree":
+					subtree = repo[e.id]
+					if isinstance(subtree, pygit2.Tree):
+						found = find_in_tree(subtree, p + "/")
+						if found:
+							return found
+			return None
+
+		return find_in_tree(tree, "")
+	except (KeyError, pygit2.GitError, OSError):
+		return None
 
 
 def git_get_file_or_project_config(project: str, name: str) -> str | None:
-    """Value from $GIT_DIR/name file or gitweb.name config. Port of git_get_file_or_project_config."""
-    path = os.path.join(_repo_path(project), name)
-    if os.path.isfile(path):
-        try:
-            with open(path) as f:
-                return f.read().strip()
-        except OSError:
-            pass
-    val = git_get_project_config(project, name)
-    return val[0] if isinstance(val, list) else (val if isinstance(val, str) else None)
+	"""Value from $GIT_DIR/name file or gitweb.name config. Port of git_get_file_or_project_config."""
+	path = os.path.join(_repo_path(project), name)
+	if os.path.isfile(path):
+		try:
+			with open(path) as f:
+				return f.read().strip()
+		except OSError:
+			pass
+	val = git_get_project_config(project, name)
+	return val[0] if isinstance(val, list) else (val if isinstance(val, str) else None)
 
 
 def git_get_project_description(project: str) -> str | None:
-    """Content of description file or config. Port of git_get_project_description."""
-    return git_get_file_or_project_config(project, "description")
+	"""Content of description file or config. Port of git_get_project_description."""
+	return git_get_file_or_project_config(project, "description")
 
 
 def git_get_project_category(project: str) -> str | None:
-    """Category file. Port of git_get_project_category."""
-    return git_get_file_or_project_config(project, "category")
+	"""Category file. Port of git_get_project_category."""
+	return git_get_file_or_project_config(project, "category")
 
 
 def git_get_references(project: str, ref_prefix: str = "refs/heads") -> list[tuple[str, str]]:
-    """List (ref_name, oid) for prefix. Port of git_get_references (pygit2 references)."""
-    try:
-        repo = open_repo(project)
-        prefix = ref_prefix + "/"
-        return [
-            (ref_name, str(repo.references[ref_name].resolve().target))
-            for ref_name in repo.references
-            if ref_name.startswith(prefix)
-        ]
-    except (pygit2.GitError, OSError):
-        return []
+	"""List (ref_name, oid) for prefix. Port of git_get_references (pygit2 references)."""
+	try:
+		repo = open_repo(project)
+		prefix = ref_prefix + "/"
+		return [
+			(ref_name, str(repo.references[ref_name].resolve().target))
+			for ref_name in repo.references
+			if ref_name.startswith(prefix)
+		]
+	except (pygit2.GitError, OSError):
+		return []
 
 
 def git_get_heads_list(project: str) -> list[tuple[str, str, str]]:
-    """List (name, ref, oid) for heads. Port of git_get_heads_list."""
-    refs = git_get_references(project, "refs/heads")
-    return [(ref.replace("refs/heads/", ""), ref, oid) for ref, oid in refs]
+	"""List (name, ref, oid) for heads. Port of git_get_heads_list."""
+	refs = git_get_references(project, "refs/heads")
+	return [(ref.replace("refs/heads/", ""), ref, oid) for ref, oid in refs]
 
 
 def _tag_timestamp(repo: pygit2.Repository, oid: str) -> int:
-    """Return tagger time for an annotated tag, or committer time for a lightweight tag (commit)."""
-    try:
-        obj = repo.revparse_single(oid)
-        if isinstance(obj, pygit2.Tag) and obj.tagger:
-            return obj.tagger.time
-        if isinstance(obj, pygit2.Commit):
-            return obj.committer.time
-    except (KeyError, pygit2.GitError):
-        pass
-    return 0
+	"""Return tagger time for an annotated tag, or committer time for a lightweight tag (commit)."""
+	try:
+		obj = repo.revparse_single(oid)
+		if isinstance(obj, pygit2.Tag) and obj.tagger:
+			return obj.tagger.time
+		if isinstance(obj, pygit2.Commit):
+			return obj.committer.time
+	except (KeyError, pygit2.GitError):
+		pass
+	return 0
 
 
 def git_get_tags_list(project: str) -> list[tuple[str, str, str]]:
-    """List (name, ref, oid) for tags. Sorted by tag creation time descending (newest first)."""
-    try:
-        repo = open_repo(project)
-        result = []
-        for ref in repo.references.iterator(pygit2.GIT_REFERENCES_TAGS):
-            oid = str(ref.resolve().target)
-            name = ref.name.replace("refs/tags/", "")
-            ts = _tag_timestamp(repo, oid)
-            result.append((name, ref.name, oid, ts))
-        result.sort(key=lambda x: (x[3], x[0]), reverse=True)
-        return [(name, ref_name, oid) for name, ref_name, oid, _ in result]
-    except (pygit2.GitError, OSError):
-        return []
+	"""List (name, ref, oid) for tags. Sorted by tag creation time descending (newest first)."""
+	try:
+		repo = open_repo(project)
+		result = []
+		for ref in repo.references.iterator(pygit2.GIT_REFERENCES_TAGS):
+			oid = str(ref.resolve().target)
+			name = ref.name.replace("refs/tags/", "")
+			ts = _tag_timestamp(repo, oid)
+			result.append((name, ref.name, oid, ts))
+		result.sort(key=lambda x: (x[3], x[0]), reverse=True)
+		return [(name, ref_name, oid) for name, ref_name, oid, _ in result]
+	except (pygit2.GitError, OSError):
+		return []
 
 
 def git_get_remotes_list(project: str) -> list[str]:
-    """Remote names. Port of git_get_remotes_list (pygit2 remotes)."""
-    try:
-        repo = open_repo(project)
-        return list(repo.remotes.names())
-    except (pygit2.GitError, OSError):
-        return []
+	"""Remote names. Port of git_get_remotes_list (pygit2 remotes)."""
+	try:
+		repo = open_repo(project)
+		return list(repo.remotes.names())
+	except (pygit2.GitError, OSError):
+		return []
 
 
 def git_get_remotes_info(project: str) -> list[dict[str, Any]]:
-    """Remote info: name, url, push_url. Uses pygit2 Remote.url and Remote.push_url."""
-    try:
-        repo = open_repo(project)
-        result = []
-        for name in repo.remotes.names():
-            remote = repo.remotes[name]
-            result.append({
-                "name": name,
-                "url": remote.url or "",
-                "push_url": remote.push_url or remote.url or "",
-            })
-        return result
-    except (pygit2.GitError, OSError):
-        return []
+	"""Remote info: name, url, push_url. Uses pygit2 Remote.url and Remote.push_url."""
+	try:
+		repo = open_repo(project)
+		result = []
+		for name in repo.remotes.names():
+			remote = repo.remotes[name]
+			result.append(
+				{
+					"name": name,
+					"url": remote.url or "",
+					"push_url": remote.push_url or remote.url or "",
+				}
+			)
+		return result
+	except (pygit2.GitError, OSError):
+		return []
 
 
 def parse_commit(project: str, oid: str) -> dict[str, Any]:
-    """Commit metadata dict. Port of parse_commit (pygit2 commit)."""
-    try:
-        repo = open_repo(project)
-        obj = repo.revparse_single(oid)
-        if not isinstance(obj, pygit2.Commit):
-            return {}
-        commit = obj
-        return {
-            "parent": [str(p) for p in commit.parent_ids],
-            "tree": str(commit.tree_id),
-            "author": commit.author.name,
-            "author_email": commit.author.email,
-            "author_epoch": commit.author.time,
-            "author_tz": commit.author.offset,
-            "committer": commit.committer.name,
-            "committer_email": commit.committer.email,
-            "committer_epoch": commit.committer.time,
-            "committer_tz": commit.committer.offset,
-            "subject": commit.message.split("\n")[0] if commit.message else "",
-            "body": commit.message or "",
-        }
-    except (KeyError, pygit2.GitError, OSError):
-        return {}
+	"""Commit metadata dict. Port of parse_commit (pygit2 commit)."""
+	try:
+		repo = open_repo(project)
+		obj = repo.revparse_single(oid)
+		if not isinstance(obj, pygit2.Commit):
+			return {}
+		commit = obj
+		return {
+			"parent": [str(p) for p in commit.parent_ids],
+			"tree": str(commit.tree_id),
+			"author": commit.author.name,
+			"author_email": commit.author.email,
+			"author_epoch": commit.author.time,
+			"author_tz": commit.author.offset,
+			"committer": commit.committer.name,
+			"committer_email": commit.committer.email,
+			"committer_epoch": commit.committer.time,
+			"committer_tz": commit.committer.offset,
+			"subject": commit.message.split("\n")[0] if commit.message else "",
+			"body": commit.message or "",
+		}
+	except (KeyError, pygit2.GitError, OSError):
+		return {}
 
 
 def parse_tag(project: str, oid: str) -> dict[str, Any]:
-    """Tag metadata. Port of parse_tag (pygit2 tag)."""
-    try:
-        repo = open_repo(project)
-        obj = repo.revparse_single(oid)
-        if not isinstance(obj, pygit2.Tag):
-            return {}
-        tag = obj
-        return {
-            "object": str(tag.target),
-            "type": tag.type_str,
-            "tagger": tag.tagger.name if tag.tagger else "",
-            "tagger_email": tag.tagger.email if tag.tagger else "",
-            "tagger_epoch": tag.tagger.time if tag.tagger else 0,
-            "tagger_tz": tag.tagger.offset if tag.tagger else 0,
-            "message": tag.message or "",
-        }
-    except (KeyError, pygit2.GitError, OSError):
-        return {}
+	"""Tag metadata. Port of parse_tag (pygit2 tag)."""
+	try:
+		repo = open_repo(project)
+		obj = repo.revparse_single(oid)
+		if not isinstance(obj, pygit2.Tag):
+			return {}
+		tag = obj
+		return {
+			"object": str(tag.target),
+			"type": tag.type_str,
+			"tagger": tag.tagger.name if tag.tagger else "",
+			"tagger_email": tag.tagger.email if tag.tagger else "",
+			"tagger_epoch": tag.tagger.time if tag.tagger else 0,
+			"tagger_tz": tag.tagger.offset if tag.tagger else 0,
+			"message": tag.message or "",
+		}
+	except (KeyError, pygit2.GitError, OSError):
+		return {}
 
 
 def get_commit_history(
-    project: str,
-    ref: str | None = None,
-    path: str | None = None,
-    max_count: int = 100,
-    skip: int = 0,
+	project: str,
+	ref: str | None = None,
+	path: str | None = None,
+	max_count: int = 100,
+	skip: int = 0,
 ) -> list[dict[str, Any]]:
-    """
-    Get commit history for a project, optionally filtered by path.
-    Returns list of commit dicts with oid and parsed commit data.
-    Port of git log functionality.
-    skip: number of matching commits to skip (for pagination).
-    """
-    try:
-        repo = open_repo(project)
-        if ref:
-            try:
-                start_oid = repo.revparse_single(ref).peel(pygit2.Commit).id
-            except (KeyError, pygit2.GitError, ValueError):
-                start_oid = None
-        else:
-            start_oid = repo.head.target if repo.head else None
-        if not start_oid:
-            return []
-        
-        commits = []
-        skipped = 0
-        walker = repo.walk(start_oid, pygit2.GIT_SORT_TIME | pygit2.GIT_SORT_TOPOLOGICAL)
-        
-        if path:
-            # Filter by path: only commits that touched this path (or a file under it)
-            path_clean = path.strip("/")
-            path_prefix = path_clean + "/"
-
-            def touched_path(diff: pygit2.Diff) -> bool:
-                for delta in diff.deltas:
-                    old_p, new_p = delta.old_file.path, delta.new_file.path
-                    if old_p == path_clean or new_p == path_clean:
-                        return True
-                    if old_p.startswith(path_prefix) or new_p.startswith(path_prefix):
-                        return True
-                return False
-
-            for commit in walker:
-                if len(commits) >= max_count:
-                    break
-                try:
-                    if commit.parents:
-                        diff = repo.diff(commit.parents[0], commit)
-                        if not touched_path(diff):
-                            continue
-                    else:
-                        # Root commit: include if path is root or path exists in tree
-                        if path_clean:
-                            try:
-                                commit.tree / path_clean
-                            except KeyError:
-                                continue
-                    if skipped < skip:
-                        skipped += 1
-                        continue
-                    commit_data = parse_commit(project, str(commit.id))
-                    commit_data["oid"] = str(commit.id)
-                    commits.append(commit_data)
-                except (KeyError, AttributeError, pygit2.GitError):
-                    pass
-        else:
-            # No path filter, get all commits
-            for commit in walker:
-                if skipped < skip:
-                    skipped += 1
-                    continue
-                if len(commits) >= max_count:
-                    break
-                commit_data = parse_commit(project, str(commit.id))
-                commit_data["oid"] = str(commit.id)
-                commits.append(commit_data)
-        
-        return commits
-    except (KeyError, pygit2.GitError, OSError):
-        return []
+	"""
+	Get commit history for a project, optionally filtered by path.
+	Returns list of commit dicts with oid and parsed commit data.
+	Port of git log functionality.
+	skip: number of matching commits to skip (for pagination).
+	"""
+	try:
+		repo = open_repo(project)
+		if ref:
+			try:
+				start_oid = repo.revparse_single(ref).peel(pygit2.Commit).id
+			except (KeyError, pygit2.GitError, ValueError):
+				start_oid = None
+		else:
+			start_oid = repo.head.target if repo.head else None
+		if not start_oid:
+			return []
+
+		commits = []
+		skipped = 0
+		walker = repo.walk(start_oid, pygit2.GIT_SORT_TIME | pygit2.GIT_SORT_TOPOLOGICAL)
+
+		if path:
+			# Filter by path: only commits that touched this path (or a file under it)
+			path_clean = path.strip("/")
+			path_prefix = path_clean + "/"
+
+			def touched_path(diff: pygit2.Diff) -> bool:
+				for delta in diff.deltas:
+					old_p, new_p = delta.old_file.path, delta.new_file.path
+					if old_p == path_clean or new_p == path_clean:
+						return True
+					if old_p.startswith(path_prefix) or new_p.startswith(path_prefix):
+						return True
+				return False
+
+			for commit in walker:
+				if len(commits) >= max_count:
+					break
+				try:
+					if commit.parents:
+						diff = repo.diff(commit.parents[0], commit)
+						if not touched_path(diff):
+							continue
+					else:
+						# Root commit: include if path is root or path exists in tree
+						if path_clean:
+							try:
+								commit.tree / path_clean
+							except KeyError:
+								continue
+					if skipped < skip:
+						skipped += 1
+						continue
+					commit_data = parse_commit(project, str(commit.id))
+					commit_data["oid"] = str(commit.id)
+					commits.append(commit_data)
+				except (KeyError, AttributeError, pygit2.GitError):
+					pass
+		else:
+			# No path filter, get all commits
+			for commit in walker:
+				if skipped < skip:
+					skipped += 1
+					continue
+				if len(commits) >= max_count:
+					break
+				commit_data = parse_commit(project, str(commit.id))
+				commit_data["oid"] = str(commit.id)
+				commits.append(commit_data)
+
+		return commits
+	except (KeyError, pygit2.GitError, OSError):
+		return []
 
 
 def get_commits_in_range(project: str, tip: str, base: str | None) -> list[str]:
-    """Return list of commit OIDs from tip back to (but not including) base. Newest first."""
-    try:
-        repo = open_repo(project)
-        tip_commit = repo.revparse_single(tip).peel(pygit2.Commit)
-        walker = repo.walk(tip_commit.id, pygit2.GIT_SORT_TIME | pygit2.GIT_SORT_TOPOLOGICAL)
-        if base:
-            try:
-                walker.hide(repo.revparse_single(base).peel(pygit2.Commit).id)
-            except (KeyError, pygit2.GitError, ValueError):
-                pass
-        return [str(c.id) for c in walker]
-    except (KeyError, pygit2.GitError, OSError, ValueError):
-        return []
+	"""Return list of commit OIDs from tip back to (but not including) base. Newest first."""
+	try:
+		repo = open_repo(project)
+		tip_commit = repo.revparse_single(tip).peel(pygit2.Commit)
+		walker = repo.walk(tip_commit.id, pygit2.GIT_SORT_TIME | pygit2.GIT_SORT_TOPOLOGICAL)
+		if base:
+			try:
+				walker.hide(repo.revparse_single(base).peel(pygit2.Commit).id)
+			except (KeyError, pygit2.GitError, ValueError):
+				pass
+		return [str(c.id) for c in walker]
+	except (KeyError, pygit2.GitError, OSError, ValueError):
+		return []
 
 
 def get_blob_unified_diff(
-    project: str,
-    ref_old: str,
-    path_old: str,
-    ref_new: str,
-    path_new: str,
+	project: str,
+	ref_old: str,
+	path_old: str,
+	ref_new: str,
+	path_new: str,
 ) -> str | None:
-    """
-    Return unified diff between blob at ref_old:path_old and ref_new:path_new using pygit2.
-    Returns None if either blob is not found; otherwise returns the diff string (possibly empty).
-    """
-    result_old = get_blob_at_ref_path(project, ref_old, path_old)
-    result_new = get_blob_at_ref_path(project, ref_new, path_new)
-    if not result_old or not result_new:
-        return None
-    old_blob, _ = result_old
-    new_blob, _ = result_new
-    patch = old_blob.diff(new_blob, old_as_path=path_old, new_as_path=path_new)
-    return patch.text if patch.text else ""
-
-
-def get_readme_at_ref_path(
-    project: str, ref: str | None, dir_path: str | None
-) -> tuple[str, str] | None:
-    """
-    If a README exists at ref in the given tree (dir_path), return (filename, utf8_content).
-    Otherwise None. dir_path is the tree path (e.g. '' for root, 'docs' for docs/).
-    """
-    if not ref:
-        return None
-    for name in README_CANDIDATES:
-        path = f"{dir_path.strip('/')}/{name}".strip("/") if dir_path else name
-        result = get_blob_at_ref_path(project, ref, path)
-        if result:
-            blob, _ = result
-            text = to_utf8(blob.data) or ""
-            return (name, text)
-    return None
+	"""
+	Return unified diff between blob at ref_old:path_old and ref_new:path_new using pygit2.
+	Returns None if either blob is not found; otherwise returns the diff string (possibly empty).
+	"""
+	result_old = get_blob_at_ref_path(project, ref_old, path_old)
+	result_new = get_blob_at_ref_path(project, ref_new, path_new)
+	if not result_old or not result_new:
+		return None
+	old_blob, _ = result_old
+	new_blob, _ = result_new
+	patch = old_blob.diff(new_blob, old_as_path=path_old, new_as_path=path_new)
+	return patch.text if patch.text else ""
+
+
+def get_readme_at_ref_path(project: str, ref: str | None, dir_path: str | None) -> tuple[str, str] | None:
+	"""
+	If a README exists at ref in the given tree (dir_path), return (filename, utf8_content).
+	Otherwise None. dir_path is the tree path (e.g. '' for root, 'docs' for docs/).
+	"""
+	if not ref:
+		return None
+	for name in README_CANDIDATES:
+		path = f"{dir_path.strip('/')}/{name}".strip("/") if dir_path else name
+		result = get_blob_at_ref_path(project, ref, path)
+		if result:
+			blob, _ = result
+			text = to_utf8(blob.data) or ""
+			return (name, text)
+	return None
 
 
 def get_commit_unified_diff(project: str, h: str) -> tuple[str, str]:
-    """
-    Return (unified_diff_text, oid_short) for commit h.
-    Raises KeyError, pygit2.GitError, OSError on error (caller should validate ref and map to HTTPException).
-    """
-    repo = open_repo(project)
-    commit = repo.revparse_single(h).peel(pygit2.Commit)
-    if commit.parents:
-        diff = repo.diff(commit.parents[0], commit)
-    else:
-        diff = commit.tree.diff_to_tree(swap=True)
-    body = diff.patch or ""
-    oid_short = commit.short_id[:7] if len(commit.short_id) >= 7 else commit.short_id
-    return body, oid_short
+	"""
+	Return (unified_diff_text, oid_short) for commit h.
+	Raises KeyError, pygit2.GitError, OSError on error (caller should validate ref and map to HTTPException).
+	"""
+	repo = open_repo(project)
+	commit = repo.revparse_single(h).peel(pygit2.Commit)
+	if commit.parents:
+		diff = repo.diff(commit.parents[0], commit)
+	else:
+		diff = commit.tree.diff_to_tree(swap=True)
+	body = diff.patch or ""
+	oid_short = commit.short_id[:7] if len(commit.short_id) >= 7 else commit.short_id
+	return body, oid_short
diff --git a/pygitweb/main.py b/pygitweb/main.py
index 77fbceb..6f85605 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -2,6 +2,7 @@
 FastAPI app and routes: gitweb actions as path operations.
 Ported from gitweb/gitweb.perl dispatch and action handlers.
 """
+
 from __future__ import annotations
 
 import os
@@ -10,123 +11,134 @@ import tempfile
 import zipfile
 from pathlib import Path
 from typing import Annotated
+from urllib.parse import quote
 
 import pygit2
 from fastapi import FastAPI, File, Form, HTTPException, Query, Request, UploadFile
 from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
 from fastapi.staticfiles import StaticFiles
 
-from pygitweb import config, __meta__
+from pygitweb import __meta__, config
+from pygitweb.actions import (
+	git_blob,
+	git_blobdiff,
+	git_blobpatch,
+	git_commit,
+	git_commitdiff,
+	git_heads,
+	git_history,
+	git_log,
+	git_object,
+	git_patch,
+	git_patches,
+	git_remotes,
+	git_shortlog,
+	git_summary,
+	git_tag,
+	git_tags,
+	git_tree,
+	parse_pagination,
+)
 from pygitweb.config import (
-    ACTIONS,
-    DISTGIT_AUTH,
-    DISTGIT_ADMIN_USER,
-    DISTGIT_ADMIN_PASSWORD,
-    DISTGIT_SESSION_TIMEOUT,
-    EXPORT_OK,
-    PROJECTROOT,
-    STRICT_EXPORT,
-    check_loadavg,
-    configure_gitweb_features,
-    evaluate_gitweb_config,
+	ACTIONS,
+	DISTGIT_ADMIN_PASSWORD,
+	DISTGIT_ADMIN_USER,
+	DISTGIT_AUTH,
+	DISTGIT_SESSION_TIMEOUT,
+	EXPORT_OK,
+	PROJECTROOT,
+	STRICT_EXPORT,
+	check_loadavg,
+	configure_gitweb_features,
+	evaluate_gitweb_config,
 )
 from pygitweb.formatting import esc_html
-from urllib.parse import quote
-
-from pygitweb.git_helpers import git_get_project_config, git_get_type, git_get_references
-from pygitweb.projects import git_get_projects_list, git_get_project_owner
-from pygitweb.templates_env import PREAMBLE, POSTAMBLE, env
-from pygitweb.validation import is_valid_action, is_valid_pathname, is_valid_project, is_valid_ref_format
-
-from pygitweb.actions import (
-    git_blob,
-    git_blobdiff,
-    git_blobpatch,
-    git_commit,
-    git_commitdiff,
-    git_heads,
-    git_history,
-    git_log,
-    git_object,
-    git_patch,
-    git_patches,
-    git_remotes,
-    git_shortlog,
-    git_summary,
-    git_tag,
-    git_tags,
-    git_tree,
-    parse_pagination,
+from pygitweb.git_helpers import (
+	git_get_project_config,
+	git_get_references,
+	git_get_type,
 )
+from pygitweb.projects import git_get_project_owner, git_get_projects_list
 from pygitweb.settings import router as settings_router
-from pygitweb.tasks import board_router, comment_router, create_board_for_project, get_board_tasks_grouped, task_router
+from pygitweb.tasks import (
+	board_router,
+	comment_router,
+	create_board_for_project,
+	get_board_tasks_grouped,
+	task_router,
+)
+from pygitweb.templates_env import POSTAMBLE, PREAMBLE, env
+from pygitweb.validation import (
+	is_valid_action,
+	is_valid_pathname,
+	is_valid_project,
+	is_valid_ref_format,
+)
 
 app = FastAPI(
-    debug=(DISTGIT_AUTH == "None"),
-    title="PyGitWeb",
-    summary="FastAPI + Pygit2 Repo Browser",
-    description=open("pygitweb/README.md", "r", encoding="utf-8").read(),
-    version=__meta__.__version__,
+	debug=(DISTGIT_AUTH == "None"),
+	title="PyGitWeb",
+	summary="FastAPI + Pygit2 Repo Browser",
+	description=open("pygitweb/README.md", encoding="utf-8").read(),
+	version=__meta__.__version__,
 )
 
 # Todo handle with nginx route
 _static_dir = Path(__file__).parent / "static"
 if _static_dir.is_dir():
-    app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")
+	app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")
 
 
 def _project_in_list(project: str) -> bool:
-    lst = git_get_projects_list(
-        filter_path="",
-        paranoid=STRICT_EXPORT,
-        export_ok=EXPORT_OK,
-    )
-    return any(p.get("path") == project for p in lst)
+	lst = git_get_projects_list(
+		filter_path="",
+		paranoid=STRICT_EXPORT,
+		export_ok=EXPORT_OK,
+	)
+	return any(p.get("path") == project for p in lst)
 
 
 _auth_provider = None
 
 
 def _get_auth_provider():
-    """Return auth provider instance if DISTGIT_AUTH is set; None if auth disabled."""
-    global _auth_provider
-    if DISTGIT_AUTH == "None":
-        return None
-    if _auth_provider is not None:
-        return _auth_provider
-    try:
-        mod_name, _, cls_name = DISTGIT_AUTH.rpartition(".")
-        mod = __import__(mod_name, fromlist=[cls_name])
-        cls = getattr(mod, cls_name)
-        # RootAuthProvider accepts admin_user, admin_password, session_timeout from env
-        admin_user = DISTGIT_ADMIN_USER.encode("utf-8") or None
-        admin_password = DISTGIT_ADMIN_PASSWORD.encode("utf-8") or None
-        timeout = DISTGIT_SESSION_TIMEOUT
-        _auth_provider = cls(
-            admin_user=admin_user,
-            admin_password=admin_password,
-            session_timeout=timeout,
-        )
-    except Exception:
-        _auth_provider = None
-    return _auth_provider
+	"""Return auth provider instance if DISTGIT_AUTH is set; None if auth disabled."""
+	global _auth_provider
+	if DISTGIT_AUTH == "None":
+		return None
+	if _auth_provider is not None:
+		return _auth_provider
+	try:
+		mod_name, _, cls_name = DISTGIT_AUTH.rpartition(".")
+		mod = __import__(mod_name, fromlist=[cls_name])
+		cls = getattr(mod, cls_name)
+		# RootAuthProvider accepts admin_user, admin_password, session_timeout from env
+		admin_user = DISTGIT_ADMIN_USER.encode("utf-8") or None
+		admin_password = DISTGIT_ADMIN_PASSWORD.encode("utf-8") or None
+		timeout = DISTGIT_SESSION_TIMEOUT
+		_auth_provider = cls(
+			admin_user=admin_user,
+			admin_password=admin_password,
+			session_timeout=timeout,
+		)
+	except Exception:
+		_auth_provider = None
+	return _auth_provider
 
 
 def _request_can_add_project(request: Request) -> bool:
-    """True if auth is disabled or request has a valid session (header X-Session-Token or param/cookie session)."""
-    if DISTGIT_AUTH == "None":
-        return True
-    provider = _get_auth_provider()
-    if provider is None:
-        return False
-    token = (
-        request.headers.get("X-Session-Token")
-        or request.query_params.get("session")
-        or request.cookies.get("session")
-    )
-    if not token:
-        return False
-    return getattr(provider, "validate_session", lambda _: False)(token)
+	"""True if auth is disabled or request has a valid session (header X-Session-Token or param/cookie session)."""
+	if DISTGIT_AUTH == "None":
+		return True
+	provider = _get_auth_provider()
+	if provider is None:
+		return False
+	token = (
+		request.headers.get("X-Session-Token") or request.query_params.get("session") or request.cookies.get("session")
+	)
+	if not token:
+		return False
+	return getattr(provider, "validate_session", lambda _: False)(token)
 
 
 app.include_router(settings_router)
@@ -137,38 +149,38 @@ app.include_router(comment_router, prefix="/comments")
 
 @app.on_event("startup")
 def startup():
-    evaluate_gitweb_config()
-    configure_gitweb_features(
-        get_project_config=git_get_project_config,
-        git_dir=None,
-        is_valid_ref_format=is_valid_ref_format,
-    )
+	evaluate_gitweb_config()
+	configure_gitweb_features(
+		get_project_config=git_get_project_config,
+		git_dir=None,
+		is_valid_ref_format=is_valid_ref_format,
+	)
 
 
 @app.middleware("http")
 async def loadavg_middleware(request: Request, call_next):
-    try:
-        check_loadavg()
-    except RuntimeError as e:
-        msg = str(e)
-        if msg.startswith("503:"):
-            raise HTTPException(status_code=503, detail=msg[4:])
-        raise
-    return await call_next(request)
+	try:
+		check_loadavg()
+	except RuntimeError as e:
+		msg = str(e)
+		if msg.startswith("503:"):
+			raise HTTPException(status_code=503, detail=msg[4:])
+		raise
+	return await call_next(request)
 
 
 def _validate_project(project: str | None) -> str:
-    if not project:
-        raise HTTPException(status_code=400, detail="Project needed")
-    if not is_valid_project(
-        project,
-        PROJECTROOT,
-        EXPORT_OK,
-        STRICT_EXPORT,
-        _project_in_list,
-    ):
-        raise HTTPException(status_code=404, detail="No such project")
-    return project
+	if not project:
+		raise HTTPException(status_code=400, detail="Project needed")
+	if not is_valid_project(
+		project,
+		PROJECTROOT,
+		EXPORT_OK,
+		STRICT_EXPORT,
+		_project_in_list,
+	):
+		raise HTTPException(status_code=404, detail="No such project")
+	return project
 
 
 # ---------- Routes (no project) ----------
@@ -176,114 +188,115 @@ def _validate_project(project: str | None) -> str:
 
 @app.get("/", response_class=HTMLResponse)
 def git_project_list(
-    request: Request,
-    a: Annotated[str | None, Query(alias="a")] = None,
-    pf: Annotated[str | None, Query(alias="pf")] = None,
-    o: Annotated[str | None, Query(alias="o")] = None,
+	request: Request,
+	a: Annotated[str | None, Query(alias="a")] = None,
+	pf: Annotated[str | None, Query(alias="pf")] = None,
+	o: Annotated[str | None, Query(alias="o")] = None,
 ):
-    """Project list page. Port of git_project_list."""
-    if a and a != "project_list":
-        raise HTTPException(status_code=400, detail="Unknown action")
-    if o and o not in ("none", "project", "descr", "owner", "age"):
-        raise HTTPException(status_code=400, detail="Unknown order parameter")
-    project_filter = pf or ""
-    list_ = git_get_projects_list(
-        filter_path=project_filter,
-        paranoid=STRICT_EXPORT,
-        export_ok=EXPORT_OK,
-    )
-    if not list_:
-        raise HTTPException(status_code=404, detail="No projects found")
-
-    auth_disabled = DISTGIT_AUTH == "None"
-
-    def board_cell(pr: dict) -> str:
-        path = pr.get("path", "")
-        path_enc = quote(path, safe="/")
-        try:
-            board_refs = git_get_references(path, "refs/boards")
-            has_boards = len(board_refs) > 0
-        except Exception:
-            has_boards = False
-        if has_boards:
-            return f'<a href="/project/{path_enc}/board/">project board</a>'
-        grey_style = ' style="color: #999; cursor: not-allowed;"' if not auth_disabled else ""
-        proj_q = quote(path, safe="")
-        return f'<a href="/board/create?project={proj_q}"{grey_style}>create board</a>'
-
-    # We need better escaping logic here but can wait until we harden the templates
-    table = env.get_template("table.html").render(
-        cols=["Project", "Description", "Board"],
-        rows=[
-            [
-                f"<a href='/project/{quote(pr.get('path', ''), safe='/')}'>{esc_html(pr.get('path', ''))}</a>",
-                esc_html(pr.get("descr") or pr.get("path", "")),
-                board_cell(pr),
-            ]
-            for pr in list_[:50]
-        ],
-    )
-    pre = PREAMBLE.render(title=f'{esc_html(config.SITE_NAME)} - Projects', site_name=config.SITE_NAME)
-    return HTMLResponse(f"{pre}<h1>Project List</h1>{table}{POSTAMBLE}")
+	"""Project list page. Port of git_project_list."""
+	if a and a != "project_list":
+		raise HTTPException(status_code=400, detail="Unknown action")
+	if o and o not in ("none", "project", "descr", "owner", "age"):
+		raise HTTPException(status_code=400, detail="Unknown order parameter")
+	project_filter = pf or ""
+	list_ = git_get_projects_list(
+		filter_path=project_filter,
+		paranoid=STRICT_EXPORT,
+		export_ok=EXPORT_OK,
+	)
+	if not list_:
+		raise HTTPException(status_code=404, detail="No projects found")
+
+	auth_disabled = DISTGIT_AUTH == "None"
+
+	def board_cell(pr: dict) -> str:
+		path = pr.get("path", "")
+		path_enc = quote(path, safe="/")
+		try:
+			board_refs = git_get_references(path, "refs/boards")
+			has_boards = len(board_refs) > 0
+		except Exception:
+			has_boards = False
+		if has_boards:
+			return f'<a href="/project/{path_enc}/board/">project board</a>'
+		grey_style = ' style="color: #999; cursor: not-allowed;"' if not auth_disabled else ""
+		proj_q = quote(path, safe="")
+		return f'<a href="/board/create?project={proj_q}"{grey_style}>create board</a>'
+
+	# We need better escaping logic here but can wait until we harden the templates
+	table = env.get_template("table.html").render(
+		cols=["Project", "Description", "Board"],
+		rows=[
+			[
+				f"<a href='/project/{quote(pr.get('path', ''), safe='/')}'>{esc_html(pr.get('path', ''))}</a>",
+				esc_html(pr.get("descr") or pr.get("path", "")),
+				board_cell(pr),
+			]
+			for pr in list_[:50]
+		],
+	)
+	pre = PREAMBLE.render(title=f"{esc_html(config.SITE_NAME)} - Projects", site_name=config.SITE_NAME)
+	return HTMLResponse(f"{pre}<h1>Project List</h1>{table}{POSTAMBLE}")
 
 
 @app.get("/index", response_class=PlainTextResponse)
 def git_project_index(
-    pf: Annotated[str | None, Query(alias="pf")] = None,
+	pf: Annotated[str | None, Query(alias="pf")] = None,
 ):
-    """Plain text project index (path owner). Port of git_project_index."""
-    from urllib.parse import quote_plus
-    projects = git_get_projects_list(
-        filter_path=pf or "",
-        paranoid=STRICT_EXPORT,
-        export_ok=EXPORT_OK,
-    )
-    if not projects:
-        raise HTTPException(status_code=404, detail="No projects found")
-    lines = []
-    for pr in projects:
-        path = pr.get("path", "")
-        owner = pr.get("owner") or git_get_project_owner(path) or ""
-        path_enc = quote_plus(path, safe="/")
-        owner_enc = quote_plus(owner, safe="/")
-        lines.append(f"{path_enc} {owner_enc}")
-    return PlainTextResponse("\n".join(lines), media_type="text/plain; charset=utf-8")
+	"""Plain text project index (path owner). Port of git_project_index."""
+	from urllib.parse import quote_plus
+
+	projects = git_get_projects_list(
+		filter_path=pf or "",
+		paranoid=STRICT_EXPORT,
+		export_ok=EXPORT_OK,
+	)
+	if not projects:
+		raise HTTPException(status_code=404, detail="No projects found")
+	lines = []
+	for pr in projects:
+		path = pr.get("path", "")
+		owner = pr.get("owner") or git_get_project_owner(path) or ""
+		path_enc = quote_plus(path, safe="/")
+		owner_enc = quote_plus(owner, safe="/")
+		lines.append(f"{path_enc} {owner_enc}")
+	return PlainTextResponse("\n".join(lines), media_type="text/plain; charset=utf-8")
 
 
 @app.get("/opml", response_class=PlainTextResponse)
 def git_opml():
-    """OPML feed list. Port of git_opml (stub)."""
-    projects = git_get_projects_list(export_ok=EXPORT_OK)
-    # Minimal OPML
-    lines = ['<?xml version="1.0"?><opml version="1.0"><head><title>Git</title></head><body>']
-    for pr in projects[:100]:
-        path = pr.get("path", "")
-        lines.append(f'<outline text="{esc_html(path)}" />')
-    lines.append("</body></opml>")
-    return PlainTextResponse("\n".join(lines), media_type="text/xml; charset=utf-8")
+	"""OPML feed list. Port of git_opml (stub)."""
+	projects = git_get_projects_list(export_ok=EXPORT_OK)
+	# Minimal OPML
+	lines = ['<?xml version="1.0"?><opml version="1.0"><head><title>Git</title></head><body>']
+	for pr in projects[:100]:
+		path = pr.get("path", "")
+		lines.append(f'<outline text="{esc_html(path)}" />')
+	lines.append("</body></opml>")
+	return PlainTextResponse("\n".join(lines), media_type="text/xml; charset=utf-8")
 
 
 @app.get("/board/create", response_class=RedirectResponse)
 def board_create_page(
-    request: Request,
-    project: Annotated[str | None, Query(alias="p")] = None,
+	request: Request,
+	project: Annotated[str | None, Query(alias="p")] = None,
 ) -> RedirectResponse:
-    """Create a board named 'Tasks' (refs/boards/Tasks) and redirect to the project summary."""
-    if DISTGIT_AUTH != "None":
-        raise HTTPException(status_code=401, detail="Authentication required")
-    p = project or request.query_params.get("project")
-    if not p:
-        raise HTTPException(status_code=400, detail="Project needed (use ?project= or ?p=)")
-    _validate_project(p)
-    try:
-        create_board_for_project(p, name="Tasks", description="")
-    except HTTPException as e:
-        if e.status_code == 409:
-            pass
-        else:
-            raise
-    p_url = quote(p, safe="/")
-    return RedirectResponse(url=f"/project/{p_url}", status_code=303)
+	"""Create a board named 'Tasks' (refs/boards/Tasks) and redirect to the project summary."""
+	if DISTGIT_AUTH != "None":
+		raise HTTPException(status_code=401, detail="Authentication required")
+	p = project or request.query_params.get("project")
+	if not p:
+		raise HTTPException(status_code=400, detail="Project needed (use ?project= or ?p=)")
+	_validate_project(p)
+	try:
+		create_board_for_project(p, name="Tasks", description="")
+	except HTTPException as e:
+		if e.status_code == 409:
+			pass
+		else:
+			raise
+	p_url = quote(p, safe="/")
+	return RedirectResponse(url=f"/project/{p_url}", status_code=303)
 
 
 # ---------- Add project ----------
@@ -291,120 +304,123 @@ def board_create_page(
 
 @app.get("/projectnamevalid", response_class=HTMLResponse)
 def addproject_namevalid(request: Request):
-    """Check if project name is valid."""
-    project_name = request.query_params.get("name")
-    if not project_name:
-        raise HTTPException(status_code=400, detail="Param 'name' required")
-    if not is_valid_pathname(project_name):
-        raise HTTPException(status_code=400, detail="Invalid project name (no path segments)")
-    if _project_in_list(project_name):
-        raise HTTPException(status_code=409, detail="Project already exists")
-    return HTMLResponse(status_code=200, content=f"Project name '{project_name}' is valid")
+	"""Check if project name is valid."""
+	project_name = request.query_params.get("name")
+	if not project_name:
+		raise HTTPException(status_code=400, detail="Param 'name' required")
+	if not is_valid_pathname(project_name):
+		raise HTTPException(status_code=400, detail="Invalid project name (no path segments)")
+	if _project_in_list(project_name):
+		raise HTTPException(status_code=409, detail="Project already exists")
+	return HTMLResponse(status_code=200, content=f"Project name '{project_name}' is valid")
+
 
 @app.get("/addproject", response_class=HTMLResponse)
 def addproject_page(request: Request):
-    """Add project form page."""
-    pre = PREAMBLE.render(
-        title=f"{config.SITE_NAME} - Add Project",
-        site_name=config.SITE_NAME,
-    )
-    tpl = env.get_template("addproject.html")
-    body = tpl.render(site_name=config.SITE_NAME)
-    return HTMLResponse(status_code=200, content=f"{pre}{body}{POSTAMBLE}")
+	"""Add project form page."""
+	pre = PREAMBLE.render(
+		title=f"{config.SITE_NAME} - Add Project",
+		site_name=config.SITE_NAME,
+	)
+	tpl = env.get_template("addproject.html")
+	body = tpl.render(site_name=config.SITE_NAME)
+	return HTMLResponse(status_code=200, content=f"{pre}{body}{POSTAMBLE}")
 
 
 @app.post("/addproject", response_class=HTMLResponse)
 async def addproject_submit(
-    request: Request,
-    project_name: Annotated[str, Form()] = "",
-    pull_from_remote: Annotated[str, Form()] = "",
-    perform_maintenance: Annotated[str, Form()] = "off",
-    create_task_board: Annotated[str, Form()] = "off",
-    repo_zip: UploadFile | None = File(default=None),
+	request: Request,
+	project_name: Annotated[str, Form()] = "",
+	pull_from_remote: Annotated[str, Form()] = "",
+	perform_maintenance: Annotated[str, Form()] = "off",
+	create_task_board: Annotated[str, Form()] = "off",
+	repo_zip: UploadFile | None = File(default=None),
 ):
-    """
-    Create a new project. Allowed only if project name does not exist,
-    and valid session in headers / session param (or auth is disabled).
-    """
-    if not _request_can_add_project(request):
-        raise HTTPException(status_code=401, detail="Authentication required to add projects")
-
-    project_name = (project_name or "").strip()
-    if not project_name:
-        raise HTTPException(status_code=400, detail="Project name is required")
-    if not is_valid_pathname(project_name):
-        raise HTTPException(status_code=400, detail="Invalid project name")
-    if _project_in_list(project_name):
-        raise HTTPException(status_code=409, detail="Project already exists")
-
-    remote_url = (pull_from_remote or "").strip()
-    do_maintenance = perform_maintenance.lower() in ("on", "1", "true", "yes")
-    do_task_board = create_task_board.lower() in ("on", "1", "true", "yes")
-
-    dest_path = os.path.join(PROJECTROOT, project_name)
-    os.makedirs(PROJECTROOT, exist_ok=True)
-
-    try:
-        if remote_url and remote_url != "":
-            pygit2.clone_repository(remote_url, dest_path, bare=True)
-        elif repo_zip and repo_zip.filename and repo_zip.filename.lower().endswith(".zip"):
-            with tempfile.TemporaryDirectory() as tmpdir:
-                zip_path = os.path.join(tmpdir, "repo.zip")
-                content = await repo_zip.read()
-                with open(zip_path, "wb") as f:
-                    f.write(content)
-                with zipfile.ZipFile(zip_path, "r") as zf:
-                    zf.extractall(tmpdir)
-                # Find .git: either at root or inside a single top-level dir
-                repo_root = None
-                for name in os.listdir(tmpdir):
-                    if name == "repo.zip":
-                        continue
-                    p = os.path.join(tmpdir, name)
-                    if os.path.isdir(p):
-                        if os.path.isdir(os.path.join(p, ".git")):
-                            repo_root = p
-                            break
-                        if name == ".git":
-                            repo_root = tmpdir
-                            break
-                if repo_root is None:
-                    if os.path.isdir(os.path.join(tmpdir, ".git")):
-                        repo_root = tmpdir
-                    else:
-                        # Single subdir that might be the repo
-                        subs = [x for x in os.listdir(tmpdir) if x != "repo.zip" and os.path.isdir(os.path.join(tmpdir, x))]
-                        if len(subs) == 1:
-                            repo_root = os.path.join(tmpdir, subs[0])
-                if repo_root is None or not pygit2.discover_repository(repo_root):
-                    raise HTTPException(
-                        status_code=400,
-                        detail="ZIP must contain a git repository (directory with .git)",
-                    )
-                pygit2.clone_repository(repo_root, dest_path, bare=True)
-        else:
-            pygit2.init_repository(dest_path, bare=True)
-
-        if do_maintenance:
-            try:
-                subprocess.run(
-                    [config.GIT, "-C", dest_path, "maintenance", "start"],
-                    capture_output=True,
-                    timeout=60,
-                )
-            except (subprocess.SubprocessError, FileNotFoundError):
-                pass  # best-effort
-
-        if do_task_board:
-            pass  # stub
-    except pygit2.GitError as e:
-        raise HTTPException(status_code=400, detail=f"Git error: {e}")
-    except HTTPException:
-        raise
-    except OSError as e:
-        raise HTTPException(status_code=500, detail=str(e))
-
-    return RedirectResponse(url=f"/project/{project_name}", status_code=303)
+	"""
+	Create a new project. Allowed only if project name does not exist,
+	and valid session in headers / session param (or auth is disabled).
+	"""
+	if not _request_can_add_project(request):
+		raise HTTPException(status_code=401, detail="Authentication required to add projects")
+
+	project_name = (project_name or "").strip()
+	if not project_name:
+		raise HTTPException(status_code=400, detail="Project name is required")
+	if not is_valid_pathname(project_name):
+		raise HTTPException(status_code=400, detail="Invalid project name")
+	if _project_in_list(project_name):
+		raise HTTPException(status_code=409, detail="Project already exists")
+
+	remote_url = (pull_from_remote or "").strip()
+	do_maintenance = perform_maintenance.lower() in ("on", "1", "true", "yes")
+	do_task_board = create_task_board.lower() in ("on", "1", "true", "yes")
+
+	dest_path = os.path.join(PROJECTROOT, project_name)
+	os.makedirs(PROJECTROOT, exist_ok=True)
+
+	try:
+		if remote_url and remote_url != "":
+			pygit2.clone_repository(remote_url, dest_path, bare=True)
+		elif repo_zip and repo_zip.filename and repo_zip.filename.lower().endswith(".zip"):
+			with tempfile.TemporaryDirectory() as tmpdir:
+				zip_path = os.path.join(tmpdir, "repo.zip")
+				content = await repo_zip.read()
+				with open(zip_path, "wb") as f:
+					f.write(content)
+				with zipfile.ZipFile(zip_path, "r") as zf:
+					zf.extractall(tmpdir)
+				# Find .git: either at root or inside a single top-level dir
+				repo_root = None
+				for name in os.listdir(tmpdir):
+					if name == "repo.zip":
+						continue
+					p = os.path.join(tmpdir, name)
+					if os.path.isdir(p):
+						if os.path.isdir(os.path.join(p, ".git")):
+							repo_root = p
+							break
+						if name == ".git":
+							repo_root = tmpdir
+							break
+				if repo_root is None:
+					if os.path.isdir(os.path.join(tmpdir, ".git")):
+						repo_root = tmpdir
+					else:
+						# Single subdir that might be the repo
+						subs = [
+							x for x in os.listdir(tmpdir) if x != "repo.zip" and os.path.isdir(os.path.join(tmpdir, x))
+						]
+						if len(subs) == 1:
+							repo_root = os.path.join(tmpdir, subs[0])
+				if repo_root is None or not pygit2.discover_repository(repo_root):
+					raise HTTPException(
+						status_code=400,
+						detail="ZIP must contain a git repository (directory with .git)",
+					)
+				pygit2.clone_repository(repo_root, dest_path, bare=True)
+		else:
+			pygit2.init_repository(dest_path, bare=True)
+
+		if do_maintenance:
+			try:
+				subprocess.run(
+					[config.GIT, "-C", dest_path, "maintenance", "start"],
+					capture_output=True,
+					timeout=60,
+				)
+			except (subprocess.SubprocessError, FileNotFoundError):
+				pass  # best-effort
+
+		if do_task_board:
+			pass  # stub
+	except pygit2.GitError as e:
+		raise HTTPException(status_code=400, detail=f"Git error: {e}")
+	except HTTPException:
+		raise
+	except OSError as e:
+		raise HTTPException(status_code=500, detail=str(e))
+
+	return RedirectResponse(url=f"/project/{project_name}", status_code=303)
 
 
 # ---------- Routes (project required) ----------
@@ -412,121 +428,123 @@ async def addproject_submit(
 
 @app.get("/project/{project:path}/board/", response_class=HTMLResponse)
 def project_board(
-    project: str,
-    board: Annotated[str, Query(alias="b")] = "Tasks",
+	project: str,
+	board: Annotated[str, Query(alias="b")] = "Tasks",
 ) -> HTMLResponse:
-    """Board view: columns (dropzones) and task cards."""
-    _validate_project(project)
-    board_name = board
-    project_url = f"/project/{quote(project, safe='/')}"
-    board_url = f"{project_url}/board/"
-    columns = get_board_tasks_grouped(project, board_name)
-    for col in columns:
-        for t in col["tasks"]:
-            t["task_url"] = f"{board_url}?task={quote(t['ref'], safe='')}"
-    pre = PREAMBLE.render(
-        title=f"{esc_html(project)} - Board",
-        site_name=config.SITE_NAME,
-    )
-    body = env.get_template("board.html").render(
-        project=project,
-        project_url=project_url,
-        board_name=board_name,
-        board_url=board_url,
-        columns=columns,
-    )
-    return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
+	"""Board view: columns (dropzones) and task cards."""
+	_validate_project(project)
+	board_name = board
+	project_url = f"/project/{quote(project, safe='/')}"
+	board_url = f"{project_url}/board/"
+	columns = get_board_tasks_grouped(project, board_name)
+	for col in columns:
+		for t in col["tasks"]:
+			t["task_url"] = f"{board_url}?task={quote(t['ref'], safe='')}"
+	pre = PREAMBLE.render(
+		title=f"{esc_html(project)} - Board",
+		site_name=config.SITE_NAME,
+	)
+	body = env.get_template("board.html").render(
+		project=project,
+		project_url=project_url,
+		board_name=board_name,
+		board_url=board_url,
+		columns=columns,
+	)
+	return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
 
 
 @app.get("/project/{project:path}", response_class=HTMLResponse)
 def dispatch(
-    request: Request,
-    project: str,
-    a: Annotated[str | None, Query(alias="a")] = None,
-    h: Annotated[str | None, Query(alias="h")] = None,
-    hb: Annotated[str | None, Query(alias="hb")] = None,
-    f: Annotated[str | None, Query(alias="f")] = None,
-    fp: Annotated[str | None, Query(alias="fp")] = None,
-    page: Annotated[str | None, Query(alias="page")] = None,
-    pagecount: Annotated[str | None, Query(alias="pagecount")] = None,
+	request: Request,
+	project: str,
+	a: Annotated[str | None, Query(alias="a")] = None,
+	h: Annotated[str | None, Query(alias="h")] = None,
+	hb: Annotated[str | None, Query(alias="hb")] = None,
+	f: Annotated[str | None, Query(alias="f")] = None,
+	fp: Annotated[str | None, Query(alias="fp")] = None,
+	page: Annotated[str | None, Query(alias="page")] = None,
+	pagecount: Annotated[str | None, Query(alias="pagecount")] = None,
 ):
-    """
-    Dispatch by path: /project/{project} -> summary; /project/{project}/action/... -> action.
-    Port of dispatch + run_request path handling.
-    """
-    if not project or project == "":
-        raise HTTPException(status_code=400, detail="Project needed")
-    _validate_project(project)
-    action = a
-    hash_param = h or hb
-    file_name = f
-    file_parent = fp
-    # If no action, infer: hash only -> object type; project only -> summary
-    if not action:
-        if hash_param and file_name:
-            obj_type = git_get_type(project, f"{hash_param}:{file_name}")
-            if not obj_type:
-                raise HTTPException(status_code=404, detail="File or directory does not exist")
-            action = "tree" if obj_type == "tree" else "blob_plain"
-        elif hash_param:
-            obj_type = git_get_type(project, hash_param)
-            if not obj_type:
-                raise HTTPException(status_code=404, detail="Object does not exist")
-            action = {"commit": "commit", "tree": "tree", "blob": "blob", "tag": "tag"}.get(obj_type, "object")
-        else:
-            action = "summary"
-    if not is_valid_action(action, ACTIONS):
-        raise HTTPException(status_code=400, detail="Unknown action")
-    if action in ("opml", "project_list", "project_index"):
-        raise HTTPException(status_code=400, detail="Project not needed for this action")
-    # Route to handler
-    if action == "summary":
-        return git_summary(project)
-    if action == "tree":
-        return git_tree(project, hash_param, file_name)
-    if action in ("blob", "blob_plain"):
-        return git_blob(project, hash_param, file_name, raw=(action == "blob_plain"))
-    if action == "blobdiff":
-        return git_blobdiff(project, h, hb, file_name, file_parent)
-    if action == "blobpatch":
-        return git_blobpatch(project, h, hb, file_name, file_parent)
-    if action == "log":
-        p, pc = parse_pagination(page, pagecount)
-        return git_log(project, hash_param, request, p, pc)
-    if action == "shortlog":
-        p, pc = parse_pagination(page, pagecount)
-        return git_shortlog(project, hash_param, request, p, pc)
-    if action == "history":
-        p, pc = parse_pagination(page, pagecount)
-        return git_history(project, hash_param, file_name, request, p, pc)
-    if action == "heads":
-        return git_heads(project)
-    if action == "tags":
-        p, pc = parse_pagination(page, pagecount)
-        return git_tags(project, request, p, pc)
-    if action == "tag":
-        return git_tag(project, hash_param)
-    if action == "commit":
-        return git_commit(project, hash_param)
-    if action == "patch":
-        return git_patch(project, h)
-    if action == "patches":
-        return git_patches(project, h, hb)
-    if action == "commitdiff":
-        return git_commitdiff(project, hash_param)
-    if action == "remotes":
-        return git_remotes(project)
-    if action == "object":
-        return git_object(project, hash_param)
-    # Stub others with minimal response
-    pre = PREAMBLE.render(title=f"{esc_html(action)} - {esc_html(project)}", site_name=config.SITE_NAME)
-    return HTMLResponse(
-        f"{pre}<p>Action: {esc_html(action)}</p><p>Project: {esc_html(project)}</p>{POSTAMBLE}"
-    )
-
-
+	"""
+	Dispatch by path: /project/{project} -> summary; /project/{project}/action/... -> action.
+	Port of dispatch + run_request path handling.
+	"""
+	if not project or project == "":
+		raise HTTPException(status_code=400, detail="Project needed")
+	_validate_project(project)
+	action = a
+	hash_param = h or hb
+	file_name = f
+	file_parent = fp
+	# If no action, infer: hash only -> object type; project only -> summary
+	if not action:
+		if hash_param and file_name:
+			obj_type = git_get_type(project, f"{hash_param}:{file_name}")
+			if not obj_type:
+				raise HTTPException(status_code=404, detail="File or directory does not exist")
+			action = "tree" if obj_type == "tree" else "blob_plain"
+		elif hash_param:
+			obj_type = git_get_type(project, hash_param)
+			if not obj_type:
+				raise HTTPException(status_code=404, detail="Object does not exist")
+			action = {
+				"commit": "commit",
+				"tree": "tree",
+				"blob": "blob",
+				"tag": "tag",
+			}.get(obj_type, "object")
+		else:
+			action = "summary"
+	if not is_valid_action(action, ACTIONS):
+		raise HTTPException(status_code=400, detail="Unknown action")
+	if action in ("opml", "project_list", "project_index"):
+		raise HTTPException(status_code=400, detail="Project not needed for this action")
+	# Route to handler
+	if action == "summary":
+		return git_summary(project)
+	if action == "tree":
+		return git_tree(project, hash_param, file_name)
+	if action in ("blob", "blob_plain"):
+		return git_blob(project, hash_param, file_name, raw=(action == "blob_plain"))
+	if action == "blobdiff":
+		return git_blobdiff(project, h, hb, file_name, file_parent)
+	if action == "blobpatch":
+		return git_blobpatch(project, h, hb, file_name, file_parent)
+	if action == "log":
+		p, pc = parse_pagination(page, pagecount)
+		return git_log(project, hash_param, request, p, pc)
+	if action == "shortlog":
+		p, pc = parse_pagination(page, pagecount)
+		return git_shortlog(project, hash_param, request, p, pc)
+	if action == "history":
+		p, pc = parse_pagination(page, pagecount)
+		return git_history(project, hash_param, file_name, request, p, pc)
+	if action == "heads":
+		return git_heads(project)
+	if action == "tags":
+		p, pc = parse_pagination(page, pagecount)
+		return git_tags(project, request, p, pc)
+	if action == "tag":
+		return git_tag(project, hash_param)
+	if action == "commit":
+		return git_commit(project, hash_param)
+	if action == "patch":
+		return git_patch(project, h)
+	if action == "patches":
+		return git_patches(project, h, hb)
+	if action == "commitdiff":
+		return git_commitdiff(project, hash_param)
+	if action == "remotes":
+		return git_remotes(project)
+	if action == "object":
+		return git_object(project, hash_param)
+	# Stub others with minimal response
+	pre = PREAMBLE.render(title=f"{esc_html(action)} - {esc_html(project)}", site_name=config.SITE_NAME)
+	return HTMLResponse(f"{pre}<p>Action: {esc_html(action)}</p><p>Project: {esc_html(project)}</p>{POSTAMBLE}")
 
 
 if __name__ == "__main__":
-    import uvicorn
-    uvicorn.run(app, host="0.0.0.0", port=8000)
+	import uvicorn
+
+	uvicorn.run(app, host="0.0.0.0", port=8000)
diff --git a/pygitweb/projects.py b/pygitweb/projects.py
index 3a4ce7d..5fe0527 100644
--- a/pygitweb/projects.py
+++ b/pygitweb/projects.py
@@ -3,203 +3,210 @@ Project list: get_projects_list, search_projects_list, project_in_list,
 get_project_owner, get_project_list_from_file, get_last_activity.
 Ported from gitweb/gitweb.perl.
 """
+
 from __future__ import annotations
 
 import os
 import re
-from pathlib import Path
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
 
 import pygit2
 
-from pygitweb.config import PROJECTROOT, PROJECTS_LIST, PROJECT_MAXDEPTH, LIST_ALL
+from pygitweb.config import LIST_ALL, PROJECT_MAXDEPTH, PROJECTROOT, PROJECTS_LIST
 from pygitweb.validation import check_export_ok
 
 
 def _export_ok_path(git_dir: str, export_ok: str) -> bool:
-    return not export_ok or os.path.isfile(os.path.join(git_dir, export_ok))
+	return not export_ok or os.path.isfile(os.path.join(git_dir, export_ok))
 
 
 def project_in_list(
-    project: str,
-    get_projects_list_fn: Callable[[], list[dict[str, Any]]],
+	project: str,
+	get_projects_list_fn: Callable[[], list[dict[str, Any]]],
 ) -> bool:
-    """True if project appears in project list. Port of project_in_list."""
-    lst = get_projects_list_fn()
-    return any(p.get("path") == project for p in lst)
+	"""True if project appears in project list. Port of project_in_list."""
+	lst = get_projects_list_fn()
+	return any(p.get("path") == project for p in lst)
 
 
 def _find_projects_in_dir(
-    root: str,
-    prefix_len: int,
-    prefix_depth: int,
-    maxdepth: int,
-    export_ok: str,
-    export_auth_hook: Callable[[str], bool] | None,
-    skip_export_check: bool = False,
+	root: str,
+	prefix_len: int,
+	prefix_depth: int,
+	maxdepth: int,
+	export_ok: str,
+	export_auth_hook: Callable[[str], bool] | None,
+	skip_export_check: bool = False,
 ) -> list[dict[str, Any]]:
-    result = []
-    for dirpath, dirnames, _ in os.walk(root, topdown=True):
-        rel = os.path.relpath(dirpath, root)
-        if rel == ".":
-            depth = 0
-        else:
-            depth = rel.count(os.sep) + 1
-        if depth >= maxdepth:
-            dirnames.clear()
-            continue
-        for d in list(dirnames):
-            path = os.path.join(dirpath, d)
-            if not os.path.isdir(path):
-                continue
-            try:
-                if not os.access(path, os.X_OK):
-                    continue
-            except OSError:
-                continue
-            repo = pygit2.discover_repository(path)
-            if not repo:
-                continue
-            project_path = os.path.relpath(path, PROJECTROOT)
-            project_path = project_path.replace("\\", "/")
-            git_dir = os.path.join(PROJECTROOT, project_path)
-            if not skip_export_check and not check_export_ok(git_dir, export_ok, export_auth_hook):
-                continue
-            result.append({"path": project_path})
-            dirnames.remove(d)
-    return result
+	result = []
+	for dirpath, dirnames, _ in os.walk(root, topdown=True):
+		rel = os.path.relpath(dirpath, root)
+		if rel == ".":
+			depth = 0
+		else:
+			depth = rel.count(os.sep) + 1
+		if depth >= maxdepth:
+			dirnames.clear()
+			continue
+		for d in list(dirnames):
+			path = os.path.join(dirpath, d)
+			if not os.path.isdir(path):
+				continue
+			try:
+				if not os.access(path, os.X_OK):
+					continue
+			except OSError:
+				continue
+			repo = pygit2.discover_repository(path)
+			if not repo:
+				continue
+			project_path = os.path.relpath(path, PROJECTROOT)
+			project_path = project_path.replace("\\", "/")
+			git_dir = os.path.join(PROJECTROOT, project_path)
+			if not skip_export_check and not check_export_ok(git_dir, export_ok, export_auth_hook):
+				continue
+			result.append({"path": project_path})
+			dirnames.remove(d)
+	return result
 
 
 def git_get_projects_list(
-    filter_path: str = "",
-    paranoid: bool = False,
-    projectroot: str = PROJECTROOT,
-    projects_list: str = PROJECTS_LIST,
-    project_maxdepth: int = PROJECT_MAXDEPTH,
-    export_ok: str = "",
-    export_auth_hook: Callable[[str], bool] | None = None,
+	filter_path: str = "",
+	paranoid: bool = False,
+	projectroot: str = PROJECTROOT,
+	projects_list: str = PROJECTS_LIST,
+	project_maxdepth: int = PROJECT_MAXDEPTH,
+	export_ok: str = "",
+	export_auth_hook: Callable[[str], bool] | None = None,
 ) -> list[dict[str, Any]]:
-    """List projects from directory scan or file. Port of git_get_projects_list."""
-    if os.path.isdir(projects_list):
-        root = projects_list.rstrip("/")
-        prefix_len = len(root) + 1
-        prefix_depth = root.count(os.sep)
-        if filter_path and not paranoid:
-            root = os.path.join(root, filter_path).rstrip("/")
-        result = _find_projects_in_dir(
-            root, prefix_len, prefix_depth, project_maxdepth,
-            export_ok, export_auth_hook,
-            skip_export_check=LIST_ALL,
-        )
-        if filter_path and paranoid:
-            result = [p for p in result if p["path"].startswith(filter_path + "/")]
-        return result
-    if os.path.isfile(projects_list):
-        from urllib.parse import unquote
-        result = []
-        with open(projects_list) as f:
-            for line in f:
-                line = line.strip()
-                if not line:
-                    continue
-                parts = line.split(None, 1)
-                path = unquote(parts[0]) if parts else ""
-                owner = unquote(parts[1]) if len(parts) > 1 else None
-                if not path:
-                    continue
-                if filter_path and not path.startswith(filter_path + "/"):
-                    continue
-                git_dir = os.path.join(projectroot, path)
-                if not LIST_ALL and not check_export_ok(git_dir, export_ok, export_auth_hook):
-                    continue
-                pr = {"path": path}
-                if owner:
-                    pr["owner"] = owner
-                result.append(pr)
-        return result
-    return []
+	"""List projects from directory scan or file. Port of git_get_projects_list."""
+	if os.path.isdir(projects_list):
+		root = projects_list.rstrip("/")
+		prefix_len = len(root) + 1
+		prefix_depth = root.count(os.sep)
+		if filter_path and not paranoid:
+			root = os.path.join(root, filter_path).rstrip("/")
+		result = _find_projects_in_dir(
+			root,
+			prefix_len,
+			prefix_depth,
+			project_maxdepth,
+			export_ok,
+			export_auth_hook,
+			skip_export_check=LIST_ALL,
+		)
+		if filter_path and paranoid:
+			result = [p for p in result if p["path"].startswith(filter_path + "/")]
+		return result
+	if os.path.isfile(projects_list):
+		from urllib.parse import unquote
+
+		result = []
+		with open(projects_list) as f:
+			for line in f:
+				line = line.strip()
+				if not line:
+					continue
+				parts = line.split(None, 1)
+				path = unquote(parts[0]) if parts else ""
+				owner = unquote(parts[1]) if len(parts) > 1 else None
+				if not path:
+					continue
+				if filter_path and not path.startswith(filter_path + "/"):
+					continue
+				git_dir = os.path.join(projectroot, path)
+				if not LIST_ALL and not check_export_ok(git_dir, export_ok, export_auth_hook):
+					continue
+				pr = {"path": path}
+				if owner:
+					pr["owner"] = owner
+				result.append(pr)
+		return result
+	return []
 
 
 _gitweb_project_owner: dict[str, str] | None = None
 
 
 def git_get_project_list_from_file(
-    projects_list: str = PROJECTS_LIST,
-    projectroot: str = PROJECTROOT,
+	projects_list: str = PROJECTS_LIST,
+	projectroot: str = PROJECTROOT,
 ) -> dict[str, str]:
-    """Load project -> owner from file. Port of git_get_project_list_from_file."""
-    global _gitweb_project_owner
-    if _gitweb_project_owner is not None:
-        return _gitweb_project_owner
-    _gitweb_project_owner = {}
-    if os.path.isfile(projects_list):
-        from urllib.parse import unquote
-        with open(projects_list) as f:
-            for line in f:
-                line = line.strip()
-                if not line:
-                    continue
-                parts = line.split(None, 1)
-                path = unquote(parts[0]) if parts else ""
-                owner = unquote(parts[1]) if len(parts) > 1 else ""
-                if path:
-                    _gitweb_project_owner[path] = owner
-    return _gitweb_project_owner
+	"""Load project -> owner from file. Port of git_get_project_list_from_file."""
+	global _gitweb_project_owner
+	if _gitweb_project_owner is not None:
+		return _gitweb_project_owner
+	_gitweb_project_owner = {}
+	if os.path.isfile(projects_list):
+		from urllib.parse import unquote
+
+		with open(projects_list) as f:
+			for line in f:
+				line = line.strip()
+				if not line:
+					continue
+				parts = line.split(None, 1)
+				path = unquote(parts[0]) if parts else ""
+				owner = unquote(parts[1]) if len(parts) > 1 else ""
+				if path:
+					_gitweb_project_owner[path] = owner
+	return _gitweb_project_owner
 
 
 def git_get_project_owner(
-    project: str,
-    projectroot: str = PROJECTROOT,
-    get_project_config: Callable[[str, str], Any] | None = None,
+	project: str,
+	projectroot: str = PROJECTROOT,
+	get_project_config: Callable[[str, str], Any] | None = None,
 ) -> str | None:
-    """Owner from list file or config or file ownership. Port of git_get_project_owner."""
-    if not project:
-        return None
-    owners = git_get_project_list_from_file(PROJECTS_LIST, projectroot)
-    if project in owners and owners[project]:
-        return owners[project]
-    if get_project_config:
-        val = get_project_config(project, "owner")
-        if val:
-            return val[0] if isinstance(val, list) else val
-    return None
+	"""Owner from list file or config or file ownership. Port of git_get_project_owner."""
+	if not project:
+		return None
+	owners = git_get_project_list_from_file(PROJECTS_LIST, projectroot)
+	if project in owners and owners[project]:
+		return owners[project]
+	if get_project_config:
+		val = get_project_config(project, "owner")
+		if val:
+			return val[0] if isinstance(val, list) else val
+	return None
 
 
 def git_get_last_activity(project: str, projectroot: str = PROJECTROOT) -> int | None:
-    """Last commit timestamp for project. Port of git_get_last_activity."""
-    from pygitweb.git_helpers import git_get_head_hash, parse_commit
+	"""Last commit timestamp for project. Port of git_get_last_activity."""
+	from pygitweb.git_helpers import git_get_head_hash, parse_commit
 
-    oid = git_get_head_hash(project)
-    if not oid:
-        return None
-    co = parse_commit(project, oid)
-    return co.get("committer_epoch")
+	oid = git_get_head_hash(project)
+	if not oid:
+		return None
+	co = parse_commit(project, oid)
+	return co.get("committer_epoch")
 
 
 def search_projects_list(
-    projlist: list[dict[str, Any]],
-    tagfilter: str | None = None,
-    search_regexp: str | None = None,
-    fill_project_list_info: Callable[..., None] | None = None,
+	projlist: list[dict[str, Any]],
+	tagfilter: str | None = None,
+	search_regexp: str | None = None,
+	fill_project_list_info: Callable[..., None] | None = None,
 ) -> list[dict[str, Any]]:
-    """Filter by tag or search regex. Port of search_projects_list."""
-    if not tagfilter and not search_regexp:
-        return projlist
-    if fill_project_list_info:
-        fill_project_list_info(projlist, tagfilter=tagfilter, search_re=search_regexp)
-    result = []
-    for pr in projlist:
-        if tagfilter:
-            ctags = pr.get("ctags") or {}
-            if not any(k.lower() == tagfilter.lower() for k in ctags):
-                continue
-        if search_regexp:
-            try:
-                rex = re.compile(search_regexp)
-            except re.error:
-                continue
-            descr = (pr.get("descr_long") or "") + (pr.get("path") or "")
-            if not rex.search(descr):
-                continue
-        result.append(pr)
-    return result
+	"""Filter by tag or search regex. Port of search_projects_list."""
+	if not tagfilter and not search_regexp:
+		return projlist
+	if fill_project_list_info:
+		fill_project_list_info(projlist, tagfilter=tagfilter, search_re=search_regexp)
+	result = []
+	for pr in projlist:
+		if tagfilter:
+			ctags = pr.get("ctags") or {}
+			if not any(k.lower() == tagfilter.lower() for k in ctags):
+				continue
+		if search_regexp:
+			try:
+				rex = re.compile(search_regexp)
+			except re.error:
+				continue
+			descr = (pr.get("descr_long") or "") + (pr.get("path") or "")
+			if not rex.search(descr):
+				continue
+		result.append(pr)
+	return result
diff --git a/pygitweb/settings.py b/pygitweb/settings.py
index 72f8ebe..497c537 100644
--- a/pygitweb/settings.py
+++ b/pygitweb/settings.py
@@ -2,26 +2,21 @@
 Settings routes: PyGitWeb global options, PyGit2 library settings, and per-project git config.
 Forms are generated from type() and docstrings; project config shows only LOCAL/WORKTREE entries.
 """
+
 from __future__ import annotations
 
-import os
 from typing import Any
 from urllib.parse import quote
 
 import pygit2
-from fastapi import APIRouter, Form, HTTPException, Request
+from fastapi import APIRouter, HTTPException, Request
 from fastapi.responses import HTMLResponse, RedirectResponse
 
 from pygitweb import config
 from pygitweb.config import (
-    EXPORT_OK,
-    GIT,
-    LIST_ALL,
-    MAXLOAD,
-    PROJECTROOT,
-    PROJECTS_LIST,
-    SITE_NAME,
-    STRICT_EXPORT,
+	EXPORT_OK,
+	PROJECTROOT,
+	STRICT_EXPORT,
 )
 from pygitweb.formatting import esc_html
 from pygitweb.git_helpers import open_repo
@@ -30,9 +25,11 @@ from pygitweb.validation import is_valid_project
 
 
 def _quote_path(path: str) -> str:
-    """Quote path segment for URL (e.g. project name with slashes)."""
-    return quote(path, safe="/")
-from pygitweb.templates_env import PREAMBLE, POSTAMBLE, env
+	"""Quote path segment for URL (e.g. project name with slashes)."""
+	return quote(path, safe="/")
+
+
+from pygitweb.templates_env import POSTAMBLE, PREAMBLE, env
 
 # libgit2 config levels: only local (5) and worktree (6) for per-repo editing
 GIT_CONFIG_LEVEL_LOCAL = 5
@@ -44,294 +41,377 @@ router = APIRouter(prefix="/settings", tags=["settings"])
 # ---------- PyGitWeb settings schema ----------
 # Name, type, docstring for form generation. Must match config module attribute names.
 PYGITWEB_SETTINGS = [
-    ("PROJECTROOT", str, "Root filesystem path under which git repositories live."),
-    ("PROJECTS_LIST", str, "Path used for listing projects (often same as PROJECTROOT)."),
-    ("SITE_NAME", str, "Site name shown in the web interface."),
-    ("EXPORT_OK", str, "If set, only repos with this file (or matching path) are listed."),
-    ("LIST_ALL", bool, "When true, list all directories under project root without export_ok checks."),
-    ("STRICT_EXPORT", bool, "When true, require export_ok (or path match) to list projects."),
-    ("GIT", str, "Path to the git executable (e.g. for maintenance)."),
-    ("MAXLOAD", (float, type(None)), "Max load average; 503 when exceeded. None to disable."),
+	("PROJECTROOT", str, "Root filesystem path under which git repositories live."),
+	(
+		"PROJECTS_LIST",
+		str,
+		"Path used for listing projects (often same as PROJECTROOT).",
+	),
+	("SITE_NAME", str, "Site name shown in the web interface."),
+	(
+		"EXPORT_OK",
+		str,
+		"If set, only repos with this file (or matching path) are listed.",
+	),
+	(
+		"LIST_ALL",
+		bool,
+		"When true, list all directories under project root without export_ok checks.",
+	),
+	(
+		"STRICT_EXPORT",
+		bool,
+		"When true, require export_ok (or path match) to list projects.",
+	),
+	("GIT", str, "Path to the git executable (e.g. for maintenance)."),
+	(
+		"MAXLOAD",
+		(float, type(None)),
+		"Max load average; 503 when exceeded. None to disable.",
+	),
 ]
 
 
 def _get_pygitweb_values() -> list[tuple[str, Any, type, str]]:
-    """Return (name, current_value, type, docstring) for each PyGitWeb setting."""
-    out = []
-    for name, expected_type, docstring in PYGITWEB_SETTINGS:
-        if not hasattr(config, name):
-            continue
-        val = getattr(config, name)
-        out.append((name, val, expected_type if isinstance(expected_type, type) else type(val), docstring))
-    return out
+	"""Return (name, current_value, type, docstring) for each PyGitWeb setting."""
+	out = []
+	for name, expected_type, docstring in PYGITWEB_SETTINGS:
+		if not hasattr(config, name):
+			continue
+		val = getattr(config, name)
+		out.append(
+			(
+				name,
+				val,
+				expected_type if isinstance(expected_type, type) else type(val),
+				docstring,
+			)
+		)
+	return out
 
 
 def _render_form_field(name: str, value: Any, value_type: type, docstring: str, name_prefix: str = "") -> str:
-    """Generate HTML for a single form field based on type."""
-    field_name = f"{name_prefix}{name}" if name_prefix else name
-    safe_name = field_name.replace(".", "_")
-    hint = f'<small class="form-hint text-muted">{esc_html(docstring)}</small>'
-    if value_type is bool:
-        checked = " checked" if value else ""
-        return f'<div class="mb-3"><div class="form-check"><input type="checkbox" class="form-check-input" id="{safe_name}" name="{field_name}" value="on"{checked}><label class="form-check-label" for="{safe_name}">{name}</label></div>{hint}</div>'
-    if value_type in (int, float):
-        str_val = "" if value is None else str(value)
-        return f'<div class="mb-3"><label class="form-label" for="{safe_name}">{name}</label><input type="number" step="any" class="form-control" id="{safe_name}" name="{field_name}" value="{esc_html(str_val)}">{hint}</div>'
-    # str or None
-    str_val = "" if value is None else str(value)
-    return f'<div class="mb-3"><label class="form-label" for="{safe_name}">{name}</label><input type="text" class="form-control" id="{safe_name}" name="{field_name}" value="{esc_html(str_val)}">{hint}</div>'
+	"""Generate HTML for a single form field based on type."""
+	field_name = f"{name_prefix}{name}" if name_prefix else name
+	safe_name = field_name.replace(".", "_")
+	hint = f'<small class="form-hint text-muted">{esc_html(docstring)}</small>'
+	if value_type is bool:
+		checked = " checked" if value else ""
+		return f'<div class="mb-3"><div class="form-check"><input type="checkbox" class="form-check-input" id="{safe_name}" name="{field_name}" value="on"{checked}><label class="form-check-label" for="{safe_name}">{name}</label></div>{hint}</div>'
+	if value_type in (int, float):
+		str_val = "" if value is None else str(value)
+		return f'<div class="mb-3"><label class="form-label" for="{safe_name}">{name}</label><input type="number" step="any" class="form-control" id="{safe_name}" name="{field_name}" value="{esc_html(str_val)}">{hint}</div>'
+	# str or None
+	str_val = "" if value is None else str(value)
+	return f'<div class="mb-3"><label class="form-label" for="{safe_name}">{name}</label><input type="text" class="form-control" id="{safe_name}" name="{field_name}" value="{esc_html(str_val)}">{hint}</div>'
 
 
 # ---------- PyGit2 Settings schema ----------
 # List of (attr_name, docstring, value_kind: 'bool'|'int'|'str'|'readonly').
 # Based on https://www.pygit2.org/settings.html
 PYGIT2_SETTINGS = [
-    ("cache_max_size", "Maximum total data size (bytes) cached in memory across all repositories. Default 256MB.", "int"),
-    ("cache_object_limit", "Max size per object type for caching (use cache_object_limit_commit etc. for fine-grained).", "skip"),  # multi-param
-    ("cached_memory", "Current bytes in cache and maximum allowed (read-only).", "readonly"),
-    ("disable_pack_keep_file_checks", "Skip .keep file checks when accessing packfiles; can help on remote filesystems.", "bool"),
-    ("enable_caching", "Enable or disable caching completely.", "bool"),
-    ("enable_fsync_gitdir", "Enable or disable fsync for git directory operations.", "bool"),
-    ("enable_http_expect_continue", "Enable or disable HTTP Expect/Continue for large pushes.", "bool"),
-    ("enable_ofs_delta", "Enable or disable offset delta encoding.", "bool"),
-    ("enable_strict_hash_verification", "Enable or disable strict hash verification.", "bool"),
-    ("enable_strict_object_creation", "Enable or disable strict object creation validation.", "bool"),
-    ("enable_strict_symbolic_ref_creation", "Enable or disable strict symbolic reference creation validation.", "bool"),
-    ("enable_unsaved_index_safety", "Enable or disable unsaved index safety checks.", "bool"),
-    ("extensions", "List of enabled extensions (read-only).", "readonly"),
-    ("homedir", "Home directory for config lookup.", "str"),
-    ("mwindow_file_limit", "Maximum number of files to be mapped at any time.", "int"),
-    ("mwindow_mapped_limit", "Maximum memory that will be mapped in total by the library.", "int"),
-    ("mwindow_size", "Maximum mmap window size.", "int"),
-    ("owner_validation", "Validate that repository directories are owned by the current user.", "bool"),
-    ("pack_max_objects", "Maximum number of objects in a pack.", "int"),
-    ("search_path", "Configuration file search path (read-only).", "readonly"),
-    ("server_connect_timeout", "Server connection timeout in milliseconds.", "int"),
-    ("server_timeout", "Server timeout in milliseconds.", "int"),
-    ("ssl_cert_dir", "TLS certificates lookup directory path.", "str"),
-    ("ssl_cert_file", "TLS certificate file path.", "str"),
-    ("template_path", "Default template path for new repositories.", "str"),
-    ("user_agent", "User agent string for network operations.", "str"),
-    ("user_agent_product", "User agent product name.", "str"),
-    ("windows_sharemode", "Windows share mode for opening files.", "int"),
+	(
+		"cache_max_size",
+		"Maximum total data size (bytes) cached in memory across all repositories. Default 256MB.",
+		"int",
+	),
+	(
+		"cache_object_limit",
+		"Max size per object type for caching (use cache_object_limit_commit etc. for fine-grained).",
+		"skip",
+	),  # multi-param
+	(
+		"cached_memory",
+		"Current bytes in cache and maximum allowed (read-only).",
+		"readonly",
+	),
+	(
+		"disable_pack_keep_file_checks",
+		"Skip .keep file checks when accessing packfiles; can help on remote filesystems.",
+		"bool",
+	),
+	("enable_caching", "Enable or disable caching completely.", "bool"),
+	(
+		"enable_fsync_gitdir",
+		"Enable or disable fsync for git directory operations.",
+		"bool",
+	),
+	(
+		"enable_http_expect_continue",
+		"Enable or disable HTTP Expect/Continue for large pushes.",
+		"bool",
+	),
+	("enable_ofs_delta", "Enable or disable offset delta encoding.", "bool"),
+	(
+		"enable_strict_hash_verification",
+		"Enable or disable strict hash verification.",
+		"bool",
+	),
+	(
+		"enable_strict_object_creation",
+		"Enable or disable strict object creation validation.",
+		"bool",
+	),
+	(
+		"enable_strict_symbolic_ref_creation",
+		"Enable or disable strict symbolic reference creation validation.",
+		"bool",
+	),
+	(
+		"enable_unsaved_index_safety",
+		"Enable or disable unsaved index safety checks.",
+		"bool",
+	),
+	("extensions", "List of enabled extensions (read-only).", "readonly"),
+	("homedir", "Home directory for config lookup.", "str"),
+	("mwindow_file_limit", "Maximum number of files to be mapped at any time.", "int"),
+	(
+		"mwindow_mapped_limit",
+		"Maximum memory that will be mapped in total by the library.",
+		"int",
+	),
+	("mwindow_size", "Maximum mmap window size.", "int"),
+	(
+		"owner_validation",
+		"Validate that repository directories are owned by the current user.",
+		"bool",
+	),
+	("pack_max_objects", "Maximum number of objects in a pack.", "int"),
+	("search_path", "Configuration file search path (read-only).", "readonly"),
+	("server_connect_timeout", "Server connection timeout in milliseconds.", "int"),
+	("server_timeout", "Server timeout in milliseconds.", "int"),
+	("ssl_cert_dir", "TLS certificates lookup directory path.", "str"),
+	("ssl_cert_file", "TLS certificate file path.", "str"),
+	("template_path", "Default template path for new repositories.", "str"),
+	("user_agent", "User agent string for network operations.", "str"),
+	("user_agent_product", "User agent product name.", "str"),
+	("windows_sharemode", "Windows share mode for opening files.", "int"),
 ]
 
 
 def _get_pygit2_values() -> list[tuple[str, Any, str, str]]:
-    """Return (name, value, kind, docstring) for each PyGit2 setting we can show."""
-    st = pygit2.Settings
-    instance = pygit2.Settings()  # need instance to read property values
-    out = []
-    for attr, docstring, kind in PYGIT2_SETTINGS:
-        if kind == "skip":
-            continue
-        if not hasattr(st, attr):
-            continue
-        try:
-            prop = getattr(st, attr)
-            if callable(prop) and not isinstance(prop, property):
-                continue
-            val = getattr(instance, attr)
-            if isinstance(val, (list, tuple)):
-                val = ", ".join(str(x) for x in val) if val else ""
-            elif val is None:
-                val = ""
-            out.append((attr, val, kind, docstring))
-        except (TypeError, AttributeError):
-            continue
-    return out
+	"""Return (name, value, kind, docstring) for each PyGit2 setting we can show."""
+	st = pygit2.Settings
+	instance = pygit2.Settings()  # need instance to read property values
+	out = []
+	for attr, docstring, kind in PYGIT2_SETTINGS:
+		if kind == "skip":
+			continue
+		if not hasattr(st, attr):
+			continue
+		try:
+			prop = getattr(st, attr)
+			if callable(prop) and not isinstance(prop, property):
+				continue
+			val = getattr(instance, attr)
+			if isinstance(val, (list, tuple)):
+				val = ", ".join(str(x) for x in val) if val else ""
+			elif val is None:
+				val = ""
+			out.append((attr, val, kind, docstring))
+		except (TypeError, AttributeError):
+			continue
+	return out
 
 
 def _render_pygit2_field(name: str, value: Any, kind: str, docstring: str, name_prefix: str = "pygit2_") -> str:
-    """Generate HTML for a PyGit2 form field."""
-    field_name = f"{name_prefix}{name}"
-    safe_name = field_name.replace(".", "_")
-    hint = f'<small class="form-hint text-muted">{esc_html(docstring)}</small>'
-    if kind == "readonly":
-        str_val = str(value) if value != "" else "(not set)"
-        return f'<div class="mb-3"><label class="form-label" for="{safe_name}">{name}</label><input type="text" class="form-control bg-secondary" id="{safe_name}" name="{field_name}" value="{esc_html(str_val)}" readonly>{hint}</div>'
-    if kind == "bool":
-        checked = " checked" if (value is True or (isinstance(value, str) and value.lower() in ("true", "1", "on", "yes"))) else ""
-        return f'<div class="mb-3"><div class="form-check"><input type="checkbox" class="form-check-input" id="{safe_name}" name="{field_name}" value="on"{checked}><label class="form-check-label" for="{safe_name}">{name}</label></div>{hint}</div>'
-    if kind == "int":
-        str_val = str(value) if value != "" and value is not None else ""
-        return f'<div class="mb-3"><label class="form-label" for="{safe_name}">{name}</label><input type="number" class="form-control" id="{safe_name}" name="{field_name}" value="{esc_html(str_val)}">{hint}</div>'
-    str_val = str(value) if value is not None else ""
-    return f'<div class="mb-3"><label class="form-label" for="{safe_name}">{name}</label><input type="text" class="form-control" id="{safe_name}" name="{field_name}" value="{esc_html(str_val)}">{hint}</div>'
+	"""Generate HTML for a PyGit2 form field."""
+	field_name = f"{name_prefix}{name}"
+	safe_name = field_name.replace(".", "_")
+	hint = f'<small class="form-hint text-muted">{esc_html(docstring)}</small>'
+	if kind == "readonly":
+		str_val = str(value) if value != "" else "(not set)"
+		return f'<div class="mb-3"><label class="form-label" for="{safe_name}">{name}</label><input type="text" class="form-control bg-secondary" id="{safe_name}" name="{field_name}" value="{esc_html(str_val)}" readonly>{hint}</div>'
+	if kind == "bool":
+		checked = (
+			" checked"
+			if (value is True or (isinstance(value, str) and value.lower() in ("true", "1", "on", "yes")))
+			else ""
+		)
+		return f'<div class="mb-3"><div class="form-check"><input type="checkbox" class="form-check-input" id="{safe_name}" name="{field_name}" value="on"{checked}><label class="form-check-label" for="{safe_name}">{name}</label></div>{hint}</div>'
+	if kind == "int":
+		str_val = str(value) if value != "" and value is not None else ""
+		return f'<div class="mb-3"><label class="form-label" for="{safe_name}">{name}</label><input type="number" class="form-control" id="{safe_name}" name="{field_name}" value="{esc_html(str_val)}">{hint}</div>'
+	str_val = str(value) if value is not None else ""
+	return f'<div class="mb-3"><label class="form-label" for="{safe_name}">{name}</label><input type="text" class="form-control" id="{safe_name}" name="{field_name}" value="{esc_html(str_val)}">{hint}</div>'
 
 
 # ---------- Project config (local/worktree only) ----------
 def _get_project_config_entries(project: str) -> list[tuple[str, str]]:
-    """
-    Return list of (name, value) for repo config, only from LOCAL (5) or WORKTREE (6).
-    For each name we keep the last value (highest priority when iterating).
-    """
-    repo = open_repo(project)
-    cfg = repo.config
-    # Iterate; for level in (5, 6) keep last value per name
-    by_name: dict[str, str] = {}
-    for entry in cfg:
-        if entry.level not in (GIT_CONFIG_LEVEL_LOCAL, GIT_CONFIG_LEVEL_WORKTREE):
-            continue
-        by_name[entry.name] = entry.value
-    return [(k, v) for k, v in sorted(by_name.items())]
+	"""
+	Return list of (name, value) for repo config, only from LOCAL (5) or WORKTREE (6).
+	For each name we keep the last value (highest priority when iterating).
+	"""
+	repo = open_repo(project)
+	cfg = repo.config
+	# Iterate; for level in (5, 6) keep last value per name
+	by_name: dict[str, str] = {}
+	for entry in cfg:
+		if entry.level not in (GIT_CONFIG_LEVEL_LOCAL, GIT_CONFIG_LEVEL_WORKTREE):
+			continue
+		by_name[entry.name] = entry.value
+	return [(k, v) for k, v in sorted(by_name.items())]
 
 
 def _project_config_table_row(key: str, value: str, index: int) -> list[str]:
-    """One table row as [key_cell_html, value_cell_html] for project config form."""
-    key_name = f"config_key_{index}"
-    val_name = f"config_value_{index}"
-    key_cell = f'<input type="text" class="form-control form-control-sm" id="{key_name}" name="{key_name}" value="{esc_html(key)}" placeholder="e.g. user.name">'
-    val_cell = f'<input type="text" class="form-control form-control-sm" id="{val_name}" name="{val_name}" value="{esc_html(value)}" placeholder="value">'
-    return [key_cell, val_cell]
+	"""One table row as [key_cell_html, value_cell_html] for project config form."""
+	key_name = f"config_key_{index}"
+	val_name = f"config_value_{index}"
+	key_cell = f'<input type="text" class="form-control form-control-sm" id="{key_name}" name="{key_name}" value="{esc_html(key)}" placeholder="e.g. user.name">'
+	val_cell = f'<input type="text" class="form-control form-control-sm" id="{val_name}" name="{val_name}" value="{esc_html(value)}" placeholder="value">'
+	return [key_cell, val_cell]
 
 
 # ---------- Routes: PyGitWeb ----------
 @router.get("/pygitweb", response_class=HTMLResponse)
 def settings_pygitweb_page(request: Request):
-    """PyGitWeb global settings form."""
-    fields_html = []
-    for name, val, value_type, doc in _get_pygitweb_values():
-        fields_html.append(_render_form_field(name, val, value_type, doc))
-    form_body = "\n".join(fields_html)
-    pre = PREAMBLE.render(title=f"{config.SITE_NAME} - PyGitWeb settings", site_name=config.SITE_NAME)
-    body = f'<h1 class="page-title">PyGitWeb settings</h1><div class="card"><div class="card-body"><form action="/settings/pygitweb/submit" method="post" class="needs-validation" novalidate>{form_body}<div class="form-footer"><button type="submit" class="btn btn-primary">Save</button><a href="/" class="btn btn-ghost-secondary">Cancel</a></div></form></div></div>'
-    return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
+	"""PyGitWeb global settings form."""
+	fields_html = []
+	for name, val, value_type, doc in _get_pygitweb_values():
+		fields_html.append(_render_form_field(name, val, value_type, doc))
+	form_body = "\n".join(fields_html)
+	pre = PREAMBLE.render(title=f"{config.SITE_NAME} - PyGitWeb settings", site_name=config.SITE_NAME)
+	body = f'<h1 class="page-title">PyGitWeb settings</h1><div class="card"><div class="card-body"><form action="/settings/pygitweb/submit" method="post" class="needs-validation" novalidate>{form_body}<div class="form-footer"><button type="submit" class="btn btn-primary">Save</button><a href="/" class="btn btn-ghost-secondary">Cancel</a></div></form></div></div>'
+	return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
 
 
 @router.post("/pygitweb/submit", response_class=HTMLResponse)
 async def settings_pygitweb_submit(request: Request):
-    """Apply PyGitWeb settings (in-memory for current process)."""
-    form = await request.form()
-    def _get(k: str) -> str:
-        return (form.get(k) or "").strip()
-
-    config.PROJECTROOT = _get("PROJECTROOT") or config.PROJECTROOT
-    config.PROJECTS_LIST = _get("PROJECTS_LIST") or config.PROJECTS_LIST
-    config.SITE_NAME = _get("SITE_NAME") or config.SITE_NAME
-    config.EXPORT_OK = _get("EXPORT_OK")
-    config.LIST_ALL = _get("LIST_ALL").lower() in ("on", "1", "true", "yes")
-    config.STRICT_EXPORT = _get("STRICT_EXPORT").lower() in ("1", "true", "yes")
-    config.GIT = _get("GIT") or config.GIT
-    maxload_s = _get("MAXLOAD")
-    if not maxload_s:
-        config.MAXLOAD = None
-    else:
-        try:
-            config.MAXLOAD = float(maxload_s)
-        except ValueError:
-            config.MAXLOAD = None
-    return RedirectResponse(url="/settings/pygitweb", status_code=303)
+	"""Apply PyGitWeb settings (in-memory for current process)."""
+	form = await request.form()
+
+	def _get(k: str) -> str:
+		return (form.get(k) or "").strip()
+
+	config.PROJECTROOT = _get("PROJECTROOT") or config.PROJECTROOT
+	config.PROJECTS_LIST = _get("PROJECTS_LIST") or config.PROJECTS_LIST
+	config.SITE_NAME = _get("SITE_NAME") or config.SITE_NAME
+	config.EXPORT_OK = _get("EXPORT_OK")
+	config.LIST_ALL = _get("LIST_ALL").lower() in ("on", "1", "true", "yes")
+	config.STRICT_EXPORT = _get("STRICT_EXPORT").lower() in ("1", "true", "yes")
+	config.GIT = _get("GIT") or config.GIT
+	maxload_s = _get("MAXLOAD")
+	if not maxload_s:
+		config.MAXLOAD = None
+	else:
+		try:
+			config.MAXLOAD = float(maxload_s)
+		except ValueError:
+			config.MAXLOAD = None
+	return RedirectResponse(url="/settings/pygitweb", status_code=303)
 
 
 # ---------- Routes: PyGit2 ----------
 @router.get("/pygit2", response_class=HTMLResponse)
 def settings_pygit2_page(request: Request):
-    """PyGit2 library settings form."""
-    fields_html = []
-    for name, val, kind, doc in _get_pygit2_values():
-        fields_html.append(_render_pygit2_field(name, val, kind, doc))
-    form_body = "\n".join(fields_html)
-    pre = PREAMBLE.render(title=f"{config.SITE_NAME} - PyGit2 settings", site_name=config.SITE_NAME)
-    body = f'<h1 class="page-title">PyGit2 settings</h1><div class="card"><div class="card-body"><form action="/settings/pygit2/submit" method="post" class="needs-validation" novalidate>{form_body}<div class="form-footer"><button type="submit" class="btn btn-primary">Save</button><a href="/" class="btn btn-ghost-secondary">Cancel</a></div></form></div></div>'
-    return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
+	"""PyGit2 library settings form."""
+	fields_html = []
+	for name, val, kind, doc in _get_pygit2_values():
+		fields_html.append(_render_pygit2_field(name, val, kind, doc))
+	form_body = "\n".join(fields_html)
+	pre = PREAMBLE.render(title=f"{config.SITE_NAME} - PyGit2 settings", site_name=config.SITE_NAME)
+	body = f'<h1 class="page-title">PyGit2 settings</h1><div class="card"><div class="card-body"><form action="/settings/pygit2/submit" method="post" class="needs-validation" novalidate>{form_body}<div class="form-footer"><button type="submit" class="btn btn-primary">Save</button><a href="/" class="btn btn-ghost-secondary">Cancel</a></div></form></div></div>'
+	return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
 
 
 @router.post("/pygit2/submit", response_class=HTMLResponse)
 async def settings_pygit2_submit(request: Request):
-    """Apply PyGit2 settings from form (all pygit2_* fields)."""
-    form = await request.form()
-    st = pygit2.Settings
-    for key, form_value in form.items():
-        if not key.startswith("pygit2_"):
-            continue
-        attr = key[7:]  # strip pygit2_
-        if attr not in [x[0] for x in PYGIT2_SETTINGS]:
-            continue
-        kind = next((k[2] for k in PYGIT2_SETTINGS if k[0] == attr), "str")
-        if kind in ("readonly", "skip"):
-            continue
-        try:
-            if kind == "bool":
-                val = (form_value or "").strip().lower() in ("on", "1", "true", "yes")
-                setattr(st, attr, val)
-            elif kind == "int":
-                s = (form_value or "").strip()
-                val = int(s) if s else 0
-                setattr(st, attr, val)
-            else:
-                setattr(st, attr, (form_value or "").strip() or None)
-        except (TypeError, AttributeError, ValueError):
-            continue
-    return RedirectResponse(url="/settings/pygit2", status_code=303)
+	"""Apply PyGit2 settings from form (all pygit2_* fields)."""
+	form = await request.form()
+	st = pygit2.Settings
+	for key, form_value in form.items():
+		if not key.startswith("pygit2_"):
+			continue
+		attr = key[7:]  # strip pygit2_
+		if attr not in [x[0] for x in PYGIT2_SETTINGS]:
+			continue
+		kind = next((k[2] for k in PYGIT2_SETTINGS if k[0] == attr), "str")
+		if kind in ("readonly", "skip"):
+			continue
+		try:
+			if kind == "bool":
+				val = (form_value or "").strip().lower() in ("on", "1", "true", "yes")
+				setattr(st, attr, val)
+			elif kind == "int":
+				s = (form_value or "").strip()
+				val = int(s) if s else 0
+				setattr(st, attr, val)
+			else:
+				setattr(st, attr, (form_value or "").strip() or None)
+		except (TypeError, AttributeError, ValueError):
+			continue
+	return RedirectResponse(url="/settings/pygit2", status_code=303)
 
 
 # ---------- Routes: Project ----------
 def _project_in_list(project: str) -> bool:
-    """True if project is in the visible project list."""
-    lst = git_get_projects_list(
-        filter_path="",
-        paranoid=STRICT_EXPORT,
-        export_ok=EXPORT_OK,
-    )
-    return any(p.get("path") == project for p in lst)
+	"""True if project is in the visible project list."""
+	lst = git_get_projects_list(
+		filter_path="",
+		paranoid=STRICT_EXPORT,
+		export_ok=EXPORT_OK,
+	)
+	return any(p.get("path") == project for p in lst)
 
 
 def _validate_project(project: str | None) -> str:
-    """Validate project name and that it exists; raise HTTPException if not."""
-    if not project:
-        raise HTTPException(status_code=400, detail="Project needed")
-    if not is_valid_project(
-        project,
-        PROJECTROOT,
-        EXPORT_OK,
-        STRICT_EXPORT,
-        _project_in_list,
-    ):
-        raise HTTPException(status_code=404, detail="No such project")
-    return project
+	"""Validate project name and that it exists; raise HTTPException if not."""
+	if not project:
+		raise HTTPException(status_code=400, detail="Project needed")
+	if not is_valid_project(
+		project,
+		PROJECTROOT,
+		EXPORT_OK,
+		STRICT_EXPORT,
+		_project_in_list,
+	):
+		raise HTTPException(status_code=404, detail="No such project")
+	return project
 
 
 @router.get("/project/{name:path}", response_class=HTMLResponse)
 def settings_project_page(request: Request, name: str):
-    """Project-specific git config form (local/worktree only)."""
-    project = _validate_project(name)
-    entries = _get_project_config_entries(project)
-    rows = [_project_config_table_row(k, v, i) for i, (k, v) in enumerate(entries)]
-    # One empty row for adding new
-    rows.append(_project_config_table_row("", "", len(entries)))
-    table_html = env.get_template("table.html").render(cols=["Key", "Value"], rows=rows)
-    pre = PREAMBLE.render(title=f"{config.SITE_NAME} - Project settings: {esc_html(project)}", site_name=config.SITE_NAME)
-    submit_url = f"/settings/project/{_quote_path(project)}/submit"
-    cancel_url = f"/project/{_quote_path(project)}"
-    body = f'<h1 class="page-title">Project settings: {esc_html(project)}</h1><p class="text-muted">Repository config (local and worktree only).</p><div class="card"><div class="card-body"><form action="{esc_html(submit_url)}" method="post" class="needs-validation" novalidate>{table_html}<div class="form-footer mt-3"><button type="submit" class="btn btn-primary">Save</button><a href="{esc_html(cancel_url)}" class="btn btn-ghost-secondary">Cancel</a></div></form></div></div>'
-    return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
+	"""Project-specific git config form (local/worktree only)."""
+	project = _validate_project(name)
+	entries = _get_project_config_entries(project)
+	rows = [_project_config_table_row(k, v, i) for i, (k, v) in enumerate(entries)]
+	# One empty row for adding new
+	rows.append(_project_config_table_row("", "", len(entries)))
+	table_html = env.get_template("table.html").render(cols=["Key", "Value"], rows=rows)
+	pre = PREAMBLE.render(
+		title=f"{config.SITE_NAME} - Project settings: {esc_html(project)}",
+		site_name=config.SITE_NAME,
+	)
+	submit_url = f"/settings/project/{_quote_path(project)}/submit"
+	cancel_url = f"/project/{_quote_path(project)}"
+	body = f'<h1 class="page-title">Project settings: {esc_html(project)}</h1><p class="text-muted">Repository config (local and worktree only).</p><div class="card"><div class="card-body"><form action="{esc_html(submit_url)}" method="post" class="needs-validation" novalidate>{table_html}<div class="form-footer mt-3"><button type="submit" class="btn btn-primary">Save</button><a href="{esc_html(cancel_url)}" class="btn btn-ghost-secondary">Cancel</a></div></form></div></div>'
+	return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
 
 
 @router.post("/project/{name:path}/submit", response_class=HTMLResponse)
 async def settings_project_submit(request: Request, name: str):
-    """Apply project config from form."""
-    project = _validate_project(name)
-    form = await request.form()
-    # Collect key/value pairs; keys are config_key_0, config_value_0, ...
-    indices = set()
-    for form_key in form:
-        if form_key.startswith("config_key_"):
-            try:
-                indices.add(int(form_key.split("_")[-1]))
-            except ValueError:
-                pass
-    pairs = []
-    for i in sorted(indices):
-        k = (form.get(f"config_key_{i}") or "").strip()
-        v = (form.get(f"config_value_{i}") or "").strip()
-        if k:
-            pairs.append((k, v))
-    repo = open_repo(project)
-    cfg = repo.config
-    for config_key, config_value in pairs:
-        try:
-            cfg[config_key] = config_value
-        except (pygit2.GitError, ValueError, KeyError):
-            continue
-    return RedirectResponse(url=f"/settings/project/{_quote_path(project)}", status_code=303)
+	"""Apply project config from form."""
+	project = _validate_project(name)
+	form = await request.form()
+	# Collect key/value pairs; keys are config_key_0, config_value_0, ...
+	indices = set()
+	for form_key in form:
+		if form_key.startswith("config_key_"):
+			try:
+				indices.add(int(form_key.split("_")[-1]))
+			except ValueError:
+				pass
+	pairs = []
+	for i in sorted(indices):
+		k = (form.get(f"config_key_{i}") or "").strip()
+		v = (form.get(f"config_value_{i}") or "").strip()
+		if k:
+			pairs.append((k, v))
+	repo = open_repo(project)
+	cfg = repo.config
+	for config_key, config_value in pairs:
+		try:
+			cfg[config_key] = config_value
+		except (pygit2.GitError, ValueError, KeyError):
+			continue
+	return RedirectResponse(url=f"/settings/project/{_quote_path(project)}", status_code=303)
diff --git a/pygitweb/tasks.py b/pygitweb/tasks.py
index 4310624..1b40321 100644
--- a/pygitweb/tasks.py
+++ b/pygitweb/tasks.py
@@ -2,19 +2,27 @@
 Task-related routes: Task, comment, board creation, listing, and management.
 Data is stored in the repo ODB/RefDB via distgit.tasks (Board, Task, Comment).
 """
+
 from __future__ import annotations
 
 import time
 from typing import Any
-from urllib.parse import quote
 
 import pygit2
 from fastapi import APIRouter, HTTPException, Query, Request
 from fastapi.responses import JSONResponse
 
-from distgit.tasks import Board, Comment, Task, get_board, get_comment, get_task, get_task_by_oid
+from distgit.tasks import (
+	Board,
+	Comment,
+	Task,
+	get_board,
+	get_comment,
+	get_task,
+	get_task_by_oid,
+)
 from pygitweb.config import DISTGIT_AUTH, EXPORT_OK, PROJECTROOT, STRICT_EXPORT
-from pygitweb.git_helpers import open_repo, git_get_references
+from pygitweb.git_helpers import git_get_references, open_repo
 from pygitweb.projects import git_get_projects_list
 from pygitweb.validation import is_valid_project
 
@@ -25,62 +33,62 @@ EMPTY_TREE_OID = pygit2.Oid(hex="4b825dc642cb6eb9a060e54bf8d69288fbee4904")
 
 
 def _require_auth_disabled() -> None:
-    """Raise 401 if authentication is enabled (we will add real auth later)."""
-    if DISTGIT_AUTH != "None":
-        raise HTTPException(status_code=401, detail="Authentication required")
+	"""Raise 401 if authentication is enabled (we will add real auth later)."""
+	if DISTGIT_AUTH != "None":
+		raise HTTPException(status_code=401, detail="Authentication required")
 
 
 def _project_in_list(project: str) -> bool:
-    lst = git_get_projects_list(
-        filter_path="",
-        paranoid=STRICT_EXPORT,
-        export_ok=EXPORT_OK,
-    )
-    return any(p.get("path") == project for p in lst)
+	lst = git_get_projects_list(
+		filter_path="",
+		paranoid=STRICT_EXPORT,
+		export_ok=EXPORT_OK,
+	)
+	return any(p.get("path") == project for p in lst)
 
 
 def _validate_project(project: str | None) -> str:
-    if not project:
-        raise HTTPException(status_code=400, detail="Project needed")
-    if not is_valid_project(
-        project,
-        PROJECTROOT,
-        EXPORT_OK,
-        STRICT_EXPORT,
-        _project_in_list,
-    ):
-        raise HTTPException(status_code=404, detail="No such project")
-    return project
+	if not project:
+		raise HTTPException(status_code=400, detail="Project needed")
+	if not is_valid_project(
+		project,
+		PROJECTROOT,
+		EXPORT_OK,
+		STRICT_EXPORT,
+		_project_in_list,
+	):
+		raise HTTPException(status_code=404, detail="No such project")
+	return project
 
 
 def _board_ref(name: str) -> str:
-    return f"{BOARD_REF_PREFIX}{name}"
+	return f"{BOARD_REF_PREFIX}{name}"
 
 
 def _task_ref(task_id: str) -> str:
-    return f"{TASK_REF_PREFIX}{task_id}"
+	return f"{TASK_REF_PREFIX}{task_id}"
 
 
 def _repo_head_or_empty(repo: pygit2.Repository) -> pygit2.Oid:
-    if repo.head:
-        return repo.head.target
-    return EMPTY_TREE_OID
+	if repo.head:
+		return repo.head.target
+	return EMPTY_TREE_OID
 
 
 def create_board_for_project(
-    project: str,
-    name: str = "Tasks",
-    description: str = "",
+	project: str,
+	name: str = "Tasks",
+	description: str = "",
 ) -> str:
-    """Create a board in the project repo. Returns the board ref. Raises HTTPException on error."""
-    repo = open_repo(project)
-    ref = _board_ref(name)
-    if ref in repo.references:
-        raise HTTPException(status_code=409, detail="Board already exists")
-    target = _repo_head_or_empty(repo)
-    board = Board(target, ref, tagger="", description=description)
-    board.write(repo)
-    return ref
+	"""Create a board in the project repo. Returns the board ref. Raises HTTPException on error."""
+	repo = open_repo(project)
+	ref = _board_ref(name)
+	if ref in repo.references:
+		raise HTTPException(status_code=409, detail="Board already exists")
+	target = _repo_head_or_empty(repo)
+	board = Board(target, ref, tagger="", description=description)
+	board.write(repo)
+	return ref
 
 
 # ---------- Board Routes ----------
@@ -90,66 +98,68 @@ board_router = APIRouter(tags=["boards"])
 
 @board_router.get("/list", response_class=JSONResponse)
 def boards_list(
-    request: Request,
-    project: str = Query(..., description="Project path"),
+	request: Request,
+	project: str = Query(..., description="Project path"),
 ) -> JSONResponse:
-    """List all boards for the project."""
-    _require_auth_disabled()
-    _validate_project(project)
-    repo = open_repo(project)
-    refs = git_get_references(project, "refs/boards")
-    boards = []
-    for ref_name, _ in refs:
-        try:
-            b = get_board(repo, ref_name)
-            if b:
-                boards.append({
-                    "name": ref_name.replace(BOARD_REF_PREFIX, ""),
-                    "description": getattr(b, "description", "") or "",
-                    "task_count": len(getattr(b, "tasks", [])),
-                })
-        except (ValueError, KeyError):
-            continue
-    return JSONResponse(content=boards)
+	"""List all boards for the project."""
+	_require_auth_disabled()
+	_validate_project(project)
+	repo = open_repo(project)
+	refs = git_get_references(project, "refs/boards")
+	boards = []
+	for ref_name, _ in refs:
+		try:
+			b = get_board(repo, ref_name)
+			if b:
+				boards.append(
+					{
+						"name": ref_name.replace(BOARD_REF_PREFIX, ""),
+						"description": getattr(b, "description", "") or "",
+						"task_count": len(getattr(b, "tasks", [])),
+					}
+				)
+		except (ValueError, KeyError):
+			continue
+	return JSONResponse(content=boards)
 
 
 @board_router.post("/create", response_class=JSONResponse)
 def boards_create(
-    request: Request,
-    project: str = Query(..., description="Project path"),
-    name: str = Query(..., description="Board name"),
-    description: str = Query("", description="Board description"),
+	request: Request,
+	project: str = Query(..., description="Project path"),
+	name: str = Query(..., description="Board name"),
+	description: str = Query("", description="Board description"),
 ) -> JSONResponse:
-    """Create a new board."""
-    _require_auth_disabled()
-    _validate_project(project)
-    if not name or "/" in name or ".." in name:
-        raise HTTPException(status_code=400, detail="Invalid board name")
-    repo = open_repo(project)
-    ref = _board_ref(name)
-    if ref in repo.references:
-        raise HTTPException(status_code=409, detail="Board already exists")
-    target = _repo_head_or_empty(repo)
-    board = Board(target, ref, tagger="", description=description or "")
-    oid = board.write(repo)
-    return JSONResponse(content={"name": name, "ref": ref, "oid": str(oid)})
+	"""Create a new board."""
+	_require_auth_disabled()
+	_validate_project(project)
+	if not name or "/" in name or ".." in name:
+		raise HTTPException(status_code=400, detail="Invalid board name")
+	repo = open_repo(project)
+	ref = _board_ref(name)
+	if ref in repo.references:
+		raise HTTPException(status_code=409, detail="Board already exists")
+	target = _repo_head_or_empty(repo)
+	board = Board(target, ref, tagger="", description=description or "")
+	oid = board.write(repo)
+	return JSONResponse(content={"name": name, "ref": ref, "oid": str(oid)})
 
 
 @board_router.post("/delete", response_class=JSONResponse)
 def boards_delete(
-    request: Request,
-    project: str = Query(..., description="Project path"),
-    name: str = Query(..., description="Board name"),
+	request: Request,
+	project: str = Query(..., description="Project path"),
+	name: str = Query(..., description="Board name"),
 ) -> JSONResponse:
-    """Delete a board (removes ref; tag object remains in ODB)."""
-    _require_auth_disabled()
-    _validate_project(project)
-    repo = open_repo(project)
-    ref = _board_ref(name)
-    if ref not in repo.references:
-        raise HTTPException(status_code=404, detail="Board not found")
-    repo.references.delete(ref)
-    return JSONResponse(content={"message": "Board deleted"})
+	"""Delete a board (removes ref; tag object remains in ODB)."""
+	_require_auth_disabled()
+	_validate_project(project)
+	repo = open_repo(project)
+	ref = _board_ref(name)
+	if ref not in repo.references:
+		raise HTTPException(status_code=404, detail="Board not found")
+	repo.references.delete(ref)
+	return JSONResponse(content={"message": "Board deleted"})
 
 
 # ---------- Task Routes ----------
@@ -158,18 +168,18 @@ task_router = APIRouter(tags=["tasks"])
 
 
 def _task_to_json(t: Task) -> dict[str, Any]:
-    return {
-        "ref": t.name,
-        "title": t.title,
-        "description": getattr(t, "description", "") or "",
-        "status": t.status.value if t.status else None,
-        "priority": t.priority.value if t.priority else None,
-        "assignee": getattr(t, "assignee"),
-        "due_date": t.due_date.isoformat() if getattr(t, "due_date") else None,
-        "created_at": getattr(t, "created_at").isoformat() if getattr(t, "created_at") else None,
-        "updated_at": getattr(t, "updated_at").isoformat() if getattr(t, "updated_at") else None,
-        "comments_count": len(getattr(t, "comments", [])),
-    }
+	return {
+		"ref": t.name,
+		"title": t.title,
+		"description": getattr(t, "description", "") or "",
+		"status": t.status.value if t.status else None,
+		"priority": t.priority.value if t.priority else None,
+		"assignee": t.assignee,
+		"due_date": t.due_date.isoformat() if t.due_date else None,
+		"created_at": t.created_at.isoformat() if t.created_at else None,
+		"updated_at": t.updated_at.isoformat() if t.updated_at else None,
+		"comments_count": len(getattr(t, "comments", [])),
+	}
 
 
 # Status order for board columns
@@ -177,186 +187,196 @@ BOARD_STATUS_ORDER = ["TODO", "IN_PROGRESS", "IN_REVIEW", "DONE", "CANCELLED"]
 
 
 def get_board_tasks_grouped(
-    project: str,
-    board_name: str,
+	project: str,
+	board_name: str,
 ) -> list[dict[str, Any]]:
-    """
-    Return tasks for a board grouped by status for board view.
-    Returns a list of { "status": str, "label": str, "tasks": [ _task_to_json, ... ] }
-    in BOARD_STATUS_ORDER. Skips validation/auth (caller must validate project).
-    """
-    repo = open_repo(project)
-    ref = _board_ref(board_name)
-    b = get_board(repo, ref)
-    task_oids = getattr(b, "tasks", []) or []
-    by_status: dict[str, list[dict[str, Any]]] = {s: [] for s in BOARD_STATUS_ORDER}
-    for oid_hex in task_oids:
-        try:
-            oid = pygit2.Oid(hex=oid_hex) if isinstance(oid_hex, str) else oid_hex
-            t = get_task_by_oid(repo, oid)
-            if t:
-                d = _task_to_json(t)
-                status_key = (d.get("status") or "TODO") if d else "TODO"
-                if status_key not in by_status:
-                    by_status[status_key] = []
-                by_status[status_key].append(d)
-        except (ValueError, KeyError, TypeError):
-            continue
-    return [
-        {"status": s, "label": s.replace("_", " ").title(), "tasks": by_status.get(s, [])}
-        for s in BOARD_STATUS_ORDER
-    ]
+	"""
+	Return tasks for a board grouped by status for board view.
+	Returns a list of { "status": str, "label": str, "tasks": [ _task_to_json, ... ] }
+	in BOARD_STATUS_ORDER. Skips validation/auth (caller must validate project).
+	"""
+	repo = open_repo(project)
+	ref = _board_ref(board_name)
+	b = get_board(repo, ref)
+	task_oids = getattr(b, "tasks", []) or []
+	by_status: dict[str, list[dict[str, Any]]] = {s: [] for s in BOARD_STATUS_ORDER}
+	for oid_hex in task_oids:
+		try:
+			oid = pygit2.Oid(hex=oid_hex) if isinstance(oid_hex, str) else oid_hex
+			t = get_task_by_oid(repo, oid)
+			if t:
+				d = _task_to_json(t)
+				status_key = (d.get("status") or "TODO") if d else "TODO"
+				if status_key not in by_status:
+					by_status[status_key] = []
+				by_status[status_key].append(d)
+		except (ValueError, KeyError, TypeError):
+			continue
+	return [
+		{
+			"status": s,
+			"label": s.replace("_", " ").title(),
+			"tasks": by_status.get(s, []),
+		}
+		for s in BOARD_STATUS_ORDER
+	]
 
 
 @task_router.get("/list", response_class=JSONResponse)
 def task_list(
-    request: Request,
-    project: str = Query(..., description="Project path"),
-    board: str = Query(..., description="Board name"),
+	request: Request,
+	project: str = Query(..., description="Project path"),
+	board: str = Query(..., description="Board name"),
 ) -> JSONResponse:
-    """List all tasks on a board."""
-    _require_auth_disabled()
-    _validate_project(project)
-    repo = open_repo(project)
-    ref = _board_ref(board)
-    b = get_board(repo, ref)
-    if not b:
-        raise HTTPException(status_code=404, detail="Board not found")
-    task_oids = getattr(b, "tasks", [])
-    tasks = []
-    for oid_hex in task_oids:
-        try:
-            oid = pygit2.Oid(hex=oid_hex) if isinstance(oid_hex, str) else oid_hex
-            t = get_task_by_oid(repo, oid)
-            if t:
-                tasks.append(_task_to_json(t))
-        except (ValueError, KeyError, TypeError):
-            continue
-    return JSONResponse(content=tasks)
+	"""List all tasks on a board."""
+	_require_auth_disabled()
+	_validate_project(project)
+	repo = open_repo(project)
+	ref = _board_ref(board)
+	b = get_board(repo, ref)
+	if not b:
+		raise HTTPException(status_code=404, detail="Board not found")
+	task_oids = getattr(b, "tasks", [])
+	tasks = []
+	for oid_hex in task_oids:
+		try:
+			oid = pygit2.Oid(hex=oid_hex) if isinstance(oid_hex, str) else oid_hex
+			t = get_task_by_oid(repo, oid)
+			if t:
+				tasks.append(_task_to_json(t))
+		except (ValueError, KeyError, TypeError):
+			continue
+	return JSONResponse(content=tasks)
 
 
 @task_router.post("/create", response_class=JSONResponse)
 def task_create(
-    request: Request,
-    project: str = Query(..., description="Project path"),
-    board: str = Query(..., description="Board name"),
-    title: str = Query(..., description="Task title"),
-    description: str = Query("", description="Task description"),
-    status: str = Query("TODO", description="Task status"),
-    priority: str = Query("LOW", description="Task priority"),
-    assignee: str = Query("", description="Assignee"),
-    due_date: str = Query("", description="Due date ISO"),
+	request: Request,
+	project: str = Query(..., description="Project path"),
+	board: str = Query(..., description="Board name"),
+	title: str = Query(..., description="Task title"),
+	description: str = Query("", description="Task description"),
+	status: str = Query("TODO", description="Task status"),
+	priority: str = Query("LOW", description="Task priority"),
+	assignee: str = Query("", description="Assignee"),
+	due_date: str = Query("", description="Due date ISO"),
 ) -> JSONResponse:
-    """Create a new task on a board."""
-    _require_auth_disabled()
-    _validate_project(project)
-    repo = open_repo(project)
-    board_ref = _board_ref(board)
-    b = get_board(repo, board_ref)
-    if not b:
-        raise HTTPException(status_code=404, detail="Board not found")
-    task_id = f"task_{int(time.time() * 1000)}"
-    ref = _task_ref(task_id)
-    target = _repo_head_or_empty(repo)
-    status_enum = getattr(Task.Status, status, None) or Task.Status.TODO
-    priority_enum = getattr(Task.Priority, priority, None) or Task.Priority.LOW
-    due = None
-    if due_date:
-        try:
-            from datetime import datetime
-            due = datetime.fromisoformat(due_date.replace("Z", "+00:00"))
-        except ValueError:
-            pass
-    task = Task(
-        target, ref, tagger="",
-        title=title,
-        description=description or "",
-        status=status_enum,
-        priority=priority_enum,
-        assignee=assignee or None,
-        due_date=due,
-    )
-    oid = task.write(repo)
-    b.tasks = getattr(b, "tasks", []) or []
-    b.tasks.append(str(oid))
-    b.update_message()
-    b.write(repo)
-    return JSONResponse(content={"task_id": task_id, "ref": ref, "oid": str(oid)})
+	"""Create a new task on a board."""
+	_require_auth_disabled()
+	_validate_project(project)
+	repo = open_repo(project)
+	board_ref = _board_ref(board)
+	b = get_board(repo, board_ref)
+	if not b:
+		raise HTTPException(status_code=404, detail="Board not found")
+	task_id = f"task_{int(time.time() * 1000)}"
+	ref = _task_ref(task_id)
+	target = _repo_head_or_empty(repo)
+	status_enum = getattr(Task.Status, status, None) or Task.Status.TODO
+	priority_enum = getattr(Task.Priority, priority, None) or Task.Priority.LOW
+	due = None
+	if due_date:
+		try:
+			from datetime import datetime
+
+			due = datetime.fromisoformat(due_date.replace("Z", "+00:00"))
+		except ValueError:
+			pass
+	task = Task(
+		target,
+		ref,
+		tagger="",
+		title=title,
+		description=description or "",
+		status=status_enum,
+		priority=priority_enum,
+		assignee=assignee or None,
+		due_date=due,
+	)
+	oid = task.write(repo)
+	b.tasks = getattr(b, "tasks", []) or []
+	b.tasks.append(str(oid))
+	b.update_message()
+	b.write(repo)
+	return JSONResponse(content={"task_id": task_id, "ref": ref, "oid": str(oid)})
 
 
 @task_router.post("/update", response_class=JSONResponse)
 async def task_update(
-    request: Request,
-    project: str = Query(..., description="Project path"),
-    board: str = Query(..., description="Board name"),
-    task_ref: str = Query(..., alias="task", description="Task ref (e.g. refs/tasks/task_123)"),
+	request: Request,
+	project: str = Query(..., description="Project path"),
+	board: str = Query(..., description="Board name"),
+	task_ref: str = Query(..., alias="task", description="Task ref (e.g. refs/tasks/task_123)"),
 ) -> JSONResponse:
-    """Update a task (body: optional title, description, status, priority, assignee, due_date)."""
-    _require_auth_disabled()
-    _validate_project(project)
-    try:
-        body = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
-    except Exception:
-        body = {}
-    repo = open_repo(project)
-    ref = task_ref if task_ref.startswith("refs/") else _task_ref(task_ref)
-    t = get_task(repo, ref)
-    if not t:
-        raise HTTPException(status_code=404, detail="Task not found")
-    if "title" in body:
-        t.title = body["title"]
-    if "description" in body:
-        t.description = body.get("description", "")
-    if "status" in body and hasattr(Task.Status, body["status"]):
-        t.status = Task.Status(body["status"])
-    if "priority" in body and hasattr(Task.Priority, body["priority"]):
-        t.priority = Task.Priority(body["priority"])
-    if "assignee" in body:
-        t.assignee = body["assignee"] or None
-    if "due_date" in body:
-        try:
-            from datetime import datetime
-            t.due_date = datetime.fromisoformat(str(body["due_date"]).replace("Z", "+00:00")) if body["due_date"] else None
-        except ValueError:
-            pass
-    old_oid = str(repo.references[ref].resolve().target)
-    t.update_message()
-    new_oid = t.write(repo)
-    board_ref = _board_ref(board)
-    b = get_board(repo, board_ref)
-    if b and getattr(b, "tasks", None):
-        b.tasks = [str(new_oid) if o == old_oid else o for o in b.tasks]
-        b.update_message()
-        b.write(repo)
-    return JSONResponse(content=_task_to_json(t))
+	"""Update a task (body: optional title, description, status, priority, assignee, due_date)."""
+	_require_auth_disabled()
+	_validate_project(project)
+	try:
+		body = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
+	except Exception:
+		body = {}
+	repo = open_repo(project)
+	ref = task_ref if task_ref.startswith("refs/") else _task_ref(task_ref)
+	t = get_task(repo, ref)
+	if not t:
+		raise HTTPException(status_code=404, detail="Task not found")
+	if "title" in body:
+		t.title = body["title"]
+	if "description" in body:
+		t.description = body.get("description", "")
+	if "status" in body and hasattr(Task.Status, body["status"]):
+		t.status = Task.Status(body["status"])
+	if "priority" in body and hasattr(Task.Priority, body["priority"]):
+		t.priority = Task.Priority(body["priority"])
+	if "assignee" in body:
+		t.assignee = body["assignee"] or None
+	if "due_date" in body:
+		try:
+			from datetime import datetime
+
+			t.due_date = (
+				datetime.fromisoformat(str(body["due_date"]).replace("Z", "+00:00")) if body["due_date"] else None
+			)
+		except ValueError:
+			pass
+	old_oid = str(repo.references[ref].resolve().target)
+	t.update_message()
+	new_oid = t.write(repo)
+	board_ref = _board_ref(board)
+	b = get_board(repo, board_ref)
+	if b and getattr(b, "tasks", None):
+		b.tasks = [str(new_oid) if o == old_oid else o for o in b.tasks]
+		b.update_message()
+		b.write(repo)
+	return JSONResponse(content=_task_to_json(t))
 
 
 @task_router.post("/delete", response_class=JSONResponse)
 def task_delete(
-    request: Request,
-    project: str = Query(..., description="Project path"),
-    board: str = Query(..., description="Board name"),
-    task_ref: str = Query(..., alias="task", description="Task ref"),
+	request: Request,
+	project: str = Query(..., description="Project path"),
+	board: str = Query(..., description="Board name"),
+	task_ref: str = Query(..., alias="task", description="Task ref"),
 ) -> JSONResponse:
-    """Delete a task (remove ref and remove from board.tasks)."""
-    _require_auth_disabled()
-    _validate_project(project)
-    repo = open_repo(project)
-    ref = task_ref if task_ref.startswith("refs/") else _task_ref(task_ref)
-    if ref not in repo.references:
-        raise HTTPException(status_code=404, detail="Task not found")
-    oid_hex = str(repo.references[ref].resolve().target)
-    repo.references.delete(ref)
-    board_ref = _board_ref(board)
-    b = get_board(repo, board_ref)
-    if b and getattr(b, "tasks", None):
-        try:
-            b.tasks = [x for x in b.tasks if x != oid_hex]
-            b.update_message()
-            b.write(repo)
-        except Exception:
-            pass
-    return JSONResponse(content={"message": "Task deleted"})
+	"""Delete a task (remove ref and remove from board.tasks)."""
+	_require_auth_disabled()
+	_validate_project(project)
+	repo = open_repo(project)
+	ref = task_ref if task_ref.startswith("refs/") else _task_ref(task_ref)
+	if ref not in repo.references:
+		raise HTTPException(status_code=404, detail="Task not found")
+	oid_hex = str(repo.references[ref].resolve().target)
+	repo.references.delete(ref)
+	board_ref = _board_ref(board)
+	b = get_board(repo, board_ref)
+	if b and getattr(b, "tasks", None):
+		try:
+			b.tasks = [x for x in b.tasks if x != oid_hex]
+			b.update_message()
+			b.write(repo)
+		except Exception:
+			pass
+	return JSONResponse(content={"message": "Task deleted"})
 
 
 # ---------- Comment Routes ----------
@@ -365,120 +385,127 @@ comment_router = APIRouter(tags=["comments"])
 
 
 def _comment_to_json(c: Comment) -> dict[str, Any]:
-    return {
-        "content": c.content,
-        "tagger": c.tagger,
-        "created_at": getattr(c, "created_at").isoformat() if getattr(c, "created_at") else None,
-        "edited_at": getattr(c, "edited_at").isoformat() if getattr(c, "edited_at") else None,
-    }
+	return {
+		"content": c.content,
+		"tagger": c.tagger,
+		"created_at": c.created_at.isoformat() if c.created_at else None,
+		"edited_at": c.edited_at.isoformat() if c.edited_at else None,
+	}
 
 
 @comment_router.get("/list", response_class=JSONResponse)
 def comment_list(
-    request: Request,
-    project: str = Query(..., description="Project path"),
-    board: str = Query(..., description="Board name"),
-    task: str = Query(..., description="Task ref (e.g. refs/tasks/task_123)"),
+	request: Request,
+	project: str = Query(..., description="Project path"),
+	board: str = Query(..., description="Board name"),
+	task: str = Query(..., description="Task ref (e.g. refs/tasks/task_123)"),
 ) -> JSONResponse:
-    """List all comments for a task."""
-    _require_auth_disabled()
-    _validate_project(project)
-    repo = open_repo(project)
-    task_ref = task if task.startswith("refs/") else _task_ref(task)
-    t = get_task(repo, task_ref)
-    if not t:
-        raise HTTPException(status_code=404, detail="Task not found")
-    comment_oids = getattr(t, "comments", []) or []
-    comments = []
-    for oid_hex in comment_oids:
-        try:
-            oid = pygit2.Oid(hex=oid_hex) if isinstance(oid_hex, str) else oid_hex
-            c = get_comment(repo, oid)
-            if c:
-                comments.append(_comment_to_json(c))
-        except (ValueError, KeyError, TypeError):
-            continue
-    return JSONResponse(content=comments)
+	"""List all comments for a task."""
+	_require_auth_disabled()
+	_validate_project(project)
+	repo = open_repo(project)
+	task_ref = task if task.startswith("refs/") else _task_ref(task)
+	t = get_task(repo, task_ref)
+	if not t:
+		raise HTTPException(status_code=404, detail="Task not found")
+	comment_oids = getattr(t, "comments", []) or []
+	comments = []
+	for oid_hex in comment_oids:
+		try:
+			oid = pygit2.Oid(hex=oid_hex) if isinstance(oid_hex, str) else oid_hex
+			c = get_comment(repo, oid)
+			if c:
+				comments.append(_comment_to_json(c))
+		except (ValueError, KeyError, TypeError):
+			continue
+	return JSONResponse(content=comments)
 
 
 @comment_router.post("/create", response_class=JSONResponse)
 def comment_create(
-    request: Request,
-    project: str = Query(..., description="Project path"),
-    board: str = Query(..., description="Board name"),
-    task: str = Query(..., description="Task ref"),
-    content: str = Query(..., description="Comment content"),
+	request: Request,
+	project: str = Query(..., description="Project path"),
+	board: str = Query(..., description="Board name"),
+	task: str = Query(..., description="Task ref"),
+	content: str = Query(..., description="Comment content"),
 ) -> JSONResponse:
-    """Create a new comment on a task."""
-    _require_auth_disabled()
-    _validate_project(project)
-    repo = open_repo(project)
-    task_ref = task if task.startswith("refs/") else _task_ref(task)
-    t = get_task(repo, task_ref)
-    if not t:
-        raise HTTPException(status_code=404, detail="Task not found")
-    resolved = repo.revparse_single(task_ref)
-    target_oid = resolved.oid if hasattr(resolved, "oid") else pygit2.Oid(hex=str(resolved))
-    comment = Comment(target=target_oid, tagger="", content=content or "")
-    comment_oid = comment.write(repo)
-    t.comments = getattr(t, "comments", []) or []
-    t.comments.append(str(comment_oid))
-    t.update_message()
-    t.write(repo)
-    return JSONResponse(content={"oid": str(comment_oid), "message": "Comment created"})
+	"""Create a new comment on a task."""
+	_require_auth_disabled()
+	_validate_project(project)
+	repo = open_repo(project)
+	task_ref = task if task.startswith("refs/") else _task_ref(task)
+	t = get_task(repo, task_ref)
+	if not t:
+		raise HTTPException(status_code=404, detail="Task not found")
+	resolved = repo.revparse_single(task_ref)
+	target_oid = resolved.oid if hasattr(resolved, "oid") else pygit2.Oid(hex=str(resolved))
+	comment = Comment(target=target_oid, tagger="", content=content or "")
+	comment_oid = comment.write(repo)
+	t.comments = getattr(t, "comments", []) or []
+	t.comments.append(str(comment_oid))
+	t.update_message()
+	t.write(repo)
+	return JSONResponse(content={"oid": str(comment_oid), "message": "Comment created"})
 
 
 @comment_router.post("/update", response_class=JSONResponse)
 async def comment_update(
-    request: Request,
-    project: str = Query(..., description="Project path"),
-    board: str = Query(..., description="Board name"),
-    task: str = Query(..., description="Task ref (to update task.comments)"),
-    comment_oid: str = Query(..., alias="comment", description="Comment OID hex"),
+	request: Request,
+	project: str = Query(..., description="Project path"),
+	board: str = Query(..., description="Board name"),
+	task: str = Query(..., description="Task ref (to update task.comments)"),
+	comment_oid: str = Query(..., alias="comment", description="Comment OID hex"),
 ) -> JSONResponse:
-    """Update a comment (body: content). Writes new comment object and updates task.comments."""
-    _require_auth_disabled()
-    _validate_project(project)
-    try:
-        body = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
-    except Exception:
-        body = {}
-    repo = open_repo(project)
-    oid = pygit2.Oid(hex=comment_oid)
-    c = get_comment(repo, oid)
-    if not c:
-        raise HTTPException(status_code=404, detail="Comment not found")
-    if "content" in body:
-        c.content = body["content"]
-    from datetime import datetime
-    c.edited_at = datetime.now()
-    c.update_message()
-    new_oid = c.write(repo)
-    task_ref = task if task.startswith("refs/") else _task_ref(task)
-    t = get_task(repo, task_ref)
-    if t and getattr(t, "comments", None):
-        t.comments = [str(new_oid) if str(co) == comment_oid else co for co in t.comments]
-        t.update_message()
-        t.write(repo)
-    return JSONResponse(content={**_comment_to_json(c), "oid": str(new_oid), "message": "Comment updated"})
+	"""Update a comment (body: content). Writes new comment object and updates task.comments."""
+	_require_auth_disabled()
+	_validate_project(project)
+	try:
+		body = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
+	except Exception:
+		body = {}
+	repo = open_repo(project)
+	oid = pygit2.Oid(hex=comment_oid)
+	c = get_comment(repo, oid)
+	if not c:
+		raise HTTPException(status_code=404, detail="Comment not found")
+	if "content" in body:
+		c.content = body["content"]
+	from datetime import datetime
+
+	c.edited_at = datetime.now()
+	c.update_message()
+	new_oid = c.write(repo)
+	task_ref = task if task.startswith("refs/") else _task_ref(task)
+	t = get_task(repo, task_ref)
+	if t and getattr(t, "comments", None):
+		t.comments = [str(new_oid) if str(co) == comment_oid else co for co in t.comments]
+		t.update_message()
+		t.write(repo)
+	return JSONResponse(
+		content={
+			**_comment_to_json(c),
+			"oid": str(new_oid),
+			"message": "Comment updated",
+		}
+	)
 
 
 @comment_router.post("/delete", response_class=JSONResponse)
 def comment_delete(
-    request: Request,
-    project: str = Query(..., description="Project path"),
-    board: str = Query(..., description="Board name"),
-    comment_oid: str = Query(..., alias="comment", description="Comment OID hex"),
+	request: Request,
+	project: str = Query(..., description="Project path"),
+	board: str = Query(..., description="Board name"),
+	comment_oid: str = Query(..., alias="comment", description="Comment OID hex"),
 ) -> JSONResponse:
-    """Delete a comment (object remains in ODB; caller may remove from task.comments)."""
-    _require_auth_disabled()
-    _validate_project(project)
-    repo = open_repo(project)
-    oid = pygit2.Oid(hex=comment_oid)
-    try:
-        c = get_comment(repo, oid)
-    except (ValueError, KeyError):
-        raise HTTPException(status_code=404, detail="Comment not found")
-    if not c:
-        raise HTTPException(status_code=404, detail="Comment not found")
-    return JSONResponse(content={"message": "Comment deleted"})
\ No newline at end of file
+	"""Delete a comment (object remains in ODB; caller may remove from task.comments)."""
+	_require_auth_disabled()
+	_validate_project(project)
+	repo = open_repo(project)
+	oid = pygit2.Oid(hex=comment_oid)
+	try:
+		c = get_comment(repo, oid)
+	except (ValueError, KeyError):
+		raise HTTPException(status_code=404, detail="Comment not found")
+	if not c:
+		raise HTTPException(status_code=404, detail="Comment not found")
+	return JSONResponse(content={"message": "Comment deleted"})
diff --git a/pygitweb/templates_env.py b/pygitweb/templates_env.py
index 0e26006..50cbd1c 100644
--- a/pygitweb/templates_env.py
+++ b/pygitweb/templates_env.py
@@ -1,10 +1,11 @@
 """Shared Jinja2 environment and preamble/postamble for pygitweb templates."""
+
 from jinja2 import Environment, PackageLoader
 
 env = Environment(
-    loader=PackageLoader("pygitweb", "templates"),
-    trim_blocks=True,
-    lstrip_blocks=True,
+	loader=PackageLoader("pygitweb", "templates"),
+	trim_blocks=True,
+	lstrip_blocks=True,
 )
 PREAMBLE = env.get_template("preamble.html")
 POSTAMBLE = """</div></div></div></body></html>"""
diff --git a/pygitweb/validation.py b/pygitweb/validation.py
index 129e4f1..9d00d86 100644
--- a/pygitweb/validation.py
+++ b/pygitweb/validation.py
@@ -3,11 +3,12 @@ Validation: pathname, ref format, refname, project, action; repo discovery via p
 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 typing import Callable
+from collections.abc import Callable
 
 import pygit2
 
@@ -18,100 +19,100 @@ SHA256_EXTRA = 24
 
 
 def oid_nlen_regex(length: int | str) -> re.Pattern[str]:
-    """Regex matching exactly `length` hex chars. Port of oid_nlen_regex."""
-    if isinstance(length, str) and "-" in length:
-        lo, hi = length.split("-")
-        return re.compile(f"^[0-9a-fA-F]{{{int(lo)},{int(hi)}}}$")
-    n = int(length)
-    return re.compile(f"^[0-9a-fA-F]{{{n}}}$")
+	"""Regex matching exactly `length` hex chars. Port of oid_nlen_regex."""
+	if isinstance(length, str) and "-" in length:
+		lo, hi = length.split("-")
+		return re.compile(f"^[0-9a-fA-F]{{{int(lo)},{int(hi)}}}$")
+	n = int(length)
+	return re.compile(f"^[0-9a-fA-F]{{{n}}}$")
 
 
 def oid_nlen_prefix_infix_regex(nlen: int, prefix: str, infix: str) -> re.Pattern[str]:
-    """Two OID-like groups with literal prefix and infix. Port of oid_nlen_prefix_infix_regex."""
-    rx = oid_nlen_regex(nlen)
-    return re.compile(f"^{re.escape(prefix)}{rx.pattern}{re.escape(infix)}{rx.pattern}$")
+	"""Two OID-like groups with literal prefix and infix. Port of oid_nlen_prefix_infix_regex."""
+	rx = oid_nlen_regex(nlen)
+	return re.compile(f"^{re.escape(prefix)}{rx.pattern}{re.escape(infix)}{rx.pattern}$")
 
 
 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("/")
-    for p in parts:
-        if p in ("", ".", ".."):
-            return False
-    return True
+	"""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("/")
+	for p in parts:
+		if p in ("", ".", ".."):
+			return False
+	return True
 
 
 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, ~^:?*[
-    if re.search(r"[\x00-\x20\x7f ~^:?*\[\]]", input_ref):
-        return False
-    return True
+	"""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, ~^:?*[
+	if re.search(r"[\x00-\x20\x7f ~^:?*\[\]]", input_ref):
+		return False
+	return True
 
 
 def is_valid_refname(input_ref: str | None) -> bool:
-    """Either full OID hex or valid pathname + ref format. Port of is_valid_refname."""
-    if input_ref is None:
-        return False
-    if OID_PATTERN.match(input_ref):
-        return True
-    return is_valid_pathname(input_ref) and is_valid_ref_format(input_ref)
+	"""Either full OID hex or valid pathname + ref format. Port of is_valid_refname."""
+	if input_ref is None:
+		return False
+	if OID_PATTERN.match(input_ref):
+		return True
+	return is_valid_pathname(input_ref) and is_valid_ref_format(input_ref)
 
 
 def check_export_ok(
-    git_dir: str,
-    export_ok: str = "",
-    export_auth_hook: Callable[[str], bool] | None = None,
-    across_fs: bool = False,
+	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
-    if export_auth_hook is not None and not export_auth_hook(git_dir):
-        return False
-    return True
+	"""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
+	if export_auth_hook is not None and not export_auth_hook(git_dir):
+		return False
+	return True
 
 
 def is_valid_action(action: str | None, allowed_actions: set[str]) -> bool:
-    """Action is in allowed set. Port of is_valid_action."""
-    return action in allowed_actions if action else False
+	"""Action is in allowed set. Port of is_valid_action."""
+	return action in allowed_actions if action else False
 
 
 def is_valid_project(
-    project: str | None,
-    projectroot: str,
-    export_ok: str,
-    strict_export: bool,
-    project_in_list: Callable[[str], bool],
+	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
-    if strict_export and not project_in_list(project):
-        return False
-    return True
+	"""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
+	if strict_export and not project_in_list(project):
+		return False
+	return True
diff --git a/pyproject.toml b/pyproject.toml
index f257dbe..f38f3e4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,3 +13,27 @@ members = ["postgit", "distgit", "pygitweb"]
 
 [tool.uv]
 package = false
+
+[tool.ruff]
+line-length = 120
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "tab"
+docstring-code-format = true
+
+[tool.ruff.lint]
+select = [
+    # pycodestyle
+    "E",
+    # Pyflakes
+    "F",
+    # pyupgrade
+    "UP",
+    # flake8-bugbear
+    "B",
+    # flake8-simplify
+    "SIM",
+    # isort
+    "I",
+]
