@@ -18,7 +18,11 @@ const StackCommentMarker = "<!-- gh-stack:nav -->"
1818//
1919// Only the current stack is rendered: the path from root to the current branch,
2020// plus all descendants of the current branch. Sibling stacks are excluded.
21- func GenerateStackComment (root * tree.Node , currentBranch , trunk , repoURL string , prInfo map [int ]PRInfo ) string {
21+ //
22+ // If remoteBranches is non-nil, branches without PRs that don't exist on the
23+ // remote are omitted (their children are promoted up a level). Pass nil to
24+ // disable this filtering.
25+ func GenerateStackComment (root * tree.Node , currentBranch , trunk , repoURL string , prInfo map [int ]PRInfo , remoteBranches map [string ]bool ) string {
2226 var sb strings.Builder
2327
2428 // Find the current node
@@ -40,13 +44,18 @@ func GenerateStackComment(root *tree.Node, currentBranch, trunk, repoURL string,
4044 sb .WriteString (StackCommentMarker )
4145 sb .WriteString ("\n " )
4246
47+ // Find the effective parent: the nearest ancestor that would be visible
48+ // in the rendered tree. A node is visible if it has a PR, exists on the
49+ // remote, or remote filtering is disabled.
50+ effectiveParent := findEffectiveParent (currentNode , remoteBranches )
51+
4352 // Add warning if not targeting trunk
44- if currentNode . Parent != nil && currentNode . Parent .Name != trunk {
53+ if effectiveParent != nil && effectiveParent .Name != trunk {
4554 sb .WriteString ("> [!WARNING]\n " )
46- sb .WriteString (fmt .Sprintf ("> This PR is part of a stack and targets branch `%s`, not `%s`.\n " , currentNode . Parent .Name , trunk ))
55+ sb .WriteString (fmt .Sprintf ("> This PR is part of a stack and targets branch `%s`, not `%s`.\n " , effectiveParent .Name , trunk ))
4756
4857 // Build parent PR reference if available
49- parentPR := currentNode . Parent .PR
58+ parentPR := effectiveParent .PR
5059 if parentPR > 0 {
5160 parentURL := fmt .Sprintf ("%s/pull/%d" , repoURL , parentPR )
5261 linkText := fmt .Sprintf ("#%d" , parentPR )
@@ -63,7 +72,7 @@ func GenerateStackComment(root *tree.Node, currentBranch, trunk, repoURL string,
6372 sb .WriteString ("### :books: Pull Request Stack\n \n " )
6473
6574 // Render tree from root as nested markdown list, filtered to current stack
66- renderTree (& sb , root , currentBranch , repoURL , prInfo , 0 , ancestorPath )
75+ renderTree (& sb , root , currentBranch , repoURL , prInfo , 0 , ancestorPath , remoteBranches )
6776
6877 sb .WriteString ("\n ---\n " )
6978 sb .WriteString ("*Managed by [gh-stack](https://github.com/boneskull/gh-stack)*\n " )
@@ -87,44 +96,70 @@ func collectPRNumbersRecursive(node *tree.Node, numbers *[]int) {
8796 }
8897}
8998
99+ // findEffectiveParent walks up from a node's parent to find the nearest ancestor
100+ // that would be rendered in the comment. A node is "visible" if it has a PR or
101+ // exists on the remote (or if remote filtering is disabled). Returns nil if the
102+ // node has no parent (i.e., it's the root).
103+ func findEffectiveParent (node * tree.Node , remoteBranches map [string ]bool ) * tree.Node {
104+ for p := node .Parent ; p != nil ; p = p .Parent {
105+ if p .PR > 0 || remoteBranches == nil || remoteBranches [p .Name ] {
106+ return p
107+ }
108+ }
109+ return nil
110+ }
111+
90112// renderTree recursively renders the tree structure as nested markdown lists.
91113//
92114// The ancestorPath set controls which children are rendered at each level:
93115// - For nodes that are ancestors of the current branch (on the path but not
94116// the current branch itself), only the child on the ancestor path is shown.
95117// - For the current branch and all its descendants, all children are shown.
96118//
97- // This ensures only the current stack is rendered, not sibling stacks.
98- func renderTree ( sb * strings. Builder , node * tree. Node , currentBranch , repoURL string , prInfo map [ int ] PRInfo , depth int , ancestorPath map [ string ] bool ) {
99- // Build prefix based on depth (2 spaces per level for markdown nested lists)
100- prefix := strings . Repeat ( " " , depth ) + "- "
101-
119+ // If remoteBranches is non-nil, nodes without PRs that aren't on the remote
120+ // are collapsed: their children are rendered at the skipped node's depth
121+ // instead of being indented further. This prevents local-only branches from
122+ // appearing in the GitHub comment.
123+ func renderTree ( sb * strings. Builder , node * tree. Node , currentBranch , repoURL string , prInfo map [ int ] PRInfo , depth int , ancestorPath map [ string ] bool , remoteBranches map [ string ] bool ) {
102124 isCurrent := node .Name == currentBranch
103125
104- // Format: "[Title #N](url) - branch: `name`" or "branch: `name`" if no PR
105- if node .PR > 0 {
106- prURL := fmt .Sprintf ("%s/pull/%d" , repoURL , node .PR )
107- linkText := fmt .Sprintf ("#%d" , node .PR )
108- if info , ok := prInfo [node .PR ]; ok && info .Title != "" {
109- linkText = fmt .Sprintf ("%s #%d" , info .Title , node .PR )
110- }
126+ // Skip branches that have no PR and don't exist on the remote.
127+ // The current branch is never skipped (defensive: it should always have
128+ // a PR since we're generating a comment for it, but just in case).
129+ // When remoteBranches is nil, no filtering is applied.
130+ skipNode := node .PR == 0 && ! isCurrent && remoteBranches != nil && ! remoteBranches [node .Name ]
131+
132+ nextDepth := depth
133+ if ! skipNode {
134+ // Build prefix based on depth (2 spaces per level for markdown nested lists)
135+ prefix := strings .Repeat (" " , depth ) + "- "
136+
137+ // Format: "[Title #N](url) - branch: `name`" or "branch: `name`" if no PR
138+ if node .PR > 0 {
139+ prURL := fmt .Sprintf ("%s/pull/%d" , repoURL , node .PR )
140+ linkText := fmt .Sprintf ("#%d" , node .PR )
141+ if info , ok := prInfo [node .PR ]; ok && info .Title != "" {
142+ linkText = fmt .Sprintf ("%s #%d" , info .Title , node .PR )
143+ }
111144
112- if isCurrent {
113- // Bold the link for current PR
114- fmt .Fprintf (sb , "%s**[%s](%s)** - branch: `%s` *(this PR)*" , prefix , linkText , prURL , node .Name )
115- } else {
116- fmt .Fprintf (sb , "%s[%s](%s) - branch: `%s`" , prefix , linkText , prURL , node .Name )
117- }
118- } else {
119- // No PR - show "branch: `name`"
120- if isCurrent {
121- fmt .Fprintf (sb , "%s**branch: `%s`** *(this PR)*" , prefix , node .Name )
145+ if isCurrent {
146+ // Bold the link for current PR
147+ fmt .Fprintf (sb , "%s**[%s](%s)** - branch: `%s` *(this PR)*" , prefix , linkText , prURL , node .Name )
148+ } else {
149+ fmt .Fprintf (sb , "%s[%s](%s) - branch: `%s`" , prefix , linkText , prURL , node .Name )
150+ }
122151 } else {
123- fmt .Fprintf (sb , "%sbranch: `%s`" , prefix , node .Name )
152+ // No PR - show "branch: `name`"
153+ if isCurrent {
154+ fmt .Fprintf (sb , "%s**branch: `%s`** *(this PR)*" , prefix , node .Name )
155+ } else {
156+ fmt .Fprintf (sb , "%sbranch: `%s`" , prefix , node .Name )
157+ }
124158 }
125- }
126159
127- sb .WriteString ("\n " )
160+ sb .WriteString ("\n " )
161+ nextDepth = depth + 1
162+ }
128163
129164 // For ancestor nodes (above the current branch), only render the child
130165 // that leads to the current branch. For the current branch and its
@@ -134,7 +169,7 @@ func renderTree(sb *strings.Builder, node *tree.Node, currentBranch, repoURL str
134169 if isAncestorAboveCurrent && ! ancestorPath [child .Name ] {
135170 continue
136171 }
137- renderTree (sb , child , currentBranch , repoURL , prInfo , depth + 1 , ancestorPath )
172+ renderTree (sb , child , currentBranch , repoURL , prInfo , nextDepth , ancestorPath , remoteBranches )
138173 }
139174}
140175
@@ -177,7 +212,10 @@ func (c *Client) CreateOrUpdateStackComment(prNumber int, body string) error {
177212
178213// GenerateAndPostStackComment generates and posts/updates a stack comment for a PR.
179214// It fetches PR titles via GraphQL and renders the full comment.
180- func (c * Client ) GenerateAndPostStackComment (root * tree.Node , branch , trunk string , prNumber int ) error {
215+ //
216+ // If remoteBranches is non-nil, branches without PRs that don't exist on the
217+ // remote are omitted from the rendered comment. Pass nil to disable filtering.
218+ func (c * Client ) GenerateAndPostStackComment (root * tree.Node , branch , trunk string , prNumber int , remoteBranches map [string ]bool ) error {
181219 // Collect all PR numbers from the tree
182220 prNumbers := CollectPRNumbers (root )
183221
@@ -188,7 +226,7 @@ func (c *Client) GenerateAndPostStackComment(root *tree.Node, branch, trunk stri
188226 prInfo = make (map [int ]PRInfo )
189227 }
190228
191- comment := GenerateStackComment (root , branch , trunk , c .RepoURL (), prInfo )
229+ comment := GenerateStackComment (root , branch , trunk , c .RepoURL (), prInfo , remoteBranches )
192230 if comment == "" {
193231 return nil
194232 }
0 commit comments