Skip to content

Commit eb93a8a

Browse files
committed
✨ feat: Enhance command display and add unit tests for extension entrypoint
1 parent 29ea698 commit eb93a8a

9 files changed

Lines changed: 199 additions & 41 deletions

File tree

.github/workflows/unit-test.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,10 @@ jobs:
4141
- name: unit test
4242
run: |
4343
just ci-test
44+
45+
- name: e2e extension entrypoint
46+
env:
47+
UV_CACHE_DIR: /tmp/uv-cache
48+
UV_TOOL_DIR: /tmp/uv-tools
49+
run: |
50+
./gh-llm --help

gh-llm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ if ! command -v uv >/dev/null 2>&1; then
88
exit 1
99
fi
1010

11+
export GH_LLM_DISPLAY_CMD="gh llm"
1112
exec uvx --from "${SCRIPT_DIR}" gh-llm "$@"

src/gh_llm/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
parse_review_ids as _parse_review_ids,
1111
register_pr_parser,
1212
)
13+
from gh_llm.invocation import detect_prog_name
1314

1415

1516
def run(argv: list[str]) -> int:
@@ -30,7 +31,7 @@ def run(argv: list[str]) -> int:
3031

3132
def _build_parser() -> argparse.ArgumentParser:
3233
parser = argparse.ArgumentParser(
33-
prog="gh-llm",
34+
prog=detect_prog_name(sys.argv[0]),
3435
description="LLM-friendly GitHub pull request timeline viewer",
3536
)
3637
parser.add_argument("-v", "--version", action="version", version=__version__)

src/gh_llm/commands/pr.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import TYPE_CHECKING, Any
55

66
from gh_llm.github_api import GitHubClient
7+
from gh_llm.invocation import display_command_with
78
from gh_llm.pager import DEFAULT_PAGE_SIZE, TimelinePager
89
from gh_llm.render import (
910
render_checks_section,
@@ -460,15 +461,14 @@ def cmd_pr_review_start(args: Any) -> int:
460461
print(f"PR: {meta.ref.number} ({repo})")
461462
print(f"Total hunks: {len(hunks)}")
462463
print(f"Δ full diff: `gh pr diff {meta.ref.number} --repo {repo}`")
463-
print(
464-
"Comment template: `gh-llm pr review-comment --path '<path>' --line <line> --side RIGHT --body '<review_comment>' "
465-
f"--pr {meta.ref.number} --repo {repo}`"
464+
comment_template_cmd = display_command_with(
465+
f"pr review-comment --path '<path>' --line <line> --side RIGHT --body '<review_comment>' --pr {meta.ref.number} --repo {repo}"
466466
)
467-
print(
468-
"Suggestion template: `gh-llm pr review-suggest --path '<path>' --line <line> --side RIGHT "
469-
"--body '<reason>' --suggestion '<replacement>' "
470-
f"--pr {meta.ref.number} --repo {repo}`"
467+
suggestion_template_cmd = display_command_with(
468+
f"pr review-suggest --path '<path>' --line <line> --side RIGHT --body '<reason>' --suggestion '<replacement>' --pr {meta.ref.number} --repo {repo}"
471469
)
470+
print(f"Comment template: `{comment_template_cmd}`")
471+
print(f"Suggestion template: `{suggestion_template_cmd}`")
472472
print()
473473

474474
if not visible:
@@ -480,12 +480,14 @@ def cmd_pr_review_start(args: Any) -> int:
480480
print(f"File: {hunk.path}")
481481
print(f"Header: {hunk.header}")
482482
print(f"Suggested anchor line (RIGHT): {hunk.anchor_line}")
483-
print(
484-
f"⏎ comment: `gh-llm pr review-comment --path '{hunk.path}' --line {hunk.anchor_line} --side RIGHT --body '<review_comment>' --pr {meta.ref.number} --repo {repo}`"
483+
comment_cmd = display_command_with(
484+
f"pr review-comment --path '{hunk.path}' --line {hunk.anchor_line} --side RIGHT --body '<review_comment>' --pr {meta.ref.number} --repo {repo}"
485485
)
486-
print(
487-
f"⏎ suggest: `gh-llm pr review-suggest --path '{hunk.path}' --line {hunk.anchor_line} --side RIGHT --body '<reason>' --suggestion '<replacement>' --pr {meta.ref.number} --repo {repo}`"
486+
suggest_cmd = display_command_with(
487+
f"pr review-suggest --path '{hunk.path}' --line {hunk.anchor_line} --side RIGHT --body '<reason>' --suggestion '<replacement>' --pr {meta.ref.number} --repo {repo}"
488488
)
489+
print(f"⏎ comment: `{comment_cmd}`")
490+
print(f"⏎ suggest: `{suggest_cmd}`")
489491
print("```diff")
490492
for line in hunk.lines:
491493
print(line)

src/gh_llm/github_api.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import cast
1010
from urllib.parse import urlparse
1111

12+
from gh_llm.invocation import display_command, display_command_with
1213
from gh_llm.models import CheckItem, PageInfo, PullRequestMeta, PullRequestRef, TimelineEvent, TimelinePage
1314

1415
MAX_INLINE_TEXT = 8000
@@ -1254,13 +1255,15 @@ def _parse_node(
12541255
source_repo = subject.repo
12551256
detail = subject.detail
12561257
summary_lines.append(f"referenced by PR #{source_number} {detail} ({source_repo})")
1257-
summary_lines.append(f"⏎ view: `gh-llm pr view {source_number} --repo {source_repo}`")
1258+
summary_lines.append(f"⏎ view: `{display_command_with(f'pr view {source_number} --repo {source_repo}')}`")
12581259
elif subject is not None and subject.type == "Issue":
12591260
source_number = subject.number
12601261
source_repo = subject.repo
12611262
detail = subject.detail
12621263
summary_lines.append(f"referenced by issue #{source_number} {detail} ({source_repo})")
1263-
summary_lines.append(f"⏎ view (reserved): `gh-llm issue view {source_number} --repo {source_repo}`")
1264+
summary_lines.append(
1265+
f"⏎ view (reserved): `{display_command_with(f'issue view {source_number} --repo {source_repo}')}`"
1266+
)
12641267
else:
12651268
summary_lines.append("referenced by another item")
12661269
if is_cross:
@@ -1285,13 +1288,15 @@ def _parse_node(
12851288
source_repo = subject.repo
12861289
detail = subject.detail
12871290
summary_lines.append(f"cross-referenced by PR #{source_number} {detail} ({source_repo})")
1288-
summary_lines.append(f"⏎ view: `gh-llm pr view {source_number} --repo {source_repo}`")
1291+
summary_lines.append(f"⏎ view: `{display_command_with(f'pr view {source_number} --repo {source_repo}')}`")
12891292
elif subject is not None and subject.type == "Issue":
12901293
source_number = subject.number
12911294
source_repo = subject.repo
12921295
detail = subject.detail
12931296
summary_lines.append(f"cross-referenced by issue #{source_number} {detail} ({source_repo})")
1294-
summary_lines.append(f"⏎ view (reserved): `gh-llm issue view {source_number} --repo {source_repo}`")
1297+
summary_lines.append(
1298+
f"⏎ view (reserved): `{display_command_with(f'issue view {source_number} --repo {source_repo}')}`"
1299+
)
12951300
else:
12961301
summary_lines.append("cross-referenced by another item")
12971302
if is_cross:
@@ -1559,19 +1564,20 @@ def _render_review_thread_block(
15591564
if minimized_hidden_count > 0:
15601565
reason_text = ", ".join(sorted(minimized_reasons))
15611566
lines.append(f" ... {minimized_hidden_count} hidden review comments (reason: {reason_text}).")
1567+
reply_cmd = display_command_with(
1568+
f"pr thread-reply {thread_id} --body '<reply>' --pr {ref.number} --repo {ref.owner}/{ref.name}"
1569+
)
1570+
unresolve_cmd = display_command_with(
1571+
f"pr thread-unresolve {thread_id} --pr {ref.number} --repo {ref.owner}/{ref.name}"
1572+
)
1573+
resolve_cmd = display_command_with(f"pr thread-resolve {thread_id} --pr {ref.number} --repo {ref.owner}/{ref.name}")
15621574
lines.append(f" ◌ thread_id: {thread_id}")
15631575
lines.append(" ⌨ reply_body: '<reply>'")
1564-
lines.append(
1565-
f" ⏎ Reply via gh-llm: `gh-llm pr thread-reply {thread_id} --body '<reply>' --pr {ref.number} --repo {ref.owner}/{ref.name}`"
1566-
)
1576+
lines.append(f" ⏎ Reply via {display_command()}: `{reply_cmd}`")
15671577
if is_resolved:
1568-
lines.append(
1569-
f" ⏎ Unresolve via gh-llm: `gh-llm pr thread-unresolve {thread_id} --pr {ref.number} --repo {ref.owner}/{ref.name}`"
1570-
)
1578+
lines.append(f" ⏎ Unresolve via {display_command()}: `{unresolve_cmd}`")
15711579
else:
1572-
lines.append(
1573-
f" ⏎ Resolve via gh-llm: `gh-llm pr thread-resolve {thread_id} --pr {ref.number} --repo {ref.owner}/{ref.name}`"
1574-
)
1580+
lines.append(f" ⏎ Resolve via {display_command()}: `{resolve_cmd}`")
15751581
return lines, visible_comments, has_clipped_diff_hunk, details_collapsed_count
15761582

15771583

@@ -1664,11 +1670,12 @@ def _render_review_comment_block(
16641670
lines.append(f" Reactions: {reactions_summary}")
16651671
comment_id = _as_optional_str(comment.get("id")) or ""
16661672
if comment_id and author == viewer_login:
1673+
edit_cmd = display_command_with(
1674+
f"pr comment-edit {comment_id} --body '<comment_body>' --pr {ref.number} --repo {ref.owner}/{ref.name}"
1675+
)
16671676
lines.append(f" ◌ comment_id: {comment_id}")
16681677
lines.append(" ⌨ comment_body: '<comment_body>'")
1669-
lines.append(
1670-
f" ⏎ Edit comment via gh-llm: `gh-llm pr comment-edit {comment_id} --body '<comment_body>' --pr {ref.number} --repo {ref.owner}/{ref.name}`"
1671-
)
1678+
lines.append(f" ⏎ Edit comment via {display_command()}: `{edit_cmd}`")
16721679

16731680
if not body and not diff_hunk:
16741681
lines.append(" (empty review comment)")

src/gh_llm/invocation.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from pathlib import Path
5+
6+
DEFAULT_DISPLAY_COMMAND = "gh-llm"
7+
DISPLAY_COMMAND_ENV = "GH_LLM_DISPLAY_CMD"
8+
9+
10+
def display_command() -> str:
11+
raw = os.environ.get(DISPLAY_COMMAND_ENV, "").strip()
12+
if raw:
13+
return raw
14+
return DEFAULT_DISPLAY_COMMAND
15+
16+
17+
def display_command_with(args: str) -> str:
18+
suffix = args.strip()
19+
if not suffix:
20+
return display_command()
21+
return f"{display_command()} {suffix}"
22+
23+
24+
def detect_prog_name(argv0: str) -> str:
25+
name = Path(argv0).name
26+
if name:
27+
return name
28+
return DEFAULT_DISPLAY_COMMAND

src/gh_llm/render.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from datetime import UTC
66
from typing import TYPE_CHECKING
77

8+
from gh_llm.invocation import display_command, display_command_with
9+
810
if TYPE_CHECKING:
911
from string.templatelib import Template
1012

@@ -142,7 +144,7 @@ def render_checks_section(
142144
if not is_open:
143145
return [
144146
"## Checks",
145-
f"Closed PR: checks are hidden by default. ⏎ run `gh-llm pr checks --pr {context.number} --repo {repo} --all`",
147+
f"Closed PR: checks are hidden by default. ⏎ run `{display_command_with(f'pr checks --pr {context.number} --repo {repo} --all')}`",
146148
"",
147149
]
148150

@@ -167,13 +169,15 @@ def render_checks_section(
167169
elif item.details_url:
168170
lines.append(f" ⏎ details: `{item.details_url}`")
169171
if show_all:
170-
lines.append(f"⏎ show only non-passed: `gh-llm pr checks --pr {context.number} --repo {repo}`")
172+
lines.append(
173+
f"⏎ show only non-passed: `{display_command_with(f'pr checks --pr {context.number} --repo {repo}')}`"
174+
)
171175
elif hidden_count > 0:
172176
lines.append(
173-
f"{hidden_count} passed checks hidden. ⏎ show all: `gh-llm pr checks --pr {context.number} --repo {repo} --all`"
177+
f"{hidden_count} passed checks hidden. ⏎ show all: `{display_command_with(f'pr checks --pr {context.number} --repo {repo} --all')}`"
174178
)
175179
else:
176-
lines.append(f"⏎ show all: `gh-llm pr checks --pr {context.number} --repo {repo} --all`")
180+
lines.append(f"⏎ show all: `{display_command_with(f'pr checks --pr {context.number} --repo {repo} --all')}`")
177181
lines.append("")
178182
return lines
179183

@@ -193,7 +197,7 @@ def render_hidden_gap(context: TimelineContext, hidden_pages: list[int]) -> list
193197
hidden_label,
194198
*[
195199
_render_template(
196-
t"- ⏎ `gh-llm {context.kind} timeline-expand {page} --{selector_name} {context.number} --repo {repo}`"
200+
t"- ⏎ `{display_command_with(f'{context.kind} timeline-expand {page} --{selector_name} {context.number} --repo {repo}')}`"
197201
)
198202
for page in hidden_pages
199203
],
@@ -204,7 +208,10 @@ def render_hidden_gap(context: TimelineContext, hidden_pages: list[int]) -> list
204208
def _render_item(index: int, event: TimelineEvent, context: TimelineContext, command_group: str) -> list[str]:
205209
timestamp = event.timestamp.astimezone(UTC).strftime("%Y-%m-%d %H:%M UTC")
206210
selector_name = "issue" if command_group == "issue" else "pr"
207-
details_action = f"⏎ run `gh-llm {command_group} details-expand {index} --{selector_name} {context.number} --repo {context.owner}/{context.name}`"
211+
details_expand_cmd = display_command_with(
212+
f"{command_group} details-expand {index} --{selector_name} {context.number} --repo {context.owner}/{context.name}"
213+
)
214+
details_action = f"⏎ run `{details_expand_cmd}`"
208215
display_summary = (event.summary or "").replace(
209216
"(details body collapsed)",
210217
f"(details body collapsed; {details_action})",
@@ -216,35 +223,36 @@ def _render_item(index: int, event: TimelineEvent, context: TimelineContext, com
216223
if event.reactions_summary:
217224
lines.append(f" Reactions: {event.reactions_summary}")
218225
if event.editable_comment_id:
226+
edit_cmd = display_command_with(
227+
f"{command_group} comment-edit {event.editable_comment_id} --body '<comment_body>' --{selector_name} {context.number} --repo {context.owner}/{context.name}"
228+
)
219229
lines.append(f" ◌ comment_id: {event.editable_comment_id}")
220230
lines.append(" ⌨ comment_body: '<comment_body>'")
221-
lines.append(
222-
f" ⏎ Edit comment via gh-llm: `gh-llm {command_group} comment-edit {event.editable_comment_id} --body '<comment_body>' --{selector_name} {context.number} --repo {context.owner}/{context.name}`"
223-
)
231+
lines.append(f" ⏎ Edit comment via {display_command()}: `{edit_cmd}`")
224232
else:
225233
lines.extend(_indent_block(display_summary))
226234
if event.resolved_hidden_count > 0:
227235
repo = f"{context.owner}/{context.name}"
228236
lines.append(
229237
f" {event.resolved_hidden_count} resolved review comments are collapsed; "
230-
f"⏎ run `gh-llm pr review-expand {event.source_id} --pr {context.number} --repo {repo}`"
238+
f"⏎ run `{display_command_with(f'pr review-expand {event.source_id} --pr {context.number} --repo {repo}')}`"
231239
)
232240
if event.minimized_hidden_count > 0:
233241
repo = f"{context.owner}/{context.name}"
234242
reason_suffix = f" (reason: {event.minimized_hidden_reasons})" if event.minimized_hidden_reasons else ""
235243
lines.append(
236244
f" {event.minimized_hidden_count} hidden review comments are collapsed{reason_suffix}; "
237-
f"⏎ run `gh-llm pr review-expand {event.source_id} --pr {context.number} --repo {repo}`"
245+
f"⏎ run `{display_command_with(f'pr review-expand {event.source_id} --pr {context.number} --repo {repo}')}`"
238246
)
239247
if event.kind.startswith("review/"):
240248
detail_text = (event.full_text or event.summary or "").lower()
241249
if "diff hunk clipped" in detail_text:
242250
lines.append(
243-
f" ⏎ run `gh-llm pr review-expand {event.source_id} --pr {context.number} --repo {context.owner}/{context.name} --diff-hunk-lines 0` for full diff hunk context"
251+
f" ⏎ run `{display_command_with(f'pr review-expand {event.source_id} --pr {context.number} --repo {context.owner}/{context.name} --diff-hunk-lines 0')}` for full diff hunk context"
244252
)
245253
if event.is_truncated:
246254
lines.append(
247-
f" ⏎ run `gh-llm {command_group} event {index} --{selector_name} {context.number} --repo {context.owner}/{context.name}` for full content"
255+
f" ⏎ run `{display_command_with(f'{command_group} event {index} --{selector_name} {context.number} --repo {context.owner}/{context.name}')}` for full content"
248256
)
249257
if event.kind == "push/commit":
250258
lines.append(

tests/test_cli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,20 @@ def _events() -> list[dict[str, Any]]:
10201020
return _base_events()
10211021

10221022

1023+
def test_display_command_env_is_used_in_actions(
1024+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
1025+
) -> None:
1026+
monkeypatch.setenv("GH_LLM_DISPLAY_CMD", "gh llm")
1027+
monkeypatch.setattr(github_api.subprocess, "run", GhResponder().run)
1028+
1029+
code = cli.run(["pr", "view", "77928", "--repo", "PaddlePaddle/Paddle", "--page-size", "2"])
1030+
assert code == 0
1031+
1032+
out = capsys.readouterr().out
1033+
assert "gh llm pr timeline-expand 2 --pr 77928 --repo PaddlePaddle/Paddle" in out
1034+
assert "gh llm pr review-expand PRR_mock --pr 77928 --repo PaddlePaddle/Paddle" in out
1035+
1036+
10231037
TEST_BASE_PAGE_SIZE = 2
10241038

10251039

0 commit comments

Comments
 (0)