patx/hglab
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
Comments
No comments yet.
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