Fork of patx/hglab.
tokenmess/hglab
added tags page + downloads
Commit 2ed5dd8e8bed · Harrison Erd · 2026-05-04 00:10 -0400
Comments
No comments yet.
Diff
diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -7,6 +7,7 @@
import os
import re
import secrets
+import shlex
import shutil
import sqlite3
import subprocess
@@ -903,6 +904,38 @@
return commits
+def list_repo_tags(path):
+ template_arg = (
+ "{tag}\\x1f{rev}\\x1f{node}\\x1f{node|short}\\x1f{date|isodate}\\x1f{desc|firstline}\\x1e"
+ )
+ completed = run_hg(["tags", "--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 tags.", completed.returncode)
+
+ tags = []
+ for record in completed.stdout.split("\x1e"):
+ if not record:
+ continue
+ parts = record.split("\x1f")
+ if len(parts) != 6 or parts[0] == "tip":
+ continue
+ tags.append(
+ {
+ "name": parts[0],
+ "shell_name": shlex.quote(parts[0]),
+ "rev": parts[1],
+ "node": parts[2],
+ "short_node": parts[3],
+ "date": parts[4],
+ "summary": parts[5],
+ }
+ )
+ return tags
+
+
def commit_count(path):
completed = run_hg(["log", "--template", "."], cwd=path, check=False)
if completed.returncode != 0:
@@ -1346,6 +1379,7 @@
for tab, suffix in (
("source", "/src"),
("commits", "/commits"),
+ ("tags", "/tags"),
("issues", "/issues"),
("pulls", "/pulls"),
("settings", "/settings"),
@@ -1421,10 +1455,15 @@
env["REMOTE_USER"] = auth_user["username"]
status_headers = {}
+ body_parts = []
+
+ def write_body(chunk):
+ body_parts.append(chunk if isinstance(chunk, bytes) else chunk.encode("utf-8"))
def start_response(status, headers, exc_info=None):
status_headers["status"] = status
status_headers["headers"] = headers
+ return write_body
# The public factory runs Mercurial's initialization hook. Importing the
# internal hgweb module directly can leave bundle2 handlers such as
@@ -1435,14 +1474,13 @@
)
body_iter = hg_app(env, start_response)
try:
- body = b"".join(
- chunk if isinstance(chunk, bytes) else chunk.encode("utf-8")
- for chunk in body_iter
- )
+ for chunk in body_iter:
+ write_body(chunk)
finally:
close = getattr(body_iter, "close", None)
if close:
close()
+ body = b"".join(body_parts)
raw_status = status_headers.get("status", "500 Internal Server Error")
if isinstance(raw_status, bytes):
@@ -1788,6 +1826,21 @@
return render("commits.tpl", repo=repo, commits=commit_log(path), **repo_page_context(repo, path))
[email protected]("/<owner>/<repo_name>/tags")
+def repo_tags(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(
+ "tags.tpl",
+ repo=repo,
+ tags=list_repo_tags(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)
diff --git a/static/styles.css b/static/styles.css
--- a/static/styles.css
+++ b/static/styles.css
@@ -67,6 +67,8 @@
.file-kind { width: 4rem; }
.commit-list, .file-list, .issue-list, .clean-list { padding-left: 0; list-style: none; }
.commit-list li, .file-list li { display: grid; grid-template-columns: 8rem 1fr; gap: 1rem; }
+.tag-list li { display: grid; grid-template-columns: 1fr 18rem; gap: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid #eee; }
+.tag-actions { display: grid; gap: .75rem; align-content: start; }
.alert { margin-bottom: 1rem; color: #900; }
.notice { margin-bottom: 1rem; color: #060; }
.danger-zone { color: #900; }
@@ -75,5 +77,5 @@
@media (max-width: 760px) {
body { margin-top: 1rem; }
- .grid.two, .commit-list li { grid-template-columns: 1fr; }
+ .grid.two, .commit-list li, .tag-list li { grid-template-columns: 1fr; }
}
diff --git a/templates/repo_nav.tpl b/templates/repo_nav.tpl
--- a/templates/repo_nav.tpl
+++ b/templates/repo_nav.tpl
@@ -3,6 +3,7 @@
<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 == 'tags' else ''}}" href="/{{repo['owner_username']}}/{{repo['name']}}/tags">Tags</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/tags.tpl b/templates/tags.tpl
new file mode 100644
--- /dev/null
+++ b/templates/tags.tpl
@@ -0,0 +1,41 @@
+% rebase("base.tpl", title=repo["owner_username"] + "/" + repo["name"] + " tags", 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 tags:
+ <ul class="clean-list tag-list">
+ % for tag in tags:
+ <li>
+ <div>
+ <h2>{{tag["name"]}}</h2>
+ <p>
+ <code><a href="/{{repo['owner_username']}}/{{repo['name']}}/commits/{{tag['short_node']}}">{{tag["short_node"]}}</a></code>
+ <span class="muted">rev {{tag["rev"]}} · {{tag["date"]}}</span>
+ </p>
+ % if tag["summary"]:
+ <p>{{tag["summary"]}}</p>
+ % end
+ </div>
+ <div class="tag-actions">
+ <a class="button small" href="/hg/{{repo['owner_username']}}/{{repo['name']}}/archive/{{tag['short_node']}}.zip">Download zip</a>
+ <pre>hg clone {{clone_url}}
+cd {{repo["name"]}}
+hg update {{tag["shell_name"]}}</pre>
+ </div>
+ </li>
+ % end
+ </ul>
+ % else:
+ <p class="empty">No tags yet.</p>
+ % end
+</section>