From 5fad0beb85c0129e63e32b0613ac917920b46de8 Mon Sep 17 00:00:00 2001 From: Luis Ball Date: Mon, 29 Dec 2025 00:47:00 -0800 Subject: [PATCH 1/4] feat: add use_badges parameter to build_table for private repo support - Add use_badges parameter to build_table function - Default (false) generates 3-column table without shields.io badges - When true, generates 4-column table with shields.io status badges - Update existing tests to use new default (no badges) - Add new tests for badges mode - Update main.rs call site to use new parameter --- src/main.rs | 9 +- src/markdown.rs | 241 ++++++++++++------ ...uild_table_all_closed_shows_checkmark.snap | 8 +- ...down__tests__build_table_linear_stack.snap | 10 +- ...down__tests__build_table_mixed_states.snap | 10 +- ...arkdown__tests__build_table_single_pr.snap | 6 +- ...kdown__tests__build_table_with_badges.snap | 9 + ..._build_table_with_badges_mixed_states.snap | 10 + ...wn__tests__build_table_with_closed_pr.snap | 6 +- ...own__tests__build_table_with_draft_pr.snap | 6 +- ...wn__tests__build_table_with_merged_pr.snap | 6 +- 11 files changed, 214 insertions(+), 107 deletions(-) create mode 100644 src/snapshots/gh_stack__markdown__tests__build_table_with_badges.snap create mode 100644 src/snapshots/gh_stack__markdown__tests__build_table_with_badges_mixed_states.snap diff --git a/src/main.rs b/src/main.rs index bc04cf0..f27eb6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -219,8 +219,13 @@ async fn main() -> Result<(), Box> { build_pr_stack_for_repo(&identifier, repository, &credentials, get_excluded(m)) .await?; - let table = - markdown::build_table(&stack, &identifier, m.value_of("prelude"), repository); + let table = markdown::build_table( + &stack, + &identifier, + m.value_of("prelude"), + repository, + false, + ); for (pr, _) in stack.iter() { println!("{}: {}", pr.number(), pr.title()); diff --git a/src/markdown.rs b/src/markdown.rs index 6bbf691..fa66e08 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -8,6 +8,7 @@ pub fn build_table( title: &str, prelude_path: Option<&str>, repository: &str, + use_badges: bool, ) -> String { let is_complete = deps .iter() @@ -27,89 +28,104 @@ pub fn build_table( out.push('\n'); } - out.push_str("| PR | Title | Status | Merges Into |\n"); - out.push_str("|:--:|:------|:-------|:-------------:|\n"); + if use_badges { + out.push_str("| PR | Title | Status | Merges Into |\n"); + out.push_str("|:--:|:------|:-------|:-----------:|\n"); + } else { + out.push_str("| PR | Title | Merges Into |\n"); + out.push_str("|:--:|:------|:-----------:|\n"); + } for (node, parent) in deps { - let review_state = match node.review_state() { - PullRequestReviewState::APPROVED => { - format!( - "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", - repository, - &node.number().to_string(), - "Approved" - ) - } - PullRequestReviewState::MERGED => { - format!( - "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", - repository, - &node.number().to_string(), - "%20" - ) - } - PullRequestReviewState::PENDING => { - format!( - "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", - repository, - &node.number().to_string(), - "Pending" - ) - } - PullRequestReviewState::CHANGES_REQUESTED => { - format!( - "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", - repository, - &node.number().to_string(), - "Changes Requested" - ) - } - PullRequestReviewState::DISMISSED => { - format!( - "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", - repository, - &node.number().to_string(), - "Dismissed" - ) - } - PullRequestReviewState::COMMENTED => { + let review_state = if use_badges { + let state = match node.review_state() { + PullRequestReviewState::APPROVED => { + format!( + "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", + repository, + &node.number().to_string(), + "Approved" + ) + } + PullRequestReviewState::MERGED => { + format!( + "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", + repository, + &node.number().to_string(), + "%20" + ) + } + PullRequestReviewState::PENDING => { + format!( + "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", + repository, + &node.number().to_string(), + "Pending" + ) + } + PullRequestReviewState::CHANGES_REQUESTED => { + format!( + "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", + repository, + &node.number().to_string(), + "Changes Requested" + ) + } + PullRequestReviewState::DISMISSED => { + format!( + "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", + repository, + &node.number().to_string(), + "Dismissed" + ) + } + PullRequestReviewState::COMMENTED => { + format!( + "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", + repository, + &node.number().to_string(), + "Commented" + ) + } + }; + + if node.review_state() != PullRequestReviewState::MERGED + && *node.state() == PullRequestStatus::Closed + { format!( "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", repository, &node.number().to_string(), - "Commented" + "Closed" ) + } else { + state } - }; - - let review_state = if node.review_state() != PullRequestReviewState::MERGED - && *node.state() == PullRequestStatus::Closed - { - format!( - "![](https://img.shields.io/github/pulls/detail/state/{}/{}?label={})", - repository, - &node.number().to_string(), - "Closed" - ) } else { - review_state + String::new() }; - let row = match (node.state(), parent) { - (_, None) => format!( - "|#{}|{}|{}|{}|\n", - node.number(), - node.title(), - review_state, - "-" - ), - (_, Some(parent)) => format!( - "|#{}|{}|{}|#{}|\n", - node.number(), - node.title(), - review_state, - parent.number(), - ), + let row = if use_badges { + match parent { + None => format!("|#{}|{}|{}|-|\n", node.number(), node.title(), review_state), + Some(parent) => format!( + "|#{}|{}|{}|#{}|\n", + node.number(), + node.title(), + review_state, + parent.number() + ), + } + } else { + match parent { + None => format!("|#{}|{}|-|\n", node.number(), node.title()), + Some(parent) => format!( + "|#{}|{}|#{}|\n", + node.number(), + node.title(), + parent.number() + ), + } }; out.push_str(&row); @@ -158,7 +174,7 @@ mod tests { ); let deps: FlatDep = vec![(pr, None)]; - let table = build_table(&deps, "JIRA-123", None, "user/repo"); + let table = build_table(&deps, "JIRA-123", None, "user/repo", false); insta::assert_snapshot!(table); } @@ -198,7 +214,7 @@ mod tests { (pr3.clone(), Some(pr2.clone())), ]; - let table = build_table(&deps, "STACK-456", None, "org/project"); + let table = build_table(&deps, "STACK-456", None, "org/project", false); insta::assert_snapshot!(table); } @@ -215,7 +231,7 @@ mod tests { ); let deps: FlatDep = vec![(pr, None)]; - let table = build_table(&deps, "DRAFT-TEST", None, "user/repo"); + let table = build_table(&deps, "DRAFT-TEST", None, "user/repo", false); insta::assert_snapshot!(table); } @@ -232,7 +248,7 @@ mod tests { ); let deps: FlatDep = vec![(pr, None)]; - let table = build_table(&deps, "CLOSED-TEST", None, "user/repo"); + let table = build_table(&deps, "CLOSED-TEST", None, "user/repo", false); insta::assert_snapshot!(table); } @@ -249,7 +265,7 @@ mod tests { ); let deps: FlatDep = vec![(pr, None)]; - let table = build_table(&deps, "MERGED-TEST", None, "user/repo"); + let table = build_table(&deps, "MERGED-TEST", None, "user/repo", false); insta::assert_snapshot!(table); } @@ -276,7 +292,7 @@ mod tests { let deps: FlatDep = vec![(pr1.clone(), None), (pr2.clone(), Some(pr1.clone()))]; - let table = build_table(&deps, "COMPLETE-STACK", None, "user/repo"); + let table = build_table(&deps, "COMPLETE-STACK", None, "user/repo", false); insta::assert_snapshot!(table); } @@ -316,7 +332,74 @@ mod tests { (pr3.clone(), Some(pr2.clone())), ]; - let table = build_table(&deps, "MIXED-STACK", None, "org/repo"); + let table = build_table(&deps, "MIXED-STACK", None, "org/repo", false); + insta::assert_snapshot!(table); + } + + #[test] + fn test_build_table_with_badges() { + let pr1 = make_pr( + 1, + "feature-1", + "main", + "Base feature", + PullRequestStatus::Open, + false, + None, + ); + let pr2 = make_pr( + 2, + "feature-2", + "feature-1", + "Second feature", + PullRequestStatus::Open, + false, + None, + ); + + let deps: FlatDep = vec![(pr1.clone(), None), (pr2.clone(), Some(pr1.clone()))]; + + let table = build_table(&deps, "BADGES-TEST", None, "org/project", true); + insta::assert_snapshot!(table); + } + + #[test] + fn test_build_table_with_badges_mixed_states() { + let pr1 = make_pr( + 1, + "feature-1", + "main", + "Merged base", + PullRequestStatus::Closed, + false, + Some("2024-01-15T10:00:00Z".to_string()), + ); + let pr2 = make_pr( + 2, + "feature-2", + "feature-1", + "Open follow-up", + PullRequestStatus::Open, + false, + None, + ); + let pr3 = make_pr( + 3, + "feature-3", + "feature-2", + "Draft WIP", + PullRequestStatus::Open, + true, + None, + ); + + let deps: FlatDep = vec![ + (pr1.clone(), None), + (pr2.clone(), Some(pr1.clone())), + (pr3.clone(), Some(pr2.clone())), + ]; + + let table = build_table(&deps, "BADGES-MIXED", None, "org/repo", true); insta::assert_snapshot!(table); } } diff --git a/src/snapshots/gh_stack__markdown__tests__build_table_all_closed_shows_checkmark.snap b/src/snapshots/gh_stack__markdown__tests__build_table_all_closed_shows_checkmark.snap index dc594f1..c63d242 100644 --- a/src/snapshots/gh_stack__markdown__tests__build_table_all_closed_shows_checkmark.snap +++ b/src/snapshots/gh_stack__markdown__tests__build_table_all_closed_shows_checkmark.snap @@ -3,7 +3,7 @@ source: src/markdown.rs expression: table --- ### ✅ Stacked PR Chain: COMPLETE-STACK -| PR | Title | Status | Merges Into | -|:--:|:------|:-------|:-------------:| -|#1|~~First~~|![](https://img.shields.io/github/pulls/detail/state/user/repo/1?label=Closed)|-| -|#2|~~Second~~|![](https://img.shields.io/github/pulls/detail/state/user/repo/2?label=Closed)|#1| +| PR | Title | Merges Into | +|:--:|:------|:-----------:| +|#1|~~First~~|-| +|#2|~~Second~~|#1| diff --git a/src/snapshots/gh_stack__markdown__tests__build_table_linear_stack.snap b/src/snapshots/gh_stack__markdown__tests__build_table_linear_stack.snap index 5fc97f8..c74ff11 100644 --- a/src/snapshots/gh_stack__markdown__tests__build_table_linear_stack.snap +++ b/src/snapshots/gh_stack__markdown__tests__build_table_linear_stack.snap @@ -3,8 +3,8 @@ source: src/markdown.rs expression: table --- ### Stacked PR Chain: STACK-456 -| PR | Title | Status | Merges Into | -|:--:|:------|:-------|:-------------:| -|#1|Base feature|![](https://img.shields.io/github/pulls/detail/state/org/project/1?label=Pending)|-| -|#2|Second feature|![](https://img.shields.io/github/pulls/detail/state/org/project/2?label=Pending)|#1| -|#3|Third feature|![](https://img.shields.io/github/pulls/detail/state/org/project/3?label=Pending)|#2| +| PR | Title | Merges Into | +|:--:|:------|:-----------:| +|#1|Base feature|-| +|#2|Second feature|#1| +|#3|Third feature|#2| diff --git a/src/snapshots/gh_stack__markdown__tests__build_table_mixed_states.snap b/src/snapshots/gh_stack__markdown__tests__build_table_mixed_states.snap index c140dac..52c1045 100644 --- a/src/snapshots/gh_stack__markdown__tests__build_table_mixed_states.snap +++ b/src/snapshots/gh_stack__markdown__tests__build_table_mixed_states.snap @@ -3,8 +3,8 @@ source: src/markdown.rs expression: table --- ### Stacked PR Chain: MIXED-STACK -| PR | Title | Status | Merges Into | -|:--:|:------|:-------|:-------------:| -|#1|~~Merged base~~|![](https://img.shields.io/github/pulls/detail/state/org/repo/1?label=%20)|-| -|#2|Open follow-up|![](https://img.shields.io/github/pulls/detail/state/org/repo/2?label=Pending)|#1| -|#3|*(Draft) Draft WIP*|![](https://img.shields.io/github/pulls/detail/state/org/repo/3?label=Pending)|#2| +| PR | Title | Merges Into | +|:--:|:------|:-----------:| +|#1|~~Merged base~~|-| +|#2|Open follow-up|#1| +|#3|*(Draft) Draft WIP*|#2| diff --git a/src/snapshots/gh_stack__markdown__tests__build_table_single_pr.snap b/src/snapshots/gh_stack__markdown__tests__build_table_single_pr.snap index 48fd35f..1848327 100644 --- a/src/snapshots/gh_stack__markdown__tests__build_table_single_pr.snap +++ b/src/snapshots/gh_stack__markdown__tests__build_table_single_pr.snap @@ -3,6 +3,6 @@ source: src/markdown.rs expression: table --- ### Stacked PR Chain: JIRA-123 -| PR | Title | Status | Merges Into | -|:--:|:------|:-------|:-------------:| -|#1|Add new feature|![](https://img.shields.io/github/pulls/detail/state/user/repo/1?label=Pending)|-| +| PR | Title | Merges Into | +|:--:|:------|:-----------:| +|#1|Add new feature|-| diff --git a/src/snapshots/gh_stack__markdown__tests__build_table_with_badges.snap b/src/snapshots/gh_stack__markdown__tests__build_table_with_badges.snap new file mode 100644 index 0000000..1f28de4 --- /dev/null +++ b/src/snapshots/gh_stack__markdown__tests__build_table_with_badges.snap @@ -0,0 +1,9 @@ +--- +source: src/markdown.rs +expression: table +--- +### Stacked PR Chain: BADGES-TEST +| PR | Title | Status | Merges Into | +|:--:|:------|:-------|:-----------:| +|#1|Base feature|![](https://img.shields.io/github/pulls/detail/state/org/project/1?label=Pending)|-| +|#2|Second feature|![](https://img.shields.io/github/pulls/detail/state/org/project/2?label=Pending)|#1| diff --git a/src/snapshots/gh_stack__markdown__tests__build_table_with_badges_mixed_states.snap b/src/snapshots/gh_stack__markdown__tests__build_table_with_badges_mixed_states.snap new file mode 100644 index 0000000..44d305b --- /dev/null +++ b/src/snapshots/gh_stack__markdown__tests__build_table_with_badges_mixed_states.snap @@ -0,0 +1,10 @@ +--- +source: src/markdown.rs +expression: table +--- +### Stacked PR Chain: BADGES-MIXED +| PR | Title | Status | Merges Into | +|:--:|:------|:-------|:-----------:| +|#1|~~Merged base~~|![](https://img.shields.io/github/pulls/detail/state/org/repo/1?label=%20)|-| +|#2|Open follow-up|![](https://img.shields.io/github/pulls/detail/state/org/repo/2?label=Pending)|#1| +|#3|*(Draft) Draft WIP*|![](https://img.shields.io/github/pulls/detail/state/org/repo/3?label=Pending)|#2| diff --git a/src/snapshots/gh_stack__markdown__tests__build_table_with_closed_pr.snap b/src/snapshots/gh_stack__markdown__tests__build_table_with_closed_pr.snap index 97bb560..448e147 100644 --- a/src/snapshots/gh_stack__markdown__tests__build_table_with_closed_pr.snap +++ b/src/snapshots/gh_stack__markdown__tests__build_table_with_closed_pr.snap @@ -3,6 +3,6 @@ source: src/markdown.rs expression: table --- ### ✅ Stacked PR Chain: CLOSED-TEST -| PR | Title | Status | Merges Into | -|:--:|:------|:-------|:-------------:| -|#1|~~Completed feature~~|![](https://img.shields.io/github/pulls/detail/state/user/repo/1?label=Closed)|-| +| PR | Title | Merges Into | +|:--:|:------|:-----------:| +|#1|~~Completed feature~~|-| diff --git a/src/snapshots/gh_stack__markdown__tests__build_table_with_draft_pr.snap b/src/snapshots/gh_stack__markdown__tests__build_table_with_draft_pr.snap index 88b5f95..fbcdc2b 100644 --- a/src/snapshots/gh_stack__markdown__tests__build_table_with_draft_pr.snap +++ b/src/snapshots/gh_stack__markdown__tests__build_table_with_draft_pr.snap @@ -3,6 +3,6 @@ source: src/markdown.rs expression: table --- ### Stacked PR Chain: DRAFT-TEST -| PR | Title | Status | Merges Into | -|:--:|:------|:-------|:-------------:| -|#1|*(Draft) Work in progress*|![](https://img.shields.io/github/pulls/detail/state/user/repo/1?label=Pending)|-| +| PR | Title | Merges Into | +|:--:|:------|:-----------:| +|#1|*(Draft) Work in progress*|-| diff --git a/src/snapshots/gh_stack__markdown__tests__build_table_with_merged_pr.snap b/src/snapshots/gh_stack__markdown__tests__build_table_with_merged_pr.snap index 0de16d2..3401944 100644 --- a/src/snapshots/gh_stack__markdown__tests__build_table_with_merged_pr.snap +++ b/src/snapshots/gh_stack__markdown__tests__build_table_with_merged_pr.snap @@ -3,6 +3,6 @@ source: src/markdown.rs expression: table --- ### ✅ Stacked PR Chain: MERGED-TEST -| PR | Title | Status | Merges Into | -|:--:|:------|:-------|:-------------:| -|#1|~~Merged feature~~|![](https://img.shields.io/github/pulls/detail/state/user/repo/1?label=%20)|-| +| PR | Title | Merges Into | +|:--:|:------|:-----------:| +|#1|~~Merged feature~~|-| From 915cf7a044bb14e3a6cccb18e508af022fdac508 Mon Sep 17 00:00:00 2001 From: Luis Ball Date: Mon, 29 Dec 2025 00:47:52 -0800 Subject: [PATCH 2/4] feat: add --badges flag to annotate command - Add --badges CLI flag to enable shields.io status badges - Default behavior now generates simpler table without badges - Works with private repositories by default --- src/main.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index f27eb6e..4ae6787 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,6 +41,11 @@ fn clap<'a, 'b>() -> App<'a, 'b> { .takes_value(true) .help("PR title prefix identifier to remove from the title"); + let badges = Arg::with_name("badges") + .long("badges") + .takes_value(false) + .help("Use shields.io badges for PR status (requires public repo visibility)"); + let annotate = SubCommand::with_name("annotate") .about("Annotate the descriptions of all PRs in a stack with metadata about all PRs in the stack") .setting(AppSettings::ArgRequiredElseHelp) @@ -49,6 +54,7 @@ fn clap<'a, 'b>() -> App<'a, 'b> { .arg(repository.clone()) .arg(ci.clone()) .arg(prefix.clone()) + .arg(badges.clone()) .arg(Arg::with_name("prelude") .long("prelude") .short("p") @@ -219,12 +225,13 @@ async fn main() -> Result<(), Box> { build_pr_stack_for_repo(&identifier, repository, &credentials, get_excluded(m)) .await?; + let use_badges = m.is_present("badges"); let table = markdown::build_table( &stack, &identifier, m.value_of("prelude"), repository, - false, + use_badges, ); for (pr, _) in stack.iter() { From 2411880fb861f3deb6fd929c4c9da9082420e170 Mon Sep 17 00:00:00 2001 From: Luis Ball Date: Mon, 29 Dec 2025 00:48:47 -0800 Subject: [PATCH 3/4] docs: update README for private repo support and --badges flag - Remove public visibility assumption from requirements - Add --badges flag documentation - Add example of new default table format - Explain GitHub's native PR autolinking feature --- README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6037440..053f14d 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,6 @@ This tool assumes that: - All PRs in a single "stack" all have a unique identifier in their title (I typically use a Jira ticket number for this). - All PRs in the stack live in a single GitHub repository. - All remote branches that these PRs represent have local branches named identically. -- Your PRs are publicly viewable by all GitHub users. - - This assumption is due to how the Markdown table is uses https://shields.io to render badges that auto-update based on your PR status. - - example URL: https://img.shields.io/github/pulls/detail/state/{{your-user-or-org}}/{{your-repository}}/{{the-pr-number}} It then looks for all PRs containing this identifier and builds a dependency graph in memory. @@ -107,9 +104,10 @@ $ gh-stack annotate 'stack-identifier' -r '' --prefix '#' # contents of `filename.txt`. $ gh-stack annotate 'stack-identifier' -p filename.txt -# Same as above, but precede the markdown table with the -# contents of `filename.txt`. -$ gh-stack annotate 'stack-identifier' -p filename.txt +# Same as above, but with shields.io status badges (requires public repo). +# By default, annotations use GitHub's native PR autolinking which works +# with both public and private repositories. +$ gh-stack annotate 'stack-identifier' --badges # Automatically update the entire stack, both locally and remotely. # WARNING: This operation modifies local branches and force-pushes. @@ -195,6 +193,19 @@ _This is a quick overview of the ways this tool could be used in practice._ ``` This (idempotently) adds a table like this to the description of every PR in the stack: + + ```markdown + ### Stacked PR Chain: EXAMPLE-13799 + | PR | Title | Merges Into | + |:--:|:------|:-----------:| + |#1|[EXAMPLE-13799] PR for branch `first`|-| + |#2|[EXAMPLE-13799] PR for branch `second`|#1| + |#3|[EXAMPLE-13799] PR for branch `third`|#2| + ``` + + GitHub automatically converts `#1`, `#2`, `#3` to clickable links. Hovering over them shows PR details including current status. + + For public repositories, you can use `--badges` to add shields.io status badges: 7. Make changes to a branch that rewrites commits in some way (amend, remove a commit, combine commits): From f48d915299a4200568abe8e722f18e4a304d405b Mon Sep 17 00:00:00 2001 From: Luis Ball Date: Mon, 29 Dec 2025 00:55:54 -0800 Subject: [PATCH 4/4] docs: add stacked PR workflow to AGENTS.md Dogfood gh-stack by documenting how to use it for stacked PRs in this repo. --- AGENTS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 5180e1a..a0b9aaf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,35 @@ docs: update documentation chore: maintenance tasks ``` +## Stacked PRs + +When creating stacked PRs, use `gh-stack` to manage and annotate them: + +1. Create branches that build on each other: + ```bash + git checkout -b feat/my-feature-part1 + # make changes, commit + git checkout -b feat/my-feature-part2 + # make changes, commit + ``` + +2. Push branches and create PRs with proper base branches: + ```bash + git push origin feat/my-feature-part1 feat/my-feature-part2 + gh pr create --base master --head feat/my-feature-part1 --title "[STACK-ID] part 1" + gh pr create --base feat/my-feature-part1 --head feat/my-feature-part2 --title "[STACK-ID] part 2" + ``` + +3. Annotate PRs with stack info: + ```bash + gh-stack annotate 'STACK-ID' -r 'luqven/gh-stack' --ci + ``` + +4. After rebasing, update the stack: + ```bash + gh-stack autorebase 'STACK-ID' -r 'luqven/gh-stack' -C . --ci + ``` + ## Function Signatures Pass primitives directly, keep arg count low: