Skip to content

Commit cf62538

Browse files
committed
sync state of prs in stack
1 parent 25ff8a1 commit cf62538

11 files changed

Lines changed: 106 additions & 31 deletions

File tree

cmd/add.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func runAdd(cfg *config.Config, args []string) error {
4040
return nil
4141
}
4242

43-
s, err := sf.ResolveStack(currentBranch, cfg)
43+
s, err := resolveStack(sf, currentBranch, cfg)
4444
if err != nil {
4545
cfg.Errorf("%s", err)
4646
return nil

cmd/init.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ func runInit(cfg *config.Config, opts *initOptions) error {
186186
}
187187

188188
sf.AddStack(newStack)
189+
190+
// Sync PR state for adopted branches
191+
syncStackPRs(cfg, &sf.Stacks[len(sf.Stacks)-1])
192+
189193
if err := stack.Save(gitDir, sf); err != nil {
190194
return err
191195
}

cmd/navigate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ func loadCurrentStack(cfg *config.Config) (*stack.Stack, string, error) {
170170
return nil, "", fmt.Errorf("%s", errMsg)
171171
}
172172

173-
s, err := sf.ResolveStack(currentBranch, cfg)
173+
s, err := resolveStack(sf, currentBranch, cfg)
174174
if err != nil {
175175
cfg.Errorf("%s", err)
176176
return nil, "", err

cmd/push.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
5252
return nil
5353
}
5454

55-
s, err := sf.ResolveStack(currentBranch, cfg)
55+
s, err := resolveStack(sf, currentBranch, cfg)
5656
if err != nil {
5757
cfg.Errorf("%s", err)
5858
return nil
@@ -118,7 +118,6 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
118118
Number: newPR.Number,
119119
ID: newPR.ID,
120120
URL: newPR.URL,
121-
Title: newPR.Title,
122121
}
123122
} else {
124123
// Update base if needed
@@ -136,7 +135,6 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
136135
Number: pr.Number,
137136
ID: pr.ID,
138137
URL: pr.URL,
139-
Title: pr.Title,
140138
}
141139
}
142140
}
@@ -152,7 +150,7 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
152150
fmt.Fprintf(cfg.Err, " Once the GitHub Stacks API is available, PRs will be automatically\n")
153151
fmt.Fprintf(cfg.Err, " grouped into a Stack.\n")
154152

155-
// Update base commit hashes
153+
// Update base commit hashes and sync PR state
156154
for i := range s.Branches {
157155
parent := s.Trunk.Branch
158156
if i > 0 {
@@ -162,6 +160,7 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
162160
s.Branches[i].Base = base
163161
}
164162
}
163+
syncStackPRs(cfg, s)
165164

166165
if err := stack.Save(gitDir, sf); err != nil {
167166
cfg.Errorf("failed to save stack state: %s", err)

cmd/rebase.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
9191
}
9292
}
9393

94-
s, err := sf.ResolveStack(currentBranch, cfg)
94+
s, err := resolveStack(sf, currentBranch, cfg)
9595
if err != nil {
9696
cfg.Errorf("%s", err)
9797
return nil
@@ -207,6 +207,9 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
207207
base, _ := git.HeadSHA(parent)
208208
s.Branches[i].Base = base
209209
}
210+
211+
syncStackPRs(cfg, s)
212+
210213
_ = stack.Save(gitDir, sf)
211214

212215
rangeDesc := "All branches in stack"
@@ -238,7 +241,7 @@ func continueRebase(cfg *config.Config, gitDir string) error {
238241

239242
// Use the saved original branch to find the stack, since git may be in
240243
// a detached HEAD state during an active rebase.
241-
s, err := sf.ResolveStack(state.OriginalBranch, cfg)
244+
s, err := resolveStack(sf, state.OriginalBranch, cfg)
242245
if err != nil {
243246
return err
244247
}
@@ -323,6 +326,9 @@ func continueRebase(cfg *config.Config, gitDir string) error {
323326
base, _ := git.HeadSHA(parent)
324327
s.Branches[i].Base = base
325328
}
329+
330+
syncStackPRs(cfg, s)
331+
326332
_ = stack.Save(gitDir, sf)
327333

328334
cfg.Printf("All branches in stack rebased locally with %s", s.Trunk.Branch)

cmd/unstack.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func runUnstack(cfg *config.Config, opts *unstackOptions) error {
5555
}
5656
}
5757

58-
s, err := sf.ResolveStack(target, cfg)
58+
s, err := resolveStack(sf, target, cfg)
5959
if err != nil {
6060
cfg.Errorf("%s", err)
6161
return nil
Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package stack
1+
package cmd
22

33
import (
44
"fmt"
@@ -7,15 +7,16 @@ import (
77
"github.com/cli/go-gh/v2/pkg/prompter"
88
"github.com/github/gh-stack/internal/config"
99
"github.com/github/gh-stack/internal/git"
10+
"github.com/github/gh-stack/internal/stack"
1011
)
1112

12-
// ResolveStack finds the stack for the given branch, handling ambiguity when
13+
// resolveStack finds the stack for the given branch, handling ambiguity when
1314
// a branch (typically a trunk) belongs to multiple stacks. If exactly one
1415
// stack matches, it is returned directly. If multiple stacks match, the user
1516
// is prompted to select one and the working tree is switched to the top branch
1617
// of the selected stack. Returns nil with no error if no stack contains the
1718
// branch.
18-
func (sf *StackFile) ResolveStack(branch string, cfg *config.Config) (*Stack, error) {
19+
func resolveStack(sf *stack.StackFile, branch string, cfg *config.Config) (*stack.Stack, error) {
1920
stacks := sf.FindAllStacksForBranch(branch)
2021

2122
switch len(stacks) {
@@ -56,3 +57,33 @@ func (sf *StackFile) ResolveStack(branch string, cfg *config.Config) (*Stack, er
5657

5758
return s, nil
5859
}
60+
61+
// syncStackPRs discovers and updates pull request metadata for branches in a stack.
62+
// For each branch, it queries GitHub for the most recent PR and updates the
63+
// PullRequestRef including merge status. Branches with already-merged PRs are skipped.
64+
func syncStackPRs(cfg *config.Config, s *stack.Stack) {
65+
client, err := cfg.GitHubClient()
66+
if err != nil {
67+
return
68+
}
69+
70+
for i := range s.Branches {
71+
b := &s.Branches[i]
72+
73+
if b.PullRequest != nil && b.PullRequest.Merged {
74+
continue
75+
}
76+
77+
pr, err := client.FindAnyPRForBranch(b.Branch)
78+
if err != nil || pr == nil {
79+
continue
80+
}
81+
82+
b.PullRequest = &stack.PullRequestRef{
83+
Number: pr.Number,
84+
ID: pr.ID,
85+
URL: pr.URL,
86+
Merged: pr.Merged,
87+
}
88+
}
89+
}

cmd/view.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func runView(cfg *config.Config, opts *viewOptions) error {
5656
return nil
5757
}
5858

59-
s, err := sf.ResolveStack(currentBranch, cfg)
59+
s, err := resolveStack(sf, currentBranch, cfg)
6060
if err != nil {
6161
cfg.Errorf("%s", err)
6262
return nil
@@ -74,6 +74,10 @@ func runView(cfg *config.Config, opts *viewOptions) error {
7474
return nil
7575
}
7676

77+
// Sync PR state
78+
syncStackPRs(cfg, s)
79+
_ = stack.Save(gitDir, sf)
80+
7781
if opts.web {
7882
return viewWeb(cfg, s)
7983
}

internal/github/github.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type PullRequest struct {
1717
HeadRefName string
1818
BaseRefName string
1919
IsDraft bool
20+
Merged bool
2021
}
2122

2223
// Client wraps GitHub API operations.
@@ -82,6 +83,46 @@ func (c *Client) FindPRForBranch(branch string) (*PullRequest, error) {
8283
HeadRefName: n.HeadRefName,
8384
BaseRefName: n.BaseRefName,
8485
IsDraft: n.IsDraft,
86+
Merged: n.Merged,
87+
}, nil
88+
}
89+
90+
// FindAnyPRForBranch finds the most recent PR by head branch name regardless of state.
91+
func (c *Client) FindAnyPRForBranch(branch string) (*PullRequest, error) {
92+
var query struct {
93+
Repository struct {
94+
PullRequests struct {
95+
Nodes []PullRequest
96+
} `graphql:"pullRequests(headRefName: $head, last: 1)"`
97+
} `graphql:"repository(owner: $owner, name: $name)"`
98+
}
99+
100+
variables := map[string]interface{}{
101+
"owner": graphql.String(c.owner),
102+
"name": graphql.String(c.repo),
103+
"head": graphql.String(branch),
104+
}
105+
106+
if err := c.gql.Query("FindAnyPRForBranch", &query, variables); err != nil {
107+
return nil, fmt.Errorf("querying PRs: %w", err)
108+
}
109+
110+
nodes := query.Repository.PullRequests.Nodes
111+
if len(nodes) == 0 {
112+
return nil, nil
113+
}
114+
115+
n := nodes[0]
116+
return &PullRequest{
117+
ID: n.ID,
118+
Number: n.Number,
119+
Title: n.Title,
120+
State: n.State,
121+
URL: n.URL,
122+
HeadRefName: n.HeadRefName,
123+
BaseRefName: n.BaseRefName,
124+
IsDraft: n.IsDraft,
125+
Merged: n.Merged,
85126
}, nil
86127
}
87128

internal/stack/schema.json

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"repository": {
1414
"type": "string",
15-
"description": "The owner/name slug of the repository (e.g. 'github/gh-stack')."
15+
"description": "The host:owner/name of the repository (e.g. 'github.com:github/gh-stack')."
1616
},
1717
"stacks": {
1818
"type": "array",
@@ -30,14 +30,6 @@
3030
"type": "string",
3131
"description": "Identifier for this stack, populated from the API when available."
3232
},
33-
"state": {
34-
"type": "string",
35-
"description": "State of the stack from the API (e.g. 'open', 'merged')."
36-
},
37-
"open": {
38-
"type": "boolean",
39-
"description": "Whether the stack is open or closed."
40-
},
4133
"trunk": {
4234
"$ref": "#/$defs/branchRef",
4335
"description": "The trunk (base) branch of the stack."
@@ -74,25 +66,25 @@
7466
},
7567
"pullRequestRef": {
7668
"type": "object",
77-
"description": "A snapshot of relatively immutable pull request metadata.",
69+
"description": "A snapshot of pull request metadata.",
7870
"required": ["number"],
7971
"properties": {
8072
"number": {
8173
"type": "integer",
82-
"description": "The PR number."
74+
"description": "The PR number, scoped to the repository."
8375
},
8476
"id": {
8577
"type": "string",
86-
"description": "The PR node ID (GraphQL ID)."
78+
"description": "The PR global node ID."
8779
},
8880
"url": {
8981
"type": "string",
9082
"format": "uri",
9183
"description": "Direct URL to the pull request."
9284
},
93-
"title": {
94-
"type": "string",
95-
"description": "The PR title at the time it was recorded."
85+
"merged": {
86+
"type": "boolean",
87+
"description": "Whether the pull request has been merged."
9688
}
9789
}
9890
}

0 commit comments

Comments
 (0)