Fork of patx/hglab.

added tags page + downloads

Commit 2ed5dd8e8bed · Harrison Erd · 2026-05-04 00:10 -0400

Changeset
2ed5dd8e8bed3810a67ae729c91188bbe745d71b

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
@@ -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>