From 89bbf5c64b3870de1bea6a6d2b79ae59c5028bcc Mon Sep 17 00:00:00 2001 From: Sajid Bashar Date: Tue, 21 Jul 2026 14:40:06 -0700 Subject: [PATCH] Print PR URL summary at end of submit and view (#50) Add print_pr_summary(), which prints a plain, copy-pasteable list of the stack's PR URLs after `submit` and `view`. Entries without a PR are skipped, and nothing is printed when the stack has no PRs (e.g. `view` before the stack has been exported). The summary is printed unconditionally rather than behind --show-tips, since the PR URLs are core output rather than a usage hint. Closes #50. --- CHANGELOG.md | 2 ++ src/stack_pr/cli.py | 17 +++++++++++++++++ tests/test_misc.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b103f..d92f14e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Top of tree +* Print a summary of the stack's PR URLs at the end of `submit` and `view`. (#50) + # Version 0.1.3 * Fix a bug with replacing $USERNAME in the branch name. (#44) diff --git a/src/stack_pr/cli.py b/src/stack_pr/cli.py index cf52324..ccb348a 100755 --- a/src/stack_pr/cli.py +++ b/src/stack_pr/cli.py @@ -563,6 +563,19 @@ def print_stack(st: list[StackEntry], *, links: bool, level: int = 1) -> None: log(" * " + e.pprint(links=links), level=level) +def print_pr_summary(st: list[StackEntry], *, level: int = 1) -> None: + # Print a plain, copy-pasteable list of the stack's PR URLs. Entries without a + # PR are skipped, and nothing is printed at all if the stack has no PRs (e.g. + # a 'view' on a stack that hasn't been exported yet). + if not any(e.has_pr() for e in st): + return + log(b("PRs:"), level=level) + for e in reversed(st): + if not e.has_pr(): + continue + log(" * " + e.pr, level=level) + + def draft_bitmask_type(value: str) -> list[bool]: # Validate that only 0s and 1s are present if value and not set(value).issubset({"0", "1"}): @@ -1157,6 +1170,9 @@ def command_submit( delete_local_branches(st, verbose=args.verbose) print_tips_after_export(st, args) + # Printed unconditionally (not gated behind --show-tips): the PR URLs are core + # output, not a usage hint. + print_pr_summary(st) log(h(blue("SUCCESS!")), level=1) @@ -1484,6 +1500,7 @@ def command_view(args: CommonArgs) -> None: set_base_branches(st, target=args.target) print_stack(st, links=args.hyperlinks) print_tips_after_view(st, args) + print_pr_summary(st) log(h(blue("SUCCESS!"))) diff --git a/tests/test_misc.py b/tests/test_misc.py index 5517c10..0798e41 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys import tempfile from pathlib import Path @@ -7,11 +9,14 @@ import pytest from stack_pr.cli import ( + CommitHeader, + StackEntry, generate_available_branch_name, generate_branch_name, get_branch_id, get_gh_username, get_taken_branch_ids, + print_pr_summary, ) from stack_pr.git import git_config, is_rebase_in_progress @@ -123,3 +128,40 @@ def test_is_rebase_in_progress() -> None: # Test with None repo_dir (current directory) # This should not raise an error even if .git doesn't exist in cwd assert not is_rebase_in_progress(None) + + +def _entry(pr: str | None) -> StackEntry: + # print_pr_summary only reads .has_pr()/.pr, so an empty commit header is fine. + return StackEntry(CommitHeader(""), _pr=pr) + + +def test_print_pr_summary_lists_prs_top_of_stack_first( + capsys: pytest.CaptureFixture[str], +) -> None: + st = [ + _entry("https://github.com/o/r/pull/1"), + _entry("https://github.com/o/r/pull/2"), + ] + print_pr_summary(st) + out = capsys.readouterr().out + assert "https://github.com/o/r/pull/1" in out + assert "https://github.com/o/r/pull/2" in out + # Ordering matches print_stack (top of the stack, i.e. the last entry, first). + assert out.index("pull/2") < out.index("pull/1") + + +def test_print_pr_summary_skips_entries_without_a_pr( + capsys: pytest.CaptureFixture[str], +) -> None: + st = [_entry("https://github.com/o/r/pull/1"), _entry(None)] + print_pr_summary(st) + out = capsys.readouterr().out + assert "https://github.com/o/r/pull/1" in out + assert out.count("http") == 1 + + +def test_print_pr_summary_prints_nothing_when_no_prs( + capsys: pytest.CaptureFixture[str], +) -> None: + print_pr_summary([_entry(None), _entry(None)]) + assert capsys.readouterr().out == ""