Skip to content

Commit c31fa5c

Browse files
authored
fix(github): scope PR stack comment to current stack only (#36)
- Filter rendered tree to only show the path from trunk to the current branch and its descendants; sibling stacks are excluded. - Branches without PRs now display `branch: `name`` instead of bare ``name`` for consistency with the PR line format.
1 parent 0d48b58 commit c31fa5c

File tree

2 files changed

+103
-15
lines changed

2 files changed

+103
-15
lines changed

internal/github/comments.go

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ const StackCommentMarker = "<!-- gh-stack:nav -->"
1515
// It includes a warning if the PR targets a non-trunk branch.
1616
// The repoURL should be the base repository URL (e.g., "https://github.com/owner/repo").
1717
// The prInfo map provides titles for PRs (keyed by PR number).
18+
//
19+
// Only the current stack is rendered: the path from root to the current branch,
20+
// plus all descendants of the current branch. Sibling stacks are excluded.
1821
func GenerateStackComment(root *tree.Node, currentBranch, trunk, repoURL string, prInfo map[int]PRInfo) string {
1922
var sb strings.Builder
2023

@@ -24,6 +27,15 @@ func GenerateStackComment(root *tree.Node, currentBranch, trunk, repoURL string,
2427
return ""
2528
}
2629

30+
// Build ancestor path: the set of branch names from root down to (and
31+
// including) the current branch. When rendering, only children on this
32+
// path are shown for ancestor nodes; descendants of the current branch
33+
// are always shown in full.
34+
ancestorPath := make(map[string]bool)
35+
for n := currentNode; n != nil; n = n.Parent {
36+
ancestorPath[n.Name] = true
37+
}
38+
2739
// Start with marker
2840
sb.WriteString(StackCommentMarker)
2941
sb.WriteString("\n")
@@ -50,8 +62,8 @@ func GenerateStackComment(root *tree.Node, currentBranch, trunk, repoURL string,
5062
// Stack header
5163
sb.WriteString("### :books: Pull Request Stack\n\n")
5264

53-
// Render tree from root as nested markdown list
54-
renderTree(&sb, root, currentBranch, repoURL, prInfo, 0)
65+
// Render tree from root as nested markdown list, filtered to current stack
66+
renderTree(&sb, root, currentBranch, repoURL, prInfo, 0, ancestorPath)
5567

5668
sb.WriteString("\n---\n")
5769
sb.WriteString("*Managed by [gh-stack](https://github.com/boneskull/gh-stack)*\n")
@@ -76,13 +88,20 @@ func collectPRNumbersRecursive(node *tree.Node, numbers *[]int) {
7688
}
7789

7890
// renderTree recursively renders the tree structure as nested markdown lists.
79-
func renderTree(sb *strings.Builder, node *tree.Node, currentBranch, repoURL string, prInfo map[int]PRInfo, depth int) {
91+
//
92+
// The ancestorPath set controls which children are rendered at each level:
93+
// - For nodes that are ancestors of the current branch (on the path but not
94+
// the current branch itself), only the child on the ancestor path is shown.
95+
// - For the current branch and all its descendants, all children are shown.
96+
//
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) {
8099
// Build prefix based on depth (2 spaces per level for markdown nested lists)
81100
prefix := strings.Repeat(" ", depth) + "- "
82101

83102
isCurrent := node.Name == currentBranch
84103

85-
// Format: "[Title #N](url) - branch: `name`" or just branch name if no PR
104+
// Format: "[Title #N](url) - branch: `name`" or "branch: `name`" if no PR
86105
if node.PR > 0 {
87106
prURL := fmt.Sprintf("%s/pull/%d", repoURL, node.PR)
88107
linkText := fmt.Sprintf("#%d", node.PR)
@@ -97,19 +116,25 @@ func renderTree(sb *strings.Builder, node *tree.Node, currentBranch, repoURL str
97116
fmt.Fprintf(sb, "%s[%s](%s) - branch: `%s`", prefix, linkText, prURL, node.Name)
98117
}
99118
} else {
100-
// No PR - just show branch name (e.g., trunk)
119+
// No PR - show "branch: `name`"
101120
if isCurrent {
102-
fmt.Fprintf(sb, "%s**`%s`**", prefix, node.Name)
121+
fmt.Fprintf(sb, "%s**branch: `%s`** *(this PR)*", prefix, node.Name)
103122
} else {
104-
fmt.Fprintf(sb, "%s`%s`", prefix, node.Name)
123+
fmt.Fprintf(sb, "%sbranch: `%s`", prefix, node.Name)
105124
}
106125
}
107126

108127
sb.WriteString("\n")
109128

110-
// Render children
129+
// For ancestor nodes (above the current branch), only render the child
130+
// that leads to the current branch. For the current branch and its
131+
// descendants, render all children.
132+
isAncestorAboveCurrent := ancestorPath[node.Name] && !isCurrent
111133
for _, child := range node.Children {
112-
renderTree(sb, child, currentBranch, repoURL, prInfo, depth+1)
134+
if isAncestorAboveCurrent && !ancestorPath[child.Name] {
135+
continue
136+
}
137+
renderTree(sb, child, currentBranch, repoURL, prInfo, depth+1, ancestorPath)
113138
}
114139
}
115140

internal/github/comments_test.go

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ func TestGenerateStackComment(t *testing.T) {
126126
if strings.Contains(comment, "```") {
127127
t.Error("should not use code blocks")
128128
}
129-
// Trunk has no PR, shown as backticked branch name
130-
if !strings.Contains(comment, "- `main`") {
131-
t.Error("should use markdown list format with trunk in backticks")
129+
// Trunk has no PR, shown with "branch:" prefix
130+
if !strings.Contains(comment, "- branch: `main`") {
131+
t.Error("should use markdown list format with 'branch:' prefix for trunk")
132132
}
133133
})
134134

@@ -141,6 +141,30 @@ func TestGenerateStackComment(t *testing.T) {
141141
t.Error("should fallback to '#N' when title not available")
142142
}
143143
})
144+
145+
t.Run("no-PR branches show branch prefix", func(t *testing.T) {
146+
// Branch without a PR in the middle of a stack
147+
noPRRoot := &tree.Node{Name: "main"}
148+
noPRMiddle := &tree.Node{Name: "wip-branch", PR: 0, Parent: noPRRoot}
149+
noPRChild := &tree.Node{Name: "child-branch", PR: 5, Parent: noPRMiddle}
150+
151+
noPRRoot.Children = []*tree.Node{noPRMiddle}
152+
noPRMiddle.Children = []*tree.Node{noPRChild}
153+
154+
childPRInfo := makePRInfo(struct {
155+
num int
156+
title string
157+
}{5, "Child feature"})
158+
comment := GenerateStackComment(noPRRoot, "child-branch", "main", testRepoURL, childPRInfo)
159+
160+
// Trunk and middle branch both have no PR; both should show "branch:" prefix
161+
if !strings.Contains(comment, "- branch: `main`") {
162+
t.Error("trunk without PR should show 'branch:' prefix")
163+
}
164+
if !strings.Contains(comment, "- branch: `wip-branch`") {
165+
t.Error("branch without PR should show 'branch:' prefix")
166+
}
167+
})
144168
}
145169

146170
func TestGenerateStackComment_EdgeCases(t *testing.T) {
@@ -192,7 +216,7 @@ func TestGenerateStackComment_EdgeCases(t *testing.T) {
192216
}
193217
})
194218

195-
t.Run("branch with siblings", func(t *testing.T) {
219+
t.Run("branch with siblings only shows current stack", func(t *testing.T) {
196220
root := &tree.Node{Name: "main"}
197221
a := &tree.Node{Name: "feature-a", PR: 1, Parent: root}
198222
b := &tree.Node{Name: "feature-b", PR: 2, Parent: root}
@@ -213,8 +237,47 @@ func TestGenerateStackComment_EdgeCases(t *testing.T) {
213237
if !strings.Contains(comment, "feature-a") {
214238
t.Error("should contain current branch")
215239
}
216-
if !strings.Contains(comment, "feature-b") {
217-
t.Error("should contain sibling branch")
240+
if strings.Contains(comment, "feature-b") {
241+
t.Error("should NOT contain sibling branch from a different stack")
242+
}
243+
})
244+
245+
t.Run("multiple stacks only shows current one", func(t *testing.T) {
246+
// Tree: main -> {stack1-base (#1) -> stack1-child (#2), unrelated (#3)}
247+
root := &tree.Node{Name: "main"}
248+
stack1Base := &tree.Node{Name: "stack1-base", PR: 1, Parent: root}
249+
stack1Child := &tree.Node{Name: "stack1-child", PR: 2, Parent: stack1Base}
250+
unrelated := &tree.Node{Name: "unrelated", PR: 3, Parent: root}
251+
252+
root.Children = []*tree.Node{stack1Base, unrelated}
253+
stack1Base.Children = []*tree.Node{stack1Child}
254+
255+
prInfo := makePRInfo(
256+
struct {
257+
num int
258+
title string
259+
}{1, "Stack 1 base"},
260+
struct {
261+
num int
262+
title string
263+
}{2, "Stack 1 child"},
264+
struct {
265+
num int
266+
title string
267+
}{3, "Unrelated feature"},
268+
)
269+
270+
// Viewing the child PR - should see stack1-base and stack1-child, but NOT unrelated
271+
comment := GenerateStackComment(root, "stack1-child", "main", testRepoURL, prInfo)
272+
273+
if !strings.Contains(comment, "stack1-base") {
274+
t.Error("should contain ancestor branch in the current stack")
275+
}
276+
if !strings.Contains(comment, "stack1-child") {
277+
t.Error("should contain current branch")
278+
}
279+
if strings.Contains(comment, "unrelated") {
280+
t.Error("should NOT contain branch from a different stack")
218281
}
219282
})
220283

0 commit comments

Comments
 (0)