Fork of patx/hglab.

branch and bookmark support, work in progress

Commit 956e29e14adc · Harrison Erd · 2026-05-04 03:14 -0400

Changeset
956e29e14adc749957567b0c8da1955c2da81dfc

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -12,7 +12,7 @@
 import sqlite3
 import subprocess
 from pathlib import Path, PurePosixPath
-from urllib.parse import quote, urlparse
+from urllib.parse import quote, unquote, urlencode, urlparse
 
 import bleach
 import markdown
@@ -40,6 +40,11 @@
 PASSWORD_ITERATIONS = 260_000
 SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9._-]{1,62}$")
 REV_RE = re.compile(r"^(null|[0-9a-fA-F]{1,40})$")
+REF_TYPE_BRANCH = "branch"
+REF_TYPE_BOOKMARK = "bookmark"
+REF_TYPE_TIP = "tip"
+REF_TYPES = {REF_TYPE_BRANCH, REF_TYPE_BOOKMARK, REF_TYPE_TIP}
+REF_VALUE_SEPARATOR = "|"
 SCRIPT_STYLE_RE = re.compile(r"(?is)<(script|style)\b[^>]*>.*?</\1>")
 RESERVED_USERNAMES = {"dashboard", "favicon.ico", "hg", "login", "logout", "new", "settings", "signup", "static", "harrisonerd"}
 WRITE_HG_COMMANDS = {"pushkey", "unbundle"}
@@ -228,6 +233,10 @@
                 status TEXT NOT NULL DEFAULT 'open',
                 base_node TEXT NOT NULL,
                 source_node TEXT NOT NULL,
+                target_ref_type TEXT NOT NULL DEFAULT '',
+                target_ref_name TEXT NOT NULL DEFAULT '',
+                source_ref_type TEXT NOT NULL DEFAULT '',
+                source_ref_name TEXT NOT NULL DEFAULT '',
                 created_at TEXT NOT NULL,
                 updated_at TEXT NOT NULL,
                 closed_at TEXT,
@@ -259,6 +268,7 @@
         )
         ensure_user_profile_columns(conn)
         ensure_repository_collaboration_columns(conn)
+        ensure_pull_request_ref_columns(conn)
 
 
 def ensure_user_profile_columns(conn):
@@ -289,6 +299,19 @@
     conn.execute("CREATE INDEX IF NOT EXISTS idx_repositories_forked_from ON repositories(forked_from_repo_id)")
 
 
+def ensure_pull_request_ref_columns(conn):
+    columns = {row["name"] for row in conn.execute("PRAGMA table_info(pull_requests)")}
+    ref_columns = {
+        "target_ref_type": "ALTER TABLE pull_requests ADD COLUMN target_ref_type TEXT NOT NULL DEFAULT ''",
+        "target_ref_name": "ALTER TABLE pull_requests ADD COLUMN target_ref_name TEXT NOT NULL DEFAULT ''",
+        "source_ref_type": "ALTER TABLE pull_requests ADD COLUMN source_ref_type TEXT NOT NULL DEFAULT ''",
+        "source_ref_name": "ALTER TABLE pull_requests ADD COLUMN source_ref_name TEXT NOT NULL DEFAULT ''",
+    }
+    for name, ddl in ref_columns.items():
+        if name not in columns:
+            conn.execute(ddl)
+
+
 def normalize_slug(value, label):
     slug = (value or "").strip().lower()
     if not SLUG_RE.match(slug):
@@ -367,6 +390,8 @@
     context.setdefault("notice", None)
     context.setdefault("render_markdown_links", render_markdown_links)
     context.setdefault("render_repo_description", render_repo_description)
+    context.setdefault("format_ref_label", format_ref_label)
+    context.setdefault("url_with_ref", url_with_ref)
     return template(template_name, **context)
 
 
@@ -690,7 +715,7 @@
     forked_from_node = (
         source_repo["forked_from_node"]
         if source_repo["forked_from_repo_id"] and source_repo["forked_from_node"]
-        else repo_tip_node(source_path) or NULL_REV
+        else default_code_ref(source_path).get("node") or NULL_REV
     )
 
     with db_connect() as conn:
@@ -769,8 +794,10 @@
     sync_repo_hgrc(repo)
 
 
-def hg_files(path):
-    completed = run_hg(["files", "-r", "tip"], cwd=path, check=False)
+def hg_files(path, revision="tip"):
+    if not revision:
+        return []
+    completed = run_hg(["files", "-r", revision], cwd=path, check=False)
     if completed.returncode != 0:
         stderr = (completed.stderr or "").lower()
         stdout = completed.stdout or ""
@@ -782,14 +809,16 @@
     return [line.strip() for line in completed.stdout.splitlines() if line.strip()]
 
 
-def hg_cat(path, file_path, text=True):
-    completed = run_hg(["cat", "-r", "tip", file_path], cwd=path, check=True, text=text)
+def hg_cat(path, file_path, revision="tip", text=True):
+    completed = run_hg(["cat", "-r", revision, file_path], cwd=path, check=True, text=text)
     return completed.stdout if text else completed.stdout
 
 
-def read_file_bytes(path, file_path):
+def read_file_bytes(path, file_path, revision="tip"):
+    if not revision:
+        raise HgCommandError("File not found.")
     completed = subprocess.run(
-        ["hg", "cat", "-r", "tip", file_path],
+        ["hg", "cat", "-r", revision, file_path],
         cwd=path,
         capture_output=True,
         timeout=15,
@@ -819,13 +848,13 @@
     return sorted(entries.values(), key=lambda item: (item["type"] != "dir", item["name"].lower()))
 
 
-def readme_for_repo(path, files):
+def readme_for_repo(path, files, revision="tip"):
     by_lower = {file_path.lower(): file_path for file_path in files}
     for candidate in README_CANDIDATES:
         actual = by_lower.get(candidate.lower())
         if actual:
             try:
-                return actual, hg_cat(path, actual)
+                return actual, hg_cat(path, actual, revision=revision)
             except HgCommandError:
                 return actual, ""
     return None, None
@@ -877,9 +906,18 @@
     return render_markdown_links(text)
 
 
-def commit_log(path, limit=50):
+def ancestors_revset(node):
+    return f"ancestors({validate_revision_id(node, allow_null=False)})"
+
+
+def commit_log(path, limit=50, revision=None):
+    if revision == NULL_REV:
+        return []
     template_arg = "{rev}\\x1f{node|short}\\x1f{author|person}\\x1f{date|isodate}\\x1f{desc|firstline}\\x1e"
-    completed = run_hg(["log", "-l", str(limit), "--template", template_arg], cwd=path, check=False)
+    args = ["log", "-l", str(limit), "--template", template_arg]
+    if revision:
+        args[1:1] = ["-r", ancestors_revset(revision)]
+    completed = run_hg(args, cwd=path, check=False)
     if completed.returncode != 0:
         stderr = completed.stderr.lower()
         if "empty" in stderr or "no changes found" in stderr:
@@ -936,8 +974,302 @@
     return tags
 
 
-def commit_count(path):
-    completed = run_hg(["log", "--template", "."], cwd=path, check=False)
+def hg_bool(value):
+    return str(value).strip().lower() in {"1", "true", "yes", "on"}
+
+
+def revision_info(path, revision):
+    if not revision:
+        return None
+    template_arg = (
+        "{rev}\\x1f{node}\\x1f{node|short}\\x1f{branch}\\x1f{date|isodate}\\x1f{desc|firstline}\\x1e"
+    )
+    completed = run_hg(["log", "-r", revision, "--template", template_arg], cwd=path, check=False)
+    if completed.returncode != 0 or not completed.stdout:
+        return None
+    parts = completed.stdout.rstrip("\x1e").split("\x1f")
+    if len(parts) != 6:
+        return None
+    return {
+        "rev": parts[0],
+        "node": parts[1],
+        "short_node": parts[2],
+        "branch": parts[3],
+        "date": parts[4],
+        "summary": parts[5],
+    }
+
+
+def empty_tip_ref(is_default=False):
+    return {
+        "type": REF_TYPE_TIP,
+        "name": "",
+        "label": "tip",
+        "rev": "",
+        "node": None,
+        "short_node": "",
+        "branch": "",
+        "date": "",
+        "summary": "",
+        "active": False,
+        "closed": False,
+        "is_default": is_default,
+    }
+
+
+def tip_ref(path, is_default=False):
+    info = revision_info(path, "tip")
+    if not info:
+        return empty_tip_ref(is_default=is_default)
+    info.update(
+        {
+            "type": REF_TYPE_TIP,
+            "name": "",
+            "label": "tip",
+            "active": False,
+            "closed": False,
+            "is_default": is_default,
+        }
+    )
+    return info
+
+
+def list_repo_branches(path):
+    template_arg = (
+        "{branch}\\x1f{node}\\x1f{node|short}\\x1f{rev}\\x1f{active}\\x1f{closed}\\x1f"
+        "{date|isodate}\\x1f{desc|firstline}\\x1e"
+    )
+    completed = run_hg(["branches", "-c", "--template", template_arg], cwd=path, check=False)
+    if completed.returncode != 0:
+        stderr = (completed.stderr or "").lower()
+        if "empty" in stderr or "no changes found" in stderr or "repo has no changesets" in stderr:
+            return []
+        raise HgCommandError(completed.stderr.strip() or "Unable to read repository branches.", completed.returncode)
+
+    branches = []
+    for record in completed.stdout.split("\x1e"):
+        if not record:
+            continue
+        parts = record.split("\x1f")
+        if len(parts) != 8:
+            continue
+        branches.append(
+            {
+                "type": REF_TYPE_BRANCH,
+                "name": parts[0],
+                "label": f"branch {parts[0]}",
+                "node": parts[1],
+                "short_node": parts[2],
+                "rev": parts[3],
+                "active": hg_bool(parts[4]),
+                "closed": hg_bool(parts[5]),
+                "date": parts[6],
+                "summary": parts[7],
+                "is_default": False,
+            }
+        )
+    return branches
+
+
+def list_repo_bookmarks(path):
+    template_arg = (
+        "{bookmark}\\x1f{node}\\x1f{node|short}\\x1f{rev}\\x1f{active}\\x1f"
+        "{date|isodate}\\x1f{desc|firstline}\\x1e"
+    )
+    completed = run_hg(["bookmarks", "--template", template_arg], cwd=path, check=False)
+    if completed.returncode != 0:
+        stderr = (completed.stderr or "").lower()
+        if "empty" in stderr or "no changes found" in stderr or "repo has no changesets" in stderr:
+            return []
+        raise HgCommandError(completed.stderr.strip() or "Unable to read repository bookmarks.", completed.returncode)
+
+    bookmarks = []
+    for record in completed.stdout.split("\x1e"):
+        if not record:
+            continue
+        parts = record.split("\x1f")
+        if len(parts) != 7:
+            continue
+        bookmarks.append(
+            {
+                "type": REF_TYPE_BOOKMARK,
+                "name": parts[0],
+                "label": f"bookmark {parts[0]}",
+                "node": parts[1],
+                "short_node": parts[2],
+                "rev": parts[3],
+                "active": hg_bool(parts[4]),
+                "closed": False,
+                "date": parts[5],
+                "summary": parts[6],
+                "is_default": False,
+            }
+        )
+    return bookmarks
+
+
+def default_code_ref(path):
+    for branch in list_repo_branches(path):
+        if branch["name"] == "default" and not branch["closed"]:
+            selected = dict(branch)
+            selected["is_default"] = True
+            return selected
+    return tip_ref(path, is_default=True)
+
+
+def resolve_repo_ref(path, ref_type, ref_name=""):
+    ref_type = (ref_type or "").strip().lower()
+    ref_name = ref_name or ""
+    if ref_type == REF_TYPE_TIP:
+        return tip_ref(path)
+    if ref_type == REF_TYPE_BRANCH:
+        for branch in list_repo_branches(path):
+            if branch["name"] == ref_name:
+                return dict(branch)
+        raise ValueError("Branch not found.")
+    if ref_type == REF_TYPE_BOOKMARK:
+        for bookmark in list_repo_bookmarks(path):
+            if bookmark["name"] == ref_name:
+                return dict(bookmark)
+        raise ValueError("Bookmark not found.")
+    raise ValueError("Ref not found.")
+
+
+def ref_option_value(ref_type, ref_name=""):
+    return REF_VALUE_SEPARATOR.join((ref_type, quote(ref_name or "", safe="")))
+
+
+def source_ref_option_value(repo_id, ref_type, ref_name=""):
+    return REF_VALUE_SEPARATOR.join((str(repo_id), ref_type, quote(ref_name or "", safe="")))
+
+
+def parse_ref_option_value(value):
+    parts = (value or "").split(REF_VALUE_SEPARATOR, 1)
+    if len(parts) != 2 or parts[0] not in REF_TYPES:
+        raise ValueError("Invalid ref.")
+    return parts[0], unquote(parts[1])
+
+
+def parse_source_ref_option_value(value):
+    parts = (value or "").split(REF_VALUE_SEPARATOR, 2)
+    if len(parts) != 3 or parts[1] not in REF_TYPES:
+        raise ValueError("Invalid source ref.")
+    try:
+        repo_id = int(parts[0])
+    except ValueError as exc:
+        raise ValueError("Invalid source repository.") from exc
+    return repo_id, parts[1], unquote(parts[2])
+
+
+def selected_repo_ref(path):
+    ref_value = request.query.get("ref_value")
+    if ref_value:
+        try:
+            ref_type, ref_name = parse_ref_option_value(ref_value)
+            return resolve_repo_ref(path, ref_type, ref_name)
+        except ValueError as exc:
+            abort(404, str(exc))
+
+    ref_type = (request.query.get("ref_type") or "").strip().lower()
+    if not ref_type:
+        return default_code_ref(path)
+    if ref_type not in REF_TYPES:
+        abort(404, "Ref not found.")
+    try:
+        return resolve_repo_ref(path, ref_type, request.query.get("ref") or "")
+    except ValueError as exc:
+        abort(404, str(exc))
+
+
+def ref_query_string(ref_info, force=False):
+    if not ref_info or (ref_info.get("is_default") and not force):
+        return ""
+    ref_type = ref_info.get("type") or REF_TYPE_TIP
+    params = {"ref_type": ref_type}
+    if ref_type != REF_TYPE_TIP:
+        params["ref"] = ref_info.get("name", "")
+    return urlencode(params)
+
+
+def url_with_ref(url, ref_info=None, force=False):
+    query = ref_query_string(ref_info, force=force)
+    if not query:
+        return url
+    separator = "&" if "?" in url else "?"
+    return f"{url}{separator}{query}"
+
+
+def format_ref_label(ref_type, ref_name=""):
+    ref_type = (ref_type or REF_TYPE_TIP).strip().lower()
+    if ref_type == REF_TYPE_BRANCH:
+        return f"branch {ref_name}"
+    if ref_type == REF_TYPE_BOOKMARK:
+        return f"bookmark {ref_name}"
+    return "tip"
+
+
+def ref_option_label(ref):
+    label = format_ref_label(ref["type"], ref.get("name", ""))
+    if ref["type"] == REF_TYPE_BRANCH and ref.get("closed"):
+        label += " (closed)"
+    return label
+
+
+def ref_option_from_ref(ref, repo_id=None):
+    if repo_id is None:
+        value = ref_option_value(ref["type"], ref.get("name", ""))
+    else:
+        value = source_ref_option_value(repo_id, ref["type"], ref.get("name", ""))
+    return {
+        "value": value,
+        "label": ref_option_label(ref),
+        "ref": ref,
+    }
+
+
+def repo_ref_options(path, include_closed_branches=True, include_tip=True):
+    branches = list_repo_branches(path)
+    bookmarks = list_repo_bookmarks(path)
+    options = []
+    for branch in branches:
+        if include_closed_branches or not branch["closed"]:
+            options.append(ref_option_from_ref(branch))
+    for bookmark in bookmarks:
+        options.append(ref_option_from_ref(bookmark))
+    if include_tip:
+        options.append(ref_option_from_ref(tip_ref(path)))
+    return options
+
+
+def source_repo_ref_options(source_repo):
+    path = repo_path(source_repo["owner_username"], source_repo["name"])
+    options = repo_ref_options(path, include_closed_branches=True, include_tip=True)
+    for option in options:
+        option["value"] = source_ref_option_value(
+            source_repo["id"],
+            option["ref"]["type"],
+            option["ref"].get("name", ""),
+        )
+        option["label"] = f"{source_repo['owner_username']}/{source_repo['name']} {option['label']}"
+    return options
+
+
+def target_repo_ref_options(path):
+    return repo_ref_options(path, include_closed_branches=False, include_tip=True)
+
+
+def revision_branch(path, node):
+    info = revision_info(path, node)
+    return info["branch"] if info else ""
+
+
+def commit_count(path, revision=None):
+    if revision == NULL_REV:
+        return 0
+    args = ["log", "--template", "."]
+    if revision:
+        args[1:1] = ["-r", ancestors_revset(revision)]
+    completed = run_hg(args, cwd=path, check=False)
     if completed.returncode != 0:
         stderr = (completed.stderr or "").lower()
         if "empty" in stderr or "no changes found" in stderr:
@@ -1162,30 +1494,60 @@
         ).fetchone()
 
 
-def pull_request_base_node(target_repo, source_repo):
-    target_path = repo_path(target_repo["owner_username"], target_repo["name"])
+def stored_ref_name(ref):
+    return "" if ref["type"] == REF_TYPE_TIP else ref.get("name", "")
+
+
+def pr_ref_type(pr, prefix):
+    return pr[f"{prefix}_ref_type"] or REF_TYPE_TIP
+
+
+def pr_ref_name(pr, prefix):
+    return pr[f"{prefix}_ref_name"] or ""
+
+
+def resolve_pr_ref(path, pr, prefix):
+    return resolve_repo_ref(path, pr_ref_type(pr, prefix), pr_ref_name(pr, prefix))
+
+
+def pull_request_base_node(target_repo, source_repo, target_ref):
     source_path = repo_path(source_repo["owner_username"], source_repo["name"])
-    target_tip = repo_tip_node(target_path) or NULL_REV
-    if repo_has_revision(source_path, target_tip):
-        return target_tip
+    target_node = target_ref.get("node") or NULL_REV
+    if repo_has_revision(source_path, target_node):
+        return target_node
     fork_base = source_repo["forked_from_node"] or NULL_REV
     if repo_has_revision(source_path, fork_base):
         return fork_base
     return NULL_REV
 
 
-def create_pull_request(target_repo, source_repo, author, title, body):
+def create_pull_request(
+    target_repo,
+    source_repo,
+    author,
+    title,
+    body,
+    source_ref_type,
+    source_ref_name,
+    target_ref_type,
+    target_ref_name,
+):
     if not source_repo:
         raise ValueError("Choose a source fork.")
     if source_repo["id"] == target_repo["id"]:
         raise ValueError("Choose a fork as the source repository.")
     if source_repo["owner_id"] != author["id"] or source_repo["forked_from_repo_id"] != target_repo["id"]:
         raise ValueError("Choose one of your forks of this repository.")
+    target_path = repo_path(target_repo["owner_username"], target_repo["name"])
     source_path = repo_path(source_repo["owner_username"], source_repo["name"])
-    source_node = repo_tip_node(source_path)
+    source_ref = resolve_repo_ref(source_path, source_ref_type, source_ref_name)
+    target_ref = resolve_repo_ref(target_path, target_ref_type, target_ref_name)
+    if target_ref["type"] == REF_TYPE_BRANCH and target_ref.get("closed"):
+        raise ValueError("Choose an open target branch.")
+    source_node = source_ref.get("node")
     if not source_node:
         raise ValueError("Source repository has no commits.")
-    base_node = pull_request_base_node(target_repo, source_repo)
+    base_node = pull_request_base_node(target_repo, source_repo, target_ref)
     now = utcnow()
     with db_connect() as conn:
         number = conn.execute(
@@ -1196,9 +1558,10 @@
             """
             INSERT INTO pull_requests (
                 target_repo_id, source_repo_id, author_id, number, title, body,
-                base_node, source_node, created_at, updated_at
+                base_node, source_node, target_ref_type, target_ref_name,
+                source_ref_type, source_ref_name, created_at, updated_at
             )
-            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
             """,
             (
                 target_repo["id"],
@@ -1209,6 +1572,10 @@
                 body[:5000],
                 base_node,
                 source_node,
+                target_ref["type"],
+                stored_ref_name(target_ref),
+                source_ref["type"],
+                stored_ref_name(source_ref),
                 now,
                 now,
             ),
@@ -1221,13 +1588,17 @@
     if not source_repo:
         raise HgCommandError("Source repository no longer exists.")
     source_path = repo_path(source_repo["owner_username"], source_repo["name"])
-    source_node = repo_tip_node(source_path)
+    try:
+        source_ref = resolve_pr_ref(source_path, pr, "source")
+    except ValueError as exc:
+        raise HgCommandError(str(exc)) from exc
+    source_node = source_ref.get("node")
     if not source_node:
         raise HgCommandError("Source repository has no commits.")
     base_node = pr["base_node"] or NULL_REV
     if not repo_has_revision(source_path, base_node):
         raise HgCommandError("The pull request base revision is not present in the source repository.")
-    return diff_between_revisions(source_path, base_node, source_node), source_node
+    return diff_between_revisions(source_path, base_node, source_node), source_node, source_ref
 
 
 def pull_source_into_target(target_path, source_path):
@@ -1248,21 +1619,34 @@
 
     target_path = repo_path(target_repo["owner_username"], target_repo["name"])
     source_path = repo_path(source_repo["owner_username"], source_repo["name"])
-    source_node = repo_tip_node(source_path)
+    try:
+        source_ref = resolve_pr_ref(source_path, pr, "source")
+        target_ref = resolve_pr_ref(target_path, pr, "target")
+    except ValueError as exc:
+        raise ValueError(str(exc)) from exc
+    if target_ref["type"] == REF_TYPE_BRANCH and target_ref.get("closed"):
+        raise ValueError("The target branch is closed.")
+    source_node = source_ref.get("node")
     if not source_node:
         raise ValueError("Source repository has no commits.")
 
     ensure_clean_working_copy(target_path)
-    target_node_before = repo_tip_node(target_path) or NULL_REV
+    target_node_before = target_ref.get("node") or NULL_REV
+    source_branch = revision_branch(source_path, source_node)
     run_hg(["update", "-C", target_node_before], cwd=target_path, check=False)
     pull_source_into_target(target_path, source_path)
     if not repo_has_revision(target_path, source_node):
         raise HgCommandError("Source revision was not pulled into the target repository.")
 
+    can_fast_forward = (
+        target_ref["type"] != REF_TYPE_BRANCH
+        or target_node_before == NULL_REV
+        or source_branch == target_ref["name"]
+    )
     if target_node_before != NULL_REV and is_ancestor(target_path, source_node, target_node_before):
         run_hg(["update", "-C", target_node_before], cwd=target_path, check=False)
         merge_node = target_node_before
-    elif is_ancestor(target_path, target_node_before, source_node):
+    elif can_fast_forward and is_ancestor(target_path, target_node_before, source_node):
         run_hg(["update", "-C", source_node], cwd=target_path, check=False)
         merge_node = source_node
     else:
@@ -1286,6 +1670,9 @@
             raise HgCommandError(commit.stderr.strip() or "Unable to create merge commit.", commit.returncode)
         merge_node = repo_tip_node(target_path) or source_node
 
+    if target_ref["type"] == REF_TYPE_BOOKMARK and merge_node != NULL_REV:
+        run_hg(["bookmark", "-f", "-r", merge_node, target_ref["name"]], cwd=target_path, check=True)
+
     now = utcnow()
     with db_connect() as conn:
         conn.execute(
@@ -1313,8 +1700,9 @@
     diff = ""
     diff_error = None
     current_source_node = pr["source_node"]
+    current_source_ref = None
     try:
-        diff, current_source_node = pull_request_diff(pr)
+        diff, current_source_node, current_source_ref = pull_request_diff(pr)
     except HgCommandError as exc:
         diff_error = str(exc)
     return render(
@@ -1326,6 +1714,7 @@
         diff=diff,
         diff_error=diff_error,
         current_source_node=current_source_node,
+        current_source_ref=current_source_ref,
         error=error,
         notice=notice,
         **repo_page_context(repo, path),
@@ -1351,14 +1740,16 @@
     return user_owns_repo(user, repo) or user_contributes_to_repo(user, repo)
 
 
-def repo_page_context(repo, path=None):
+def repo_page_context(repo, path=None, selected_ref=None):
     if path is None:
         path = repo_path(repo["owner_username"], repo["name"])
     user = current_user()
+    if selected_ref is None:
+        selected_ref = selected_repo_ref(path)
     fork_target_id = repo["forked_from_repo_id"] or repo["id"]
     source_repo = get_repo_by_id(repo["forked_from_repo_id"]) if repo["forked_from_repo_id"] else None
     return {
-        "commit_count": commit_count(path),
+        "commit_count": commit_count(path, selected_ref.get("node") if selected_ref else None),
         "issue_counts": issue_counts(repo["id"]),
         "pr_counts": pull_request_counts(repo["id"]),
         "star_count": repo_star_count(repo["id"]),
@@ -1368,6 +1759,14 @@
         "has_fork": bool(user and user_has_fork_for_target(user["id"], fork_target_id)),
         "repo_active_tab": repo_active_tab(repo),
         "source_repo": source_repo,
+        "selected_ref": selected_ref,
+        "ref_options": repo_ref_options(path),
+        "selected_ref_value": ref_option_value(
+            selected_ref.get("type", REF_TYPE_TIP),
+            selected_ref.get("name", ""),
+        )
+        if selected_ref
+        else "",
     }
 
 
@@ -1380,6 +1779,8 @@
         ("source", "/src"),
         ("commits", "/commits"),
         ("tags", "/tags"),
+        ("branches", "/branches"),
+        ("bookmarks", "/bookmarks"),
         ("issues", "/issues"),
         ("pulls", "/pulls"),
         ("settings", "/settings"),
@@ -1628,10 +2029,11 @@
     if not repo:
         abort(404, "Repository not found.")
     path = repo_path(owner, repo_name)
-    files = hg_files(path)
-    readme_name, readme = readme_for_repo(path, files)
+    selected_ref = selected_repo_ref(path)
+    files = hg_files(path, selected_ref.get("node"))
+    readme_name, readme = readme_for_repo(path, files, revision=selected_ref.get("node"))
     readme_is_markdown = is_markdown_file(readme_name)
-    context = repo_page_context(repo, path)
+    context = repo_page_context(repo, path, selected_ref=selected_ref)
     return render(
         "repo.tpl",
         repo=repo,
@@ -1772,10 +2174,11 @@
         abort(404, "Repository not found.")
     file_path = clean_repo_path(file_path)
     path = repo_path(owner, repo_name)
-    files = hg_files(path)
+    selected_ref = selected_repo_ref(path)
+    files = hg_files(path, selected_ref.get("node"))
 
     if file_path in files:
-        content = read_file_bytes(path, file_path)
+        content = read_file_bytes(path, file_path, revision=selected_ref.get("node"))
         is_binary = b"\0" in content[:4096]
         return render(
             "file.tpl",
@@ -1786,7 +2189,7 @@
             language_class=highlight_language_class(file_path),
             size=len(content),
             quote_path=quote_path,
-            **repo_page_context(repo, path),
+            **repo_page_context(repo, path, selected_ref=selected_ref),
         )
 
     if file_path and not any(item.startswith(file_path + "/") for item in files):
@@ -1798,7 +2201,7 @@
         current_path=file_path,
         entries=build_tree(files, file_path),
         quote_path=quote_path,
-        **repo_page_context(repo, path),
+        **repo_page_context(repo, path, selected_ref=selected_ref),
     )
 
 
@@ -1809,10 +2212,11 @@
         abort(404, "Repository not found.")
     file_path = clean_repo_path(file_path)
     path = repo_path(owner, repo_name)
-    files = hg_files(path)
+    selected_ref = selected_repo_ref(path)
+    files = hg_files(path, selected_ref.get("node"))
     if file_path not in files:
         abort(404, "File not found.")
-    content = read_file_bytes(path, file_path)
+    content = read_file_bytes(path, file_path, revision=selected_ref.get("node"))
     content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
     return HTTPResponse(content, content_type=content_type)
 
@@ -1823,7 +2227,13 @@
     if not repo:
         abort(404, "Repository not found.")
     path = repo_path(owner, repo_name)
-    return render("commits.tpl", repo=repo, commits=commit_log(path), **repo_page_context(repo, path))
+    selected_ref = selected_repo_ref(path)
+    return render(
+        "commits.tpl",
+        repo=repo,
+        commits=commit_log(path, revision=selected_ref.get("node")),
+        **repo_page_context(repo, path, selected_ref=selected_ref),
+    )
 
 
 @app.route("/<owner>/<repo_name>/tags")
@@ -1841,6 +2251,36 @@
     )
 
 
[email protected]("/<owner>/<repo_name>/branches")
+def repo_branches(owner, repo_name):
+    repo = get_repo(owner, repo_name)
+    if not repo:
+        abort(404, "Repository not found.")
+    path = repo_path(owner, repo_name)
+    return render(
+        "branches.tpl",
+        repo=repo,
+        branches=list_repo_branches(path),
+        clone_url=clone_url(owner, repo_name),
+        **repo_page_context(repo, path),
+    )
+
+
[email protected]("/<owner>/<repo_name>/bookmarks")
+def repo_bookmarks(owner, repo_name):
+    repo = get_repo(owner, repo_name)
+    if not repo:
+        abort(404, "Repository not found.")
+    path = repo_path(owner, repo_name)
+    return render(
+        "bookmarks.tpl",
+        repo=repo,
+        bookmarks=list_repo_bookmarks(path),
+        clone_url=clone_url(owner, repo_name),
+        **repo_page_context(repo, path),
+    )
+
+
 @app.route("/<owner>/<repo_name>/commits/<node>")
 def repo_commit(owner, repo_name, node):
     repo = get_repo(owner, repo_name)
@@ -1883,15 +2323,40 @@
         abort(404, "Repository not found.")
     path = repo_path(owner, repo_name)
     forks = list_user_forks_for_target(user["id"], repo["id"])
-    selected_source_id = request.forms.get("source_repo_id") if request.method == "POST" else request.query.get("source_repo_id")
+    source_options = []
+    for fork in forks:
+        source_options.extend(source_repo_ref_options(fork))
+    target_options = target_repo_ref_options(path)
+    selected_source_ref = request.forms.get("source_ref") if request.method == "POST" else request.query.get("source_ref")
+    selected_target_ref = request.forms.get("target_ref") if request.method == "POST" else request.query.get("target_ref")
+    if not selected_source_ref and source_options:
+        selected_source_ref = source_options[0]["value"]
+    if not selected_target_ref:
+        default_target = default_code_ref(path)
+        selected_target_ref = ref_option_value(default_target["type"], default_target.get("name", ""))
+    if selected_target_ref and selected_target_ref not in {option["value"] for option in target_options} and target_options:
+        selected_target_ref = target_options[0]["value"]
     title_value = request.forms.get("title", "") if request.method == "POST" else ""
     body_value = request.forms.get("body", "") if request.method == "POST" else ""
 
     if request.method == "POST":
         try:
-            source_repo_id = int(selected_source_id or "")
-        except ValueError:
-            source_repo_id = 0
+            source_repo_id, source_ref_type, source_ref_name = parse_source_ref_option_value(selected_source_ref)
+            target_ref_type, target_ref_name = parse_ref_option_value(selected_target_ref)
+        except ValueError as exc:
+            return render(
+                "new_pull_request.tpl",
+                repo=repo,
+                forks=forks,
+                source_options=source_options,
+                target_options=target_options,
+                selected_source_ref=selected_source_ref,
+                selected_target_ref=selected_target_ref,
+                title_value=title_value,
+                body_value=body_value,
+                error=str(exc),
+                **repo_page_context(repo, path),
+            )
         source_repo = get_repo_by_id(source_repo_id) if source_repo_id else None
         title = title_value.strip()
         body = body_value.strip()
@@ -1900,36 +2365,51 @@
                 "new_pull_request.tpl",
                 repo=repo,
                 forks=forks,
-                selected_source_id=source_repo_id,
+                source_options=source_options,
+                target_options=target_options,
+                selected_source_ref=selected_source_ref,
+                selected_target_ref=selected_target_ref,
                 title_value=title_value,
                 body_value=body_value,
                 error="Pull request title is required.",
                 **repo_page_context(repo, path),
             )
         try:
-            number = create_pull_request(repo, source_repo, user, title, body)
+            number = create_pull_request(
+                repo,
+                source_repo,
+                user,
+                title,
+                body,
+                source_ref_type,
+                source_ref_name,
+                target_ref_type,
+                target_ref_name,
+            )
             redirect(f"/{owner}/{repo_name}/pulls/{number}")
         except (ValueError, HgCommandError) as exc:
             return render(
                 "new_pull_request.tpl",
                 repo=repo,
                 forks=forks,
-                selected_source_id=source_repo_id,
+                source_options=source_options,
+                target_options=target_options,
+                selected_source_ref=selected_source_ref,
+                selected_target_ref=selected_target_ref,
                 title_value=title_value,
                 body_value=body_value,
                 error=str(exc),
                 **repo_page_context(repo, path),
             )
 
-    try:
-        selected_source_id = int(selected_source_id or (forks[0]["id"] if forks else 0))
-    except ValueError:
-        selected_source_id = forks[0]["id"] if forks else 0
     return render(
         "new_pull_request.tpl",
         repo=repo,
         forks=forks,
-        selected_source_id=selected_source_id,
+        source_options=source_options,
+        target_options=target_options,
+        selected_source_ref=selected_source_ref,
+        selected_target_ref=selected_target_ref,
         title_value=title_value,
         body_value=body_value,
         **repo_page_context(repo, path),
diff --git a/static/styles.css b/static/styles.css
--- a/static/styles.css
+++ b/static/styles.css
@@ -34,7 +34,7 @@
   border-bottom: 1px solid #ddd;
 }
 
-.site-header, .nav, .repo-tabs, .filters, .tabs, .panel-heading, .hero-actions, .breadcrumb, .repo-actions {
+.site-header, .nav, .repo-tabs, .filters, .tabs, .panel-heading, .hero-actions, .breadcrumb, .repo-actions, .ref-actions {
   display: flex;
   gap: .75rem;
   align-items: baseline;
@@ -47,6 +47,8 @@
 .link-button { padding: 0; color: #0645ad; background: none; border: 0; text-decoration: underline; }
 .repo-tabs .repo-tab { padding-bottom: .2rem; border-bottom: 2px solid transparent; }
 .repo-tabs .repo-tab.active { color: #111; font-weight: 700; text-decoration: none; }
+.ref-selector { display: flex; gap: .5rem; align-items: end; max-width: 28rem; margin-bottom: 1rem; }
+.ref-selector label { flex: 1; }
 .filters .active, .tabs .active { color: #111; font-weight: 700; text-decoration: none; }
 .hero, .repo-tabs { margin-bottom: 1.5rem; }
 .eyebrow, .muted, .empty, .nav-user, .repo-card small, .issue-list span, .commit-list span, .file-list span, .clean-list span, .file-kind { color: #666; }
diff --git a/templates/bookmarks.tpl b/templates/bookmarks.tpl
new file mode 100644
--- /dev/null
+++ b/templates/bookmarks.tpl
@@ -0,0 +1,34 @@
+% rebase("base.tpl", title=repo["owner_username"] + "/" + repo["name"] + " bookmarks", user=user, error=error, notice=notice)
+
+<section class="repo-header slim">
+  <div>
+    % include("repo_fork_eyebrow.tpl")
+    <h1><a href="/{{repo['owner_username']}}">{{repo["owner_username"]}}</a>/{{repo["name"]}}</h1>
+
+    % include("repo_nav.tpl", repo=repo, commit_count=commit_count, issue_counts=issue_counts, pr_counts=pr_counts, star_count=star_count, is_starred=is_starred, is_owner=is_owner, can_maintain=can_maintain)
+  </div>
+</section>
+
+<section class="panel">
+  % if bookmarks:
+    <ul class="commit-list">
+      % for bookmark in bookmarks:
+        <li>
+          <code>{{bookmark["name"]}}{{" *" if bookmark["active"] else ""}}</code>
+          <div>
+            <strong><a href="/{{repo['owner_username']}}/{{repo['name']}}/commits/{{bookmark['short_node']}}">{{bookmark["short_node"]}}</a></strong>
+            <small>rev {{bookmark["rev"]}} · {{bookmark["date"]}}</small>
+            <p>{{bookmark["summary"]}}</p>
+            <div class="ref-actions">
+              <a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/src', bookmark, True)}}">Browse code</a>
+              <a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/commits', bookmark, True)}}">Commits</a>
+              <a href="/hg/{{repo['owner_username']}}/{{repo['name']}}/archive/{{bookmark['short_node']}}.zip">Archive</a>
+            </div>
+          </div>
+        </li>
+      % end
+    </ul>
+  % else:
+    <p class="empty">No bookmarks yet.</p>
+  % end
+</section>
diff --git a/templates/branches.tpl b/templates/branches.tpl
new file mode 100644
--- /dev/null
+++ b/templates/branches.tpl
@@ -0,0 +1,34 @@
+% rebase("base.tpl", title=repo["owner_username"] + "/" + repo["name"] + " branches", user=user, error=error, notice=notice)
+
+<section class="repo-header slim">
+  <div>
+    % include("repo_fork_eyebrow.tpl")
+    <h1><a href="/{{repo['owner_username']}}">{{repo["owner_username"]}}</a>/{{repo["name"]}}</h1>
+
+    % include("repo_nav.tpl", repo=repo, commit_count=commit_count, issue_counts=issue_counts, pr_counts=pr_counts, star_count=star_count, is_starred=is_starred, is_owner=is_owner, can_maintain=can_maintain)
+  </div>
+</section>
+
+<section class="panel">
+  % if branches:
+    <ul class="commit-list">
+      % for branch in branches:
+        <li>
+          <code>{{branch["name"]}}</code>
+          <div>
+            <strong><a href="/{{repo['owner_username']}}/{{repo['name']}}/commits/{{branch['short_node']}}">{{branch["short_node"]}}</a></strong>
+            <small>rev {{branch["rev"]}} · {{branch["date"]}}{{" · closed" if branch["closed"] else ""}}</small>
+            <p>{{branch["summary"]}}</p>
+            <div class="ref-actions">
+              <a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/src', branch, True)}}">Browse code</a>
+              <a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/commits', branch, True)}}">Commits</a>
+              <a href="/hg/{{repo['owner_username']}}/{{repo['name']}}/archive/{{branch['short_node']}}.zip">Archive</a>
+            </div>
+          </div>
+        </li>
+      % end
+    </ul>
+  % else:
+    <p class="empty">No branches yet.</p>
+  % end
+</section>
diff --git a/templates/commits.tpl b/templates/commits.tpl
--- a/templates/commits.tpl
+++ b/templates/commits.tpl
@@ -7,6 +7,7 @@
     <h1><a href="/{{repo['owner_username']}}">{{repo["owner_username"]}}</a>/{{repo["name"]}}</h1>
     
     % include("repo_nav.tpl", repo=repo, commit_count=commit_count, issue_counts=issue_counts, pr_counts=pr_counts, star_count=star_count, is_starred=is_starred, is_owner=is_owner, can_maintain=can_maintain)
+    % include("ref_selector.tpl")
 
   </div>
 </section>
@@ -16,7 +17,7 @@
     <ul class="commit-list">
       % for commit in commits:
         <li>
-          <code><a href="/{{repo['owner_username']}}/{{repo['name']}}/commits/{{commit['node']}}">{{commit["node"]}}</a></code>
+          <code><a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/commits/' + commit['node'], selected_ref)}}">{{commit["node"]}}</a></code>
           <div>
             <strong>{{commit["summary"]}}</strong>
             <small>{{commit["author"]}} · {{commit["date"]}}</small>
diff --git a/templates/file.tpl b/templates/file.tpl
--- a/templates/file.tpl
+++ b/templates/file.tpl
@@ -6,21 +6,23 @@
     <h1><a href="/{{repo['owner_username']}}">{{repo["owner_username"]}}</a>/{{repo["name"]}}</h1>
     
     % include("repo_nav.tpl", repo=repo, commit_count=commit_count, issue_counts=issue_counts, pr_counts=pr_counts, star_count=star_count, is_starred=is_starred, is_owner=is_owner, can_maintain=can_maintain)
+    % include("ref_selector.tpl")
     
-  <div class="breadcrumb">
-    <a href="/{{repo['owner_username']}}/{{repo['name']}}/src">root</a>
-    % parts = file_path.split("/")
-    % running = ""
-    % for index, part in enumerate(parts):
-      % running = part if not running else running + "/" + part
-      <span>/</span>
-      % if index + 1 == len(parts):
-        <span>{{part}}</span>
-      % else:
-        <a href="/{{repo['owner_username']}}/{{repo['name']}}/src/{{quote_path(running)}}">{{part}}</a>
+    <div class="breadcrumb">
+      <a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/src', selected_ref)}}">root</a>
+      % parts = file_path.split("/")
+      % running = ""
+      % for index, part in enumerate(parts):
+        % running = part if not running else running + "/" + part
+        <span>/</span>
+        % if index + 1 == len(parts):
+          <span>{{part}}</span>
+        % else:
+          <a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/src/' + quote_path(running), selected_ref)}}">{{part}}</a>
+        % end
       % end
-    % end
-  <a href="/{{repo['owner_username']}}/{{repo['name']}}/raw/{{quote_path(file_path)}}">[View Raw]</a>
+      <a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/raw/' + quote_path(file_path), selected_ref)}}">[View Raw]</a>
+    </div>
   </div>
 </section>
 
diff --git a/templates/new_pull_request.tpl b/templates/new_pull_request.tpl
--- a/templates/new_pull_request.tpl
+++ b/templates/new_pull_request.tpl
@@ -11,14 +11,22 @@
 </section>
 
 <section class="panel">
-  % if forks:
+  % if forks and source_options:
     <h2>Open pull request</h2>
     <form method="post">
       <label>
-        Source fork
-        <select name="source_repo_id">
-          % for fork in forks:
-            <option value="{{fork['id']}}" {{"selected" if fork["id"] == selected_source_id else ""}}>{{fork["owner_username"]}}/{{fork["name"]}}</option>
+        Source ref
+        <select name="source_ref">
+          % for option in source_options:
+            <option value="{{option['value']}}" {{"selected" if option["value"] == selected_source_ref else ""}}>{{option["label"]}}</option>
+          % end
+        </select>
+      </label>
+      <label>
+        Target ref
+        <select name="target_ref">
+          % for option in target_options:
+            <option value="{{option['value']}}" {{"selected" if option["value"] == selected_target_ref else ""}}>{{option["label"]}}</option>
           % end
         </select>
       </label>
diff --git a/templates/pull_request_detail.tpl b/templates/pull_request_detail.tpl
--- a/templates/pull_request_detail.tpl
+++ b/templates/pull_request_detail.tpl
@@ -16,6 +16,7 @@
       <h2>(#{{pr["number"]}}) {{pr["title"]}}</h2>
       <p class="muted"><strong>{{pr["status"]}}</strong> <small>(created by <a href="/{{pr["author_username"]}}">{{pr["author_username"]}}</a> on {{pr["created_at"]}})</small></p>
       <p class="muted"><a href="/{{pr["source_owner_username"]}}/{{pr["source_repo_name"]}}">{{pr["source_owner_username"]}}/{{pr["source_repo_name"]}}</a> into <a href="/{{pr["target_owner_username"]}}/{{pr["target_repo_name"]}}">{{pr["target_owner_username"]}}/{{pr["target_repo_name"]}}</a></p>
+      <p class="muted">{{format_ref_label(pr["source_ref_type"], pr["source_ref_name"])}} into {{format_ref_label(pr["target_ref_type"], pr["target_ref_name"])}}</p>
     </div>
     % if can_maintain and pr["status"] == "open":
       <div class="filters">
@@ -70,7 +71,7 @@
 
 <section class="panel">
   <h2>Diff</h2>
-  <p class="muted">Base <code>{{pr["base_node"]}}</code> to source <code>{{current_source_node}}</code></p>
+  <p class="muted">Base <code>{{pr["base_node"]}}</code> to {{format_ref_label(pr["source_ref_type"], pr["source_ref_name"])}} <code>{{current_source_node}}</code></p>
   % if diff_error:
     <p class="alert">{{diff_error}}</p>
   % elif diff:
diff --git a/templates/pull_requests.tpl b/templates/pull_requests.tpl
--- a/templates/pull_requests.tpl
+++ b/templates/pull_requests.tpl
@@ -30,7 +30,7 @@
       % for pr in pull_requests:
         <li>
           <a href="/{{repo['owner_username']}}/{{repo['name']}}/pulls/{{pr['number']}}">#{{pr["number"]}} {{pr["title"]}}</a>
-          <span>({{pr["status"]}} from {{pr["source_owner_username"]}}/{{pr["source_repo_name"]}})</span>
+          <span>({{pr["status"]}} from {{pr["source_owner_username"]}}/{{pr["source_repo_name"]}} {{format_ref_label(pr["source_ref_type"], pr["source_ref_name"])}} into {{format_ref_label(pr["target_ref_type"], pr["target_ref_name"])}})</span>
         </li>
       % end
     </ul>
diff --git a/templates/ref_selector.tpl b/templates/ref_selector.tpl
new file mode 100644
--- /dev/null
+++ b/templates/ref_selector.tpl
@@ -0,0 +1,16 @@
+% selected_ref = get("selected_ref", None)
+% ref_options = get("ref_options", [])
+% selected_ref_value = get("selected_ref_value", "")
+% if selected_ref and ref_options:
+  <form class="ref-selector" method="get">
+    <label>
+      Code ref
+      <select name="ref_value">
+        % for option in ref_options:
+          <option value="{{option['value']}}" {{"selected" if option["value"] == selected_ref_value else ""}}>{{option["label"]}}</option>
+        % end
+      </select>
+    </label>
+    <button class="button small" type="submit">View</button>
+  </form>
+% end
diff --git a/templates/repo.tpl b/templates/repo.tpl
--- a/templates/repo.tpl
+++ b/templates/repo.tpl
@@ -6,6 +6,7 @@
     <h1><a href="/{{repo['owner_username']}}">{{repo["owner_username"]}}</a>/{{repo["name"]}}</h1>
 
     % include("repo_nav.tpl", repo=repo, commit_count=commit_count, issue_counts=issue_counts, pr_counts=pr_counts, star_count=star_count, is_starred=is_starred, is_owner=is_owner, can_maintain=can_maintain)
+    % include("ref_selector.tpl")
 
     <p>{{!render_markdown_links(repo["description"]) or "No description yet."}}</p>
   </div>
diff --git a/templates/repo_nav.tpl b/templates/repo_nav.tpl
--- a/templates/repo_nav.tpl
+++ b/templates/repo_nav.tpl
@@ -1,9 +1,12 @@
 % active_tab = get("repo_active_tab", "")
+% selected_ref = get("selected_ref", None)
 <nav class="repo-tabs">
-  <a class="repo-tab {{'active' if active_tab == 'overview' else ''}}" href="/{{repo['owner_username']}}/{{repo['name']}}">Overview</a>
-  <a class="repo-tab {{'active' if active_tab == 'source' else ''}}" href="/{{repo['owner_username']}}/{{repo['name']}}/src">Source</a>
-  <a class="repo-tab {{'active' if active_tab == 'commits' else ''}}" href="/{{repo['owner_username']}}/{{repo['name']}}/commits">Commits{{" (" + str(commit_count) + ")" if commit_count else ""}}</a>
+  <a class="repo-tab {{'active' if active_tab == 'overview' else ''}}" href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'], selected_ref)}}">Overview</a>
+  <a class="repo-tab {{'active' if active_tab == 'source' else ''}}" href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/src', selected_ref)}}">Source</a>
+  <a class="repo-tab {{'active' if active_tab == 'commits' else ''}}" href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/commits', selected_ref)}}">Commits{{" (" + str(commit_count) + ")" if commit_count else ""}}</a>
   <a class="repo-tab {{'active' if active_tab == 'tags' else ''}}" href="/{{repo['owner_username']}}/{{repo['name']}}/tags">Tags</a>
+  <a class="repo-tab {{'active' if active_tab == 'branches' else ''}}" href="/{{repo['owner_username']}}/{{repo['name']}}/branches">Branches</a>
+  <a class="repo-tab {{'active' if active_tab == 'bookmarks' else ''}}" href="/{{repo['owner_username']}}/{{repo['name']}}/bookmarks">Bookmarks</a>
   <a class="repo-tab {{'active' if active_tab == 'issues' else ''}}" href="/{{repo['owner_username']}}/{{repo['name']}}/issues">Issues{{" (" + str(issue_counts["open"]) + ")" if issue_counts["open"] else ""}}</a>
   <a class="repo-tab {{'active' if active_tab == 'pulls' else ''}}" href="/{{repo['owner_username']}}/{{repo['name']}}/pulls">Pull requests{{" (" + str(pr_counts["open"]) + ")" if pr_counts["open"] else ""}}</a>
   % if user:
diff --git a/templates/source.tpl b/templates/source.tpl
--- a/templates/source.tpl
+++ b/templates/source.tpl
@@ -6,15 +6,16 @@
     <h1><a href="/{{repo['owner_username']}}">{{repo["owner_username"]}}</a>/{{repo["name"]}}</h1>
     
     % include("repo_nav.tpl", repo=repo, commit_count=commit_count, issue_counts=issue_counts, pr_counts=pr_counts, star_count=star_count, is_starred=is_starred, is_owner=is_owner, can_maintain=can_maintain)
+    % include("ref_selector.tpl")
 
     <div class="breadcrumb">
-    <a href="/{{repo['owner_username']}}/{{repo['name']}}/src">root</a>
+    <a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/src', selected_ref)}}">root</a>
     % if current_path:
       % parts = current_path.split("/")
       % running = ""
       % for part in parts:
         % running = part if not running else running + "/" + part
-        <span>/</span><a href="/{{repo['owner_username']}}/{{repo['name']}}/src/{{quote_path(running)}}">{{part}}</a>
+        <span>/</span><a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/src/' + quote_path(running), selected_ref)}}">{{part}}</a>
       % end
     % end
   </div>
@@ -28,7 +29,7 @@
         <li>
           <code>{{"dir" if entry["type"] == "dir" else "file"}}</code>
           <div>
-            <a href="/{{repo['owner_username']}}/{{repo['name']}}/src/{{quote_path(entry['path'])}}">
+            <a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/src/' + quote_path(entry['path']), selected_ref)}}">
               <strong>{{entry["name"]}}{{"/" if entry["type"] == "dir" else ""}}</strong>
             </a>
           </div>