Skip to content

Commit ad9d170

Browse files
committed
push branches and create prs
1 parent 58d1549 commit ad9d170

3 files changed

Lines changed: 248 additions & 0 deletions

File tree

cmd/push.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/github/gh-stack/internal/config"
7+
"github.com/github/gh-stack/internal/git"
8+
"github.com/github/gh-stack/internal/stack"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
type pushOptions struct {
13+
force bool
14+
draft bool
15+
dryRun bool
16+
}
17+
18+
func PushCmd(cfg *config.Config) *cobra.Command {
19+
opts := &pushOptions{}
20+
21+
cmd := &cobra.Command{
22+
Use: "push",
23+
Short: "Push all branches in the current stack and create/update PRs",
24+
RunE: func(cmd *cobra.Command, args []string) error {
25+
return runPush(cfg, opts)
26+
},
27+
}
28+
29+
cmd.Flags().BoolVarP(&opts.force, "force", "f", false, "Force-push branches")
30+
cmd.Flags().BoolVar(&opts.draft, "draft", false, "Create PRs as drafts")
31+
cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "Show what would be pushed without pushing")
32+
33+
return cmd
34+
}
35+
36+
func runPush(cfg *config.Config, opts *pushOptions) error {
37+
gitDir, err := git.GitDir()
38+
if err != nil {
39+
cfg.Errorf("not a git repository")
40+
return nil
41+
}
42+
43+
sf, err := stack.Load(gitDir)
44+
if err != nil {
45+
cfg.Errorf("failed to load stack state: %s", err)
46+
return nil
47+
}
48+
49+
currentBranch, err := git.CurrentBranch()
50+
if err != nil {
51+
cfg.Errorf("failed to get current branch: %s", err)
52+
return nil
53+
}
54+
55+
s := sf.FindStackForBranch(currentBranch)
56+
if s == nil {
57+
cfg.Errorf("current branch %q is not part of a stack", currentBranch)
58+
return nil
59+
}
60+
61+
client, err := cfg.GitHubClient()
62+
if err != nil {
63+
cfg.Errorf("failed to create GitHub client: %s", err)
64+
return nil
65+
}
66+
67+
// Push all branches
68+
for _, b := range s.Branches {
69+
if opts.dryRun {
70+
cfg.Printf("Would push %s\n", b.Branch)
71+
continue
72+
}
73+
74+
cfg.Printf("Pushing %s...\n", b.Branch)
75+
if err := git.Push("origin", []string{b.Branch}, opts.force, false); err != nil {
76+
cfg.Errorf("failed to push %s: %s", b.Branch, err)
77+
return nil
78+
}
79+
}
80+
81+
if opts.dryRun {
82+
return nil
83+
}
84+
85+
// Create or update PRs
86+
for i, b := range s.Branches {
87+
baseBranch := s.BaseBranch(b.Branch)
88+
89+
pr, err := client.FindPRForBranch(b.Branch)
90+
if err != nil {
91+
cfg.Warningf("failed to check PR for %s: %v\n", b.Branch, err)
92+
continue
93+
}
94+
95+
if pr == nil {
96+
// Create new PR
97+
title := b.Branch
98+
body := fmt.Sprintf("Part %d of stack.\n\nBase: `%s`", i+1, baseBranch)
99+
100+
newPR, createErr := client.CreatePR(baseBranch, b.Branch, title, body, opts.draft)
101+
if createErr != nil {
102+
cfg.Warningf("failed to create PR for %s: %v\n", b.Branch, createErr)
103+
continue
104+
}
105+
cfg.Successf("Created PR #%d for %s\n", newPR.Number, b.Branch)
106+
} else {
107+
// Update base if needed
108+
if pr.BaseRefName != baseBranch {
109+
if err := client.UpdatePRBase(pr.ID, baseBranch); err != nil {
110+
cfg.Warningf("failed to update PR #%d base: %v\n", pr.Number, err)
111+
} else {
112+
cfg.Successf("Updated PR #%d base to %s\n", pr.Number, baseBranch)
113+
}
114+
} else {
115+
cfg.Printf("PR #%d for %s is up to date\n", pr.Number, b.Branch)
116+
}
117+
}
118+
}
119+
120+
// TODO: Add PRs to a stack
121+
//
122+
// We can call an API after all the individual PRs are created/updated to create the stack at once,
123+
// or we can add a flag to the existing PR API to incrementally build the stack.
124+
//
125+
// For now, the PRs are pushed and created individually but are NOT linked as a formal stack on GitHub.
126+
cfg.Warningf("Stacked PRs is not yet implemented — PRs were created individually.\n")
127+
fmt.Fprintf(cfg.Err, " Once the GitHub Stacks API is available, PRs will be automatically\n")
128+
fmt.Fprintf(cfg.Err, " grouped into a Stack.\n")
129+
130+
// Update head SHAs
131+
for i, b := range s.Branches {
132+
if sha, err := git.HeadSHA(b.Branch); err == nil {
133+
s.Branches[i].Head = sha
134+
}
135+
}
136+
137+
if err := stack.Save(gitDir, sf); err != nil {
138+
cfg.Errorf("failed to save stack state: %s", err)
139+
return nil
140+
}
141+
142+
cfg.Successf("Pushed and synced %d branches\n", len(s.Branches))
143+
return nil
144+
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func RootCmd() *cobra.Command {
2727

2828
// Remote operations
2929
root.AddCommand(CheckoutCmd(cfg))
30+
root.AddCommand(PushCmd(cfg))
3031
root.AddCommand(MergeCmd(cfg))
3132

3233
// Helper commands

internal/github/github.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,106 @@ func (c *Client) FindPRForBranch(branch string) (*PullRequest, error) {
8484
IsDraft: n.IsDraft,
8585
}, nil
8686
}
87+
88+
// CreatePR creates a new pull request.
89+
func (c *Client) CreatePR(base, head, title, body string, draft bool) (*PullRequest, error) {
90+
var mutation struct {
91+
CreatePullRequest struct {
92+
PullRequest struct {
93+
ID string
94+
Number int
95+
Title string
96+
State string
97+
URL string `graphql:"url"`
98+
HeadRefName string
99+
BaseRefName string
100+
IsDraft bool
101+
}
102+
} `graphql:"createPullRequest(input: $input)"`
103+
}
104+
105+
repoID, err := c.repositoryID()
106+
if err != nil {
107+
return nil, err
108+
}
109+
110+
type CreatePullRequestInput struct {
111+
RepositoryID string `json:"repositoryId"`
112+
BaseRefName string `json:"baseRefName"`
113+
HeadRefName string `json:"headRefName"`
114+
Title string `json:"title"`
115+
Body string `json:"body,omitempty"`
116+
Draft bool `json:"draft"`
117+
}
118+
119+
variables := map[string]interface{}{
120+
"input": CreatePullRequestInput{
121+
RepositoryID: repoID,
122+
BaseRefName: base,
123+
HeadRefName: head,
124+
Title: title,
125+
Body: body,
126+
Draft: draft,
127+
},
128+
}
129+
130+
if err := c.gql.Mutate("CreatePullRequest", &mutation, variables); err != nil {
131+
return nil, fmt.Errorf("creating PR: %w", err)
132+
}
133+
134+
pr := mutation.CreatePullRequest.PullRequest
135+
return &PullRequest{
136+
ID: pr.ID,
137+
Number: pr.Number,
138+
Title: pr.Title,
139+
State: pr.State,
140+
URL: pr.URL,
141+
HeadRefName: pr.HeadRefName,
142+
BaseRefName: pr.BaseRefName,
143+
IsDraft: pr.IsDraft,
144+
}, nil
145+
}
146+
147+
// UpdatePRBase updates the base branch of a pull request.
148+
func (c *Client) UpdatePRBase(prID, newBase string) error {
149+
var mutation struct {
150+
UpdatePullRequest struct {
151+
PullRequest struct {
152+
ID string
153+
}
154+
} `graphql:"updatePullRequest(input: $input)"`
155+
}
156+
157+
type UpdatePullRequestInput struct {
158+
PullRequestID string `json:"pullRequestId"`
159+
BaseRefName string `json:"baseRefName"`
160+
}
161+
162+
variables := map[string]interface{}{
163+
"input": UpdatePullRequestInput{
164+
PullRequestID: prID,
165+
BaseRefName: newBase,
166+
},
167+
}
168+
169+
return c.gql.Mutate("UpdatePullRequest", &mutation, variables)
170+
}
171+
172+
func (c *Client) repositoryID() (string, error) {
173+
var query struct {
174+
Repository struct {
175+
ID string
176+
} `graphql:"repository(owner: $owner, name: $name)"`
177+
}
178+
179+
variables := map[string]interface{}{
180+
"owner": graphql.String(c.owner),
181+
"name": graphql.String(c.repo),
182+
}
183+
184+
if err := c.gql.Query("RepositoryID", &query, variables); err != nil {
185+
return "", fmt.Errorf("fetching repository ID: %w", err)
186+
}
187+
188+
return query.Repository.ID, nil
189+
}

0 commit comments

Comments
 (0)