Skip to content

Commit 2879cfe

Browse files
committed
add links to downstack prs in body
1 parent f3a797b commit 2879cfe

2 files changed

Lines changed: 163 additions & 1 deletion

File tree

cmd/push.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
120120
title = input
121121
}
122122
}
123-
body := fmt.Sprintf("Part %d of stack.\n\nBase: `%s`", i+1, baseBranch)
123+
body := generatePRBody(s, b.Branch)
124124

125125
newPR, createErr := client.CreatePR(baseBranch, b.Branch, title, body, opts.draft)
126126
if createErr != nil {
@@ -199,6 +199,47 @@ func defaultPRTitle(base, head string) string {
199199
return humanize(head)
200200
}
201201

202+
// generatePRBody builds a rich PR description showing the downstack branches,
203+
// the current branch, and a footer with links to the CLI and feedback form.
204+
func generatePRBody(s *stack.Stack, currentBranch string) string {
205+
var lines []string
206+
207+
// Current branch entry (always first)
208+
lines = append(lines, fmt.Sprintf("- `%s` ← *this PR*", currentBranch))
209+
210+
// Walk downstack from just below current to the bottom, skipping merged branches
211+
found := false
212+
for i := len(s.Branches) - 1; i >= 0; i-- {
213+
b := s.Branches[i]
214+
if b.Branch == currentBranch {
215+
found = true
216+
continue
217+
}
218+
if !found {
219+
continue
220+
}
221+
if b.IsMerged() {
222+
continue
223+
}
224+
if b.PullRequest != nil && b.PullRequest.URL != "" {
225+
lines = append(lines, fmt.Sprintf("- `%s` %s", b.Branch, b.PullRequest.URL))
226+
} else {
227+
lines = append(lines, fmt.Sprintf("- `%s`", b.Branch))
228+
}
229+
}
230+
231+
// Trunk entry
232+
lines = append(lines, fmt.Sprintf("- `%s` (base)", s.Trunk.Branch))
233+
234+
body := "---\n\n**Stacked Pull Requests**\n" + strings.Join(lines, "\n")
235+
body += fmt.Sprintf(
236+
"\n\n<sub>Stack created with <a href=\"https://github.com/github/gh-stack\">GitHub Stacks CLI</a> • <a href=\"%s\">Give Feedback 💬</a></sub>",
237+
feedbackBaseURL,
238+
)
239+
240+
return body
241+
}
242+
202243
// humanize replaces hyphens and underscores with spaces.
203244
func humanize(s string) string {
204245
return strings.Map(func(r rune) rune {

cmd/push_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
6+
"github.com/github/gh-stack/internal/stack"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestGeneratePRBody(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
stack *stack.Stack
14+
currentBranch string
15+
wantContains []string
16+
wantAbsent []string
17+
}{
18+
{
19+
name: "single branch stack",
20+
stack: &stack.Stack{
21+
Trunk: stack.BranchRef{Branch: "main"},
22+
Branches: []stack.BranchRef{{Branch: "feature"}},
23+
},
24+
currentBranch: "feature",
25+
wantContains: []string{
26+
"---",
27+
"**Stacked Pull Requests**",
28+
"- `feature` ← *this PR*",
29+
"- `main` (base)",
30+
"GitHub Stacks CLI",
31+
feedbackBaseURL,
32+
},
33+
},
34+
{
35+
name: "multi-branch current is topmost",
36+
stack: &stack.Stack{
37+
Trunk: stack.BranchRef{Branch: "main"},
38+
Branches: []stack.BranchRef{
39+
{Branch: "part-1", PullRequest: &stack.PullRequestRef{Number: 1, URL: "https://github.com/org/repo/pull/1"}},
40+
{Branch: "part-2", PullRequest: &stack.PullRequestRef{Number: 2, URL: "https://github.com/org/repo/pull/2"}},
41+
{Branch: "part-3"},
42+
},
43+
},
44+
currentBranch: "part-3",
45+
wantContains: []string{
46+
"- `part-3` ← *this PR*",
47+
"- `part-2` https://github.com/org/repo/pull/2",
48+
"- `part-1` https://github.com/org/repo/pull/1",
49+
"- `main` (base)",
50+
},
51+
},
52+
{
53+
name: "current is in the middle excludes upstack",
54+
stack: &stack.Stack{
55+
Trunk: stack.BranchRef{Branch: "main"},
56+
Branches: []stack.BranchRef{
57+
{Branch: "part-1", PullRequest: &stack.PullRequestRef{Number: 1, URL: "https://github.com/org/repo/pull/1"}},
58+
{Branch: "part-2"},
59+
{Branch: "part-3"},
60+
},
61+
},
62+
currentBranch: "part-2",
63+
wantContains: []string{
64+
"- `part-2` ← *this PR*",
65+
"- `part-1` https://github.com/org/repo/pull/1",
66+
"- `main` (base)",
67+
},
68+
wantAbsent: []string{
69+
"part-3",
70+
},
71+
},
72+
{
73+
name: "merged branches are skipped",
74+
stack: &stack.Stack{
75+
Trunk: stack.BranchRef{Branch: "main"},
76+
Branches: []stack.BranchRef{
77+
{Branch: "part-1", PullRequest: &stack.PullRequestRef{Number: 1, URL: "https://github.com/org/repo/pull/1", Merged: true}},
78+
{Branch: "part-2", PullRequest: &stack.PullRequestRef{Number: 2, URL: "https://github.com/org/repo/pull/2"}},
79+
{Branch: "part-3"},
80+
},
81+
},
82+
currentBranch: "part-3",
83+
wantContains: []string{
84+
"- `part-3` ← *this PR*",
85+
"- `part-2` https://github.com/org/repo/pull/2",
86+
"- `main` (base)",
87+
},
88+
wantAbsent: []string{
89+
"part-1",
90+
},
91+
},
92+
{
93+
name: "downstack branch without PR shows branch name only",
94+
stack: &stack.Stack{
95+
Trunk: stack.BranchRef{Branch: "main"},
96+
Branches: []stack.BranchRef{
97+
{Branch: "part-1"},
98+
{Branch: "part-2"},
99+
},
100+
},
101+
currentBranch: "part-2",
102+
wantContains: []string{
103+
"- `part-2` ← *this PR*",
104+
"- `part-1`",
105+
"- `main` (base)",
106+
},
107+
},
108+
}
109+
110+
for _, tt := range tests {
111+
t.Run(tt.name, func(t *testing.T) {
112+
got := generatePRBody(tt.stack, tt.currentBranch)
113+
for _, want := range tt.wantContains {
114+
assert.Contains(t, got, want)
115+
}
116+
for _, absent := range tt.wantAbsent {
117+
assert.NotContains(t, got, absent)
118+
}
119+
})
120+
}
121+
}

0 commit comments

Comments
 (0)