Fork of patx/hglab.
tokenmess/hglab
branch and bookmark support, work in progress
Commit 956e29e14adc · Harrison Erd · 2026-05-04 03:14 -0400
Comments
No comments yet.
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>