ref picker only appears on overview/source/commits/tags/branches/bookmarks, which hides it on issues, PRs, and settings. I also tightened PR targets to branches/bookmarks, labelled local tags, made tag rows match branch/bookmark actions, and removed the misleading active * from bookmarks.

Commit 076922ffa056 · Harrison Erd · 2026-05-05 03:36 -0400

Changeset
076922ffa056f36ba1a79d359e0d769baa5fae41

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
@@ -72,6 +72,8 @@
 REF_TYPE_COMMIT = "commit"
 REF_TYPES = {REF_TYPE_BRANCH, REF_TYPE_BOOKMARK, REF_TYPE_TAG, REF_TYPE_TIP, REF_TYPE_COMMIT}
 PULL_REQUEST_REF_TYPES = {REF_TYPE_BRANCH, REF_TYPE_BOOKMARK, REF_TYPE_TIP}
+TARGET_PULL_REQUEST_REF_TYPES = {REF_TYPE_BRANCH, REF_TYPE_BOOKMARK}
+REF_PICKER_TABS = {"overview", "source", "commits", "tags", "branches", "bookmarks"}
 REF_QUERY_KEYS = {"ref", "ref_type", "ref_value"}
 REF_VALUE_SEPARATOR = "|"
 SCRIPT_STYLE_RE = re.compile(r"(?is)<(script|style)\b[^>]*>.*?</\1>")
@@ -1471,7 +1473,8 @@
 
 def list_repo_tags(path):
     template_arg = (
-        "{tag}\\x1f{rev}\\x1f{node}\\x1f{node|short}\\x1f{date|isodate}\\x1f{desc|firstline}\\x1e"
+        "{tag}\\x1f{rev}\\x1f{node}\\x1f{node|short}\\x1f{date|isodate}\\x1f"
+        "{desc|firstline}\\x1f{type}\\x1e"
     )
     completed = run_hg(["tags", "--template", template_arg], cwd=path, check=False)
     if completed.returncode != 0:
@@ -1485,7 +1488,7 @@
         if not record:
             continue
         parts = record.split("\x1f")
-        if len(parts) != 6 or parts[0] == "tip":
+        if len(parts) != 7 or parts[0] == "tip":
             continue
         tags.append(
             {
@@ -1499,6 +1502,7 @@
                 "branch": "",
                 "active": False,
                 "closed": False,
+                "local": parts[6] == "local",
                 "is_default": False,
                 "date": parts[4],
                 "summary": parts[5],
@@ -1794,6 +1798,8 @@
     label = format_ref_label(ref["type"], ref.get("name", ""))
     if ref["type"] == REF_TYPE_BRANCH and ref.get("closed"):
         label += " (closed)"
+    if ref["type"] == REF_TYPE_TAG and ref.get("local"):
+        label += " (local)"
     return label
 
 
@@ -1847,7 +1853,7 @@
 
 
 def target_repo_ref_options(path):
-    return repo_ref_options(path, include_closed_branches=False, include_tip=True, include_tags=False)
+    return repo_ref_options(path, include_closed_branches=False, include_tip=False, include_tags=False)
 
 
 def revision_branch(path, node):
@@ -2152,6 +2158,10 @@
     source_path = repo_path(source_repo["owner_username"], source_repo["name"])
     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 source_ref["type"] not in PULL_REQUEST_REF_TYPES:
+        raise ValueError("Choose a branch, bookmark, or tip as the source ref.")
+    if target_ref["type"] not in TARGET_PULL_REQUEST_REF_TYPES:
+        raise ValueError("Choose an open target branch or bookmark.")
     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")
@@ -2369,8 +2379,10 @@
     if path is None:
         path = repo_path(repo["owner_username"], repo["name"])
     user = current_user()
+    active_tab = repo_active_tab(repo)
+    show_ref_picker = active_tab in REF_PICKER_TABS
     if selected_ref is None:
-        selected_ref = selected_repo_ref(path)
+        selected_ref = selected_repo_ref(path) if show_ref_picker else default_code_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 {
@@ -2382,11 +2394,12 @@
         "is_owner": user_owns_repo(user, repo),
         "can_maintain": user_can_maintain_repo(user, repo),
         "has_fork": bool(user and user_has_fork_for_target(user["id"], fork_target_id)),
-        "repo_active_tab": repo_active_tab(repo),
+        "repo_active_tab": active_tab,
+        "show_ref_picker": show_ref_picker,
         "source_repo": source_repo,
         "selected_ref": selected_ref,
         "selected_ref_label": ref_option_label(selected_ref) if selected_ref else "",
-        "ref_options": repo_ref_options(path),
+        "ref_options": repo_ref_options(path) if show_ref_picker else [],
         "selected_ref_value": ref_option_value(
             selected_ref.get("type", REF_TYPE_TIP),
             selected_ref.get("name", ""),
@@ -3006,14 +3019,17 @@
     for fork in forks:
         source_options.extend(source_repo_ref_options(fork))
     target_options = target_repo_ref_options(path)
+    target_option_values = {option["value"] for option in target_options}
     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:
+    if not selected_target_ref and target_options:
         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:
+        if selected_target_ref not in target_option_values:
+            selected_target_ref = target_options[0]["value"]
+    if selected_target_ref and selected_target_ref not in target_option_values 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 ""
@@ -3023,7 +3039,7 @@
             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,
-                allowed_types=PULL_REQUEST_REF_TYPES,
+                allowed_types=TARGET_PULL_REQUEST_REF_TYPES,
             )
         except ValueError as exc:
             return render(
diff --git a/templates/bookmarks.tpl b/templates/bookmarks.tpl
--- a/templates/bookmarks.tpl
+++ b/templates/bookmarks.tpl
@@ -17,7 +17,7 @@
     <ul class="commit-list">
       % for bookmark in bookmarks:
         <li>
-          <code>{{bookmark["name"]}}{{" *" if bookmark["active"] else ""}}</code>
+          <code>{{bookmark["name"]}}</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>
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,7 +11,7 @@
 </section>
 
 <section class="panel">
-  % if forks and source_options:
+  % if forks and source_options and target_options:
     <h2>Open pull request</h2>
     <form method="post">
       {{!csrf_field()}}
@@ -41,6 +41,8 @@
       </label>
       <button class="button" type="submit">Open pull request</button>
     </form>
+  % elif forks and source_options:
+    <p class="empty">This repository has no open target branches or bookmarks.</p>
   % else:
     <p class="empty">You do not have a fork of this repository yet.</p>
     <form class="inline-form" method="post" action="/{{repo['owner_username']}}/{{repo['name']}}/fork">
diff --git a/templates/ref_selector.tpl b/templates/ref_selector.tpl
--- a/templates/ref_selector.tpl
+++ b/templates/ref_selector.tpl
@@ -3,7 +3,8 @@
 % selected_ref_value = get("selected_ref_value", "")
 % selected_ref_label = get("selected_ref_label", ref_option_label(selected_ref) if selected_ref else "")
 % active_tab = get("repo_active_tab", "")
-% if selected_ref and ref_options:
+% show_ref_picker = get("show_ref_picker", False)
+% if show_ref_picker and selected_ref and ref_options:
   <div class="ref-picker" data-ref-picker>
     <button class="ref-picker-toggle" type="button" aria-haspopup="true" aria-expanded="false">
       <span>{{selected_ref_label}}</span>
diff --git a/templates/tags.tpl b/templates/tags.tpl
--- a/templates/tags.tpl
+++ b/templates/tags.tpl
@@ -17,11 +17,17 @@
     <ul class="commit-list">
       % for tag in tags:
         <li>
-            <code>{{tag["name"]}} <a href="/hg/{{repo['owner_username']}}/{{repo['name']}}/archive/{{tag['short_node']}}.zip"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/></svg></a></code>
-            <div>
-              <strong><a href="/{{repo['owner_username']}}/{{repo['name']}}/commits/{{tag['short_node']}}">{{tag["short_node"]}}</a></strong>
-              <small>rev {{tag["rev"]}} · {{tag["date"]}}</small>
+          <code>{{tag["name"]}}</code>
+          <div>
+            <strong><a href="/{{repo['owner_username']}}/{{repo['name']}}/commits/{{tag['short_node']}}">{{tag["short_node"]}}</a></strong>
+            <small>rev {{tag["rev"]}} · {{tag["date"]}}{{" · local" if tag["local"] else ""}}</small>
+            <p>{{tag["summary"]}}</p>
+            <div class="ref-actions">
+              <a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/src', tag, True)}}">Browse code</a>
+              <a href="{{url_with_ref('/' + repo['owner_username'] + '/' + repo['name'] + '/commits', tag, True)}}">Commits</a>
+              <a href="/hg/{{repo['owner_username']}}/{{repo['name']}}/archive/{{tag['short_node']}}.zip">Archive</a>
             </div>
+          </div>
         </li>
       % end
     </ul>
diff --git a/tests/test_app.py b/tests/test_app.py
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -471,6 +471,9 @@
     assert hglab.ref_option_label({"type": hglab.REF_TYPE_BRANCH, "name": "old", "closed": True}) == (
         "branch old (closed)"
     )
+    assert hglab.ref_option_label({"type": hglab.REF_TYPE_TAG, "name": "local-only", "local": True}) == (
+        "tag local-only (local)"
+    )
 
 
 def test_init_db_creates_expected_tables_and_is_idempotent(isolated_app):
@@ -712,8 +715,8 @@
         "Please merge this",
         isolated_app.REF_TYPE_TIP,
         "",
-        isolated_app.REF_TYPE_TIP,
-        "",
+        isolated_app.REF_TYPE_BRANCH,
+        "default",
     )
     pr = isolated_app.get_pull_request(target_repo["id"], number)
     diff, current_source_node, source_ref = isolated_app.pull_request_diff(pr)
@@ -764,8 +767,8 @@
         "",
         isolated_app.REF_TYPE_BRANCH,
         "feature/pr",
-        isolated_app.REF_TYPE_TIP,
-        "",
+        isolated_app.REF_TYPE_BRANCH,
+        "default",
     )
     branch_pr = isolated_app.get_pull_request(target_repo["id"], branch_pr_number)
     branch_diff, branch_source_node, branch_source_ref = isolated_app.pull_request_diff(branch_pr)
@@ -785,8 +788,8 @@
         "",
         isolated_app.REF_TYPE_BOOKMARK,
         "feature-bm",
-        isolated_app.REF_TYPE_TIP,
-        "",
+        isolated_app.REF_TYPE_BRANCH,
+        "default",
     )
     bookmark_pr = isolated_app.get_pull_request(target_repo["id"], bookmark_pr_number)
     bookmark_diff, bookmark_source_node, bookmark_source_ref = isolated_app.pull_request_diff(bookmark_pr)
@@ -803,6 +806,7 @@
     owner = create_user("alice")
     nodes = create_repo_with_refs(owner)
     path = nodes["path"]
+    isolated_app.run_hg(["tag", "--local", "local-only"], cwd=path)
 
     branches = {branch["name"]: branch for branch in isolated_app.list_repo_branches(path)}
     tags = isolated_app.list_repo_tags(path)
@@ -820,6 +824,7 @@
     assert tags[0]["name"] == "v1.0"
     assert tags[0]["type"] == isolated_app.REF_TYPE_TAG
     assert tags[0]["node"] == nodes["feature_node"]
+    assert next(tag for tag in tags if tag["name"] == "local-only")["local"] is True
     assert bookmarks["feature-bm"]["node"] == nodes["feature_node"]
     assert isolated_app.default_code_ref(path)["node"] == nodes["default_node"]
     assert isolated_app.resolve_repo_ref(path, isolated_app.REF_TYPE_BRANCH, "feature")["name"] == "feature"
@@ -829,8 +834,10 @@
     )
     assert isolated_app.commit_ref(path, nodes["feature_node"])["type"] == isolated_app.REF_TYPE_COMMIT
     assert "tag v1.0" in all_labels
+    assert "tag local-only (local)" in all_labels
     assert "branch old (closed)" not in target_labels
     assert "tag v1.0" not in target_labels
+    assert "tip" not in target_labels
     assert "branch old (closed)" in all_labels
     assert "alice/demo tag v1.0" not in source_labels
 
@@ -937,8 +944,13 @@
         assert expected_text in response.text, path
 
     repo_response = client.get("/alice/demo")
+    issues_response = client.get("/alice/demo/issues")
+    pulls_response = client.get("/alice/demo/pulls")
     assert 'data-ref-label="tag v1.0"' in repo_response.text
     assert 'data-ref-initial="true"' in repo_response.text
+    assert 'class="ref-picker"' in repo_response.text
+    assert 'class="ref-picker"' not in issues_response.text
+    assert 'class="ref-picker"' not in pulls_response.text
 
     response = client.get("/alice/demo/raw/feature.txt?ref_type=branch&ref=feature")
     assert response.status_code == 200
@@ -978,6 +990,9 @@
     profile = owner_client.get("/alice")
     assert "Alice A." in profile.text
     assert "https://example.com" in profile.text
+    settings_response = owner_client.get("/alice/demo/settings")
+    assert settings_response.status_code == 200
+    assert 'class="ref-picker"' not in settings_response.text
 
     response = owner_client.post("/alice/demo/settings", {"action": "save", "description": "Updated"})
     assert response.status_code == 200
@@ -1020,6 +1035,7 @@
     assert response.status_code == 200
     assert "Bug report" in response.text
     assert "It fails" in response.text
+    assert 'class="ref-picker"' not in response.text
 
     response = client.post("/alice/demo/issues/1", {"action": "comment", "body": ""})
     assert response.status_code == 200
@@ -1060,6 +1076,8 @@
     assert "bob/demo-fork tip" in response.text
     assert "bob/demo-fork branch feature/pr" in response.text
     assert "bob/demo-fork bookmark feature-bm" in response.text
+    assert 'class="ref-picker"' not in response.text
+    assert 'value="tip|"' not in response.text
 
     response = bob_client.post(
         "/alice/demo/pulls/new",
@@ -1067,7 +1085,7 @@
             "source_ref": isolated_app.source_ref_option_value(
                 source_repo["id"], isolated_app.REF_TYPE_BRANCH, "feature/pr"
             ),
-            "target_ref": isolated_app.ref_option_value(isolated_app.REF_TYPE_TIP, ""),
+            "target_ref": isolated_app.ref_option_value(isolated_app.REF_TYPE_BRANCH, "default"),
             "title": "Add feature",
             "body": "Please merge this",
         },
@@ -1085,6 +1103,7 @@
     assert response.status_code == 200
     assert "feature.txt" in response.text
     assert "new feature" in response.text
+    assert 'class="ref-picker"' not in response.text
 
     response = bob_client.post("/alice/demo/pulls/1", {"action": "comment", "body": "Looks ready"})
     assert response.status_code == 303
@@ -1101,7 +1120,9 @@
 
     merged = isolated_app.get_pull_request(target_repo["id"], 1)
     assert merged["status"] == "merged"
-    assert merged["merge_node"] == source_node
+    assert merged["merge_node"]
+    assert isolated_app.repo_has_revision(target_path, merged["merge_node"])
+    assert isolated_app.repo_has_revision(target_path, source_node)
     response = owner_client.get("/alice/demo/pulls/1")
     assert response.status_code == 200
     assert "Merged by alice" in response.text