Skip to content

Commit 3c94dcc

Browse files
authored
Merge pull request #6 from github/github-structs
refactor(github): extract inlined-struct into the github package
2 parents 1fa8554 + 89325fe commit 3c94dcc

File tree

5 files changed

+162
-94
lines changed

5 files changed

+162
-94
lines changed

internal/cmd/combine_prs.go

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,91 +9,85 @@ import (
99
"strings"
1010

1111
"github.com/cli/go-gh/v2/pkg/api"
12+
"github.com/github/gh-combine/internal/github"
1213
)
1314

14-
func CombinePRs(ctx context.Context, graphQlClient *api.GraphQLClient, restClient *api.RESTClient, owner, repo string, matchedPRs []struct {
15-
Number int
16-
Title string
17-
Branch string
18-
Base string
19-
BaseSHA string
20-
},
21-
) error {
15+
func CombinePRs(ctx context.Context, graphQlClient *api.GraphQLClient, restClient *api.RESTClient, repo github.Repo, pulls github.Pulls) error {
2216
// Define the combined branch name
2317
workingBranchName := combineBranchName + workingBranchSuffix
2418

2519
// Get the default branch of the repository
26-
repoDefaultBranch, err := getDefaultBranch(ctx, restClient, owner, repo)
20+
repoDefaultBranch, err := getDefaultBranch(ctx, restClient, repo)
2721
if err != nil {
2822
return fmt.Errorf("failed to get default branch: %w", err)
2923
}
3024

31-
baseBranchSHA, err := getBranchSHA(ctx, restClient, owner, repo, repoDefaultBranch)
25+
baseBranchSHA, err := getBranchSHA(ctx, restClient, repo, repoDefaultBranch)
3226
if err != nil {
3327
return fmt.Errorf("failed to get SHA of main branch: %w", err)
3428
}
3529

3630
// Delete any pre-existing working branch
37-
err = deleteBranch(ctx, restClient, owner, repo, workingBranchName)
31+
err = deleteBranch(ctx, restClient, repo, workingBranchName)
3832
if err != nil {
3933
Logger.Debug("Working branch not found, continuing", "branch", workingBranchName)
4034
}
4135

4236
// Delete any pre-existing combined branch
43-
err = deleteBranch(ctx, restClient, owner, repo, combineBranchName)
37+
err = deleteBranch(ctx, restClient, repo, combineBranchName)
4438
if err != nil {
4539
Logger.Debug("Combined branch not found, continuing", "branch", combineBranchName)
4640
}
4741

4842
// Create the combined branch
49-
err = createBranch(ctx, restClient, owner, repo, combineBranchName, baseBranchSHA)
43+
err = createBranch(ctx, restClient, repo, combineBranchName, baseBranchSHA)
5044
if err != nil {
5145
return fmt.Errorf("failed to create combined branch: %w", err)
5246
}
5347

5448
// Create the working branch
55-
err = createBranch(ctx, restClient, owner, repo, workingBranchName, baseBranchSHA)
49+
err = createBranch(ctx, restClient, repo, workingBranchName, baseBranchSHA)
5650
if err != nil {
5751
return fmt.Errorf("failed to create working branch: %w", err)
5852
}
5953

6054
// Merge all PR branches into the working branch
6155
var combinedPRs []string
6256
var mergeFailedPRs []string
63-
for _, pr := range matchedPRs {
64-
err := mergeBranch(ctx, restClient, owner, repo, workingBranchName, pr.Branch)
57+
for _, pr := range pulls {
58+
err := mergeBranch(ctx, restClient, repo, workingBranchName, pr.Head.Ref)
6559
if err != nil {
6660
// Check if the error is a 409 merge conflict
6761
if isMergeConflictError(err) {
6862
// Log merge conflicts at DEBUG level
69-
Logger.Debug("Merge conflict", "branch", pr.Branch, "error", err)
63+
Logger.Debug("Merge conflict", "branch", pr.Head.Ref, "error", err)
7064
} else {
7165
// Log other errors at WARN level
72-
Logger.Warn("Failed to merge branch", "branch", pr.Branch, "error", err)
66+
Logger.Warn("Failed to merge branch", "branch", pr.Head.Ref, "error", err)
7367
}
7468
mergeFailedPRs = append(mergeFailedPRs, fmt.Sprintf("#%d", pr.Number))
7569
} else {
76-
Logger.Debug("Merged branch", "branch", pr.Branch)
70+
Logger.Debug("Merged branch", "branch", pr.Head.Ref)
7771
combinedPRs = append(combinedPRs, fmt.Sprintf("#%d - %s", pr.Number, pr.Title))
7872
}
7973
}
8074

8175
// Update the combined branch to the latest commit of the working branch
82-
err = updateRef(ctx, restClient, owner, repo, combineBranchName, workingBranchName)
76+
err = updateRef(ctx, restClient, repo, combineBranchName, workingBranchName)
8377
if err != nil {
8478
return fmt.Errorf("failed to update combined branch: %w", err)
8579
}
8680

8781
// Delete the temporary working branch
88-
err = deleteBranch(ctx, restClient, owner, repo, workingBranchName)
82+
err = deleteBranch(ctx, restClient, repo, workingBranchName)
8983
if err != nil {
9084
Logger.Warn("Failed to delete working branch", "branch", workingBranchName, "error", err)
9185
}
9286

9387
// Create the combined PR
9488
prBody := generatePRBody(combinedPRs, mergeFailedPRs)
9589
prTitle := "Combined PRs"
96-
err = createPullRequest(ctx, restClient, owner, repo, prTitle, combineBranchName, repoDefaultBranch, prBody)
90+
err = createPullRequest(ctx, restClient, repo, prTitle, combineBranchName, repoDefaultBranch, prBody)
9791
if err != nil {
9892
return fmt.Errorf("failed to create combined PR: %w", err)
9993
}
@@ -108,11 +102,11 @@ func isMergeConflictError(err error) bool {
108102
}
109103

110104
// Find the default branch of a repository
111-
func getDefaultBranch(ctx context.Context, client *api.RESTClient, owner, repo string) (string, error) {
105+
func getDefaultBranch(ctx context.Context, client *api.RESTClient, repo github.Repo) (string, error) {
112106
var repoInfo struct {
113107
DefaultBranch string `json:"default_branch"`
114108
}
115-
endpoint := fmt.Sprintf("repos/%s/%s", owner, repo)
109+
endpoint := fmt.Sprintf("repos/%s/%s", repo.Owner, repo.Repo)
116110
err := client.Get(endpoint, &repoInfo)
117111
if err != nil {
118112
return "", fmt.Errorf("failed to get default branch: %w", err)
@@ -121,13 +115,13 @@ func getDefaultBranch(ctx context.Context, client *api.RESTClient, owner, repo s
121115
}
122116

123117
// Get the SHA of a given branch
124-
func getBranchSHA(ctx context.Context, client *api.RESTClient, owner, repo, branch string) (string, error) {
118+
func getBranchSHA(ctx context.Context, client *api.RESTClient, repo github.Repo, branch string) (string, error) {
125119
var ref struct {
126120
Object struct {
127121
SHA string `json:"sha"`
128122
} `json:"object"`
129123
}
130-
endpoint := fmt.Sprintf("repos/%s/%s/git/ref/heads/%s", owner, repo, branch)
124+
endpoint := fmt.Sprintf("repos/%s/%s/git/ref/heads/%s", repo.Owner, repo.Repo, branch)
131125
err := client.Get(endpoint, &ref)
132126
if err != nil {
133127
return "", fmt.Errorf("failed to get SHA of branch %s: %w", branch, err)
@@ -154,14 +148,14 @@ func generatePRBody(combinedPRs, mergeFailedPRs []string) string {
154148
}
155149

156150
// deleteBranch deletes a branch in the repository
157-
func deleteBranch(ctx context.Context, client *api.RESTClient, owner, repo, branch string) error {
158-
endpoint := fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", owner, repo, branch)
151+
func deleteBranch(ctx context.Context, client *api.RESTClient, repo github.Repo, branch string) error {
152+
endpoint := fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", repo.Owner, repo.Repo, branch)
159153
return client.Delete(endpoint, nil)
160154
}
161155

162156
// createBranch creates a new branch in the repository
163-
func createBranch(ctx context.Context, client *api.RESTClient, owner, repo, branch, sha string) error {
164-
endpoint := fmt.Sprintf("repos/%s/%s/git/refs", owner, repo)
157+
func createBranch(ctx context.Context, client *api.RESTClient, repo github.Repo, branch, sha string) error {
158+
endpoint := fmt.Sprintf("repos/%s/%s/git/refs", repo.Owner, repo.Repo)
165159
payload := map[string]string{
166160
"ref": "refs/heads/" + branch,
167161
"sha": sha,
@@ -174,8 +168,8 @@ func createBranch(ctx context.Context, client *api.RESTClient, owner, repo, bran
174168
}
175169

176170
// mergeBranch merges a branch into the base branch
177-
func mergeBranch(ctx context.Context, client *api.RESTClient, owner, repo, base, head string) error {
178-
endpoint := fmt.Sprintf("repos/%s/%s/merges", owner, repo)
171+
func mergeBranch(ctx context.Context, client *api.RESTClient, repo github.Repo, base, head string) error {
172+
endpoint := fmt.Sprintf("repos/%s/%s/merges", repo.Owner, repo.Repo)
179173
payload := map[string]string{
180174
"base": base,
181175
"head": head,
@@ -188,21 +182,21 @@ func mergeBranch(ctx context.Context, client *api.RESTClient, owner, repo, base,
188182
}
189183

190184
// updateRef updates a branch to point to the latest commit of another branch
191-
func updateRef(ctx context.Context, client *api.RESTClient, owner, repo, branch, sourceBranch string) error {
185+
func updateRef(ctx context.Context, client *api.RESTClient, repo github.Repo, branch, sourceBranch string) error {
192186
// Get the SHA of the source branch
193187
var ref struct {
194188
Object struct {
195189
SHA string `json:"sha"`
196190
} `json:"object"`
197191
}
198-
endpoint := fmt.Sprintf("repos/%s/%s/git/ref/heads/%s", owner, repo, sourceBranch)
192+
endpoint := fmt.Sprintf("repos/%s/%s/git/ref/heads/%s", repo.Owner, repo.Repo, sourceBranch)
199193
err := client.Get(endpoint, &ref)
200194
if err != nil {
201195
return fmt.Errorf("failed to get SHA of source branch: %w", err)
202196
}
203197

204198
// Update the branch to point to the new SHA
205-
endpoint = fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", owner, repo, branch)
199+
endpoint = fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", repo.Owner, repo.Repo, branch)
206200
payload := map[string]interface{}{
207201
"sha": ref.Object.SHA,
208202
"force": true,
@@ -215,8 +209,8 @@ func updateRef(ctx context.Context, client *api.RESTClient, owner, repo, branch,
215209
}
216210

217211
// createPullRequest creates a new pull request
218-
func createPullRequest(ctx context.Context, client *api.RESTClient, owner, repo, title, head, base, body string) error {
219-
endpoint := fmt.Sprintf("repos/%s/%s/pulls", owner, repo)
212+
func createPullRequest(ctx context.Context, client *api.RESTClient, repo github.Repo, title, head, base, body string) error {
213+
endpoint := fmt.Sprintf("repos/%s/%s/pulls", repo.Owner, repo.Repo)
220214
payload := map[string]string{
221215
"title": title,
222216
"head": head,

internal/cmd/root.go

Lines changed: 19 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"strings"
87

98
"github.com/cli/go-gh/v2/pkg/api"
109
"github.com/spf13/cobra"
1110

11+
"github.com/github/gh-combine/internal/github"
1212
"github.com/github/gh-combine/internal/version"
1313
)
1414

@@ -185,12 +185,13 @@ func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string
185185
return fmt.Errorf("failed to create REST client: %w", err)
186186
}
187187

188+
// Create GitHub GraphQL client
188189
graphQlClient, err := api.DefaultGraphQLClient()
189190
if err != nil {
190191
return fmt.Errorf("failed to create GraphQLClient client: %w", err)
191192
}
192193

193-
for _, repo := range repos {
194+
for _, repoString := range repos {
194195
// Check if context was cancelled (CTRL+C pressed)
195196
select {
196197
case <-ctx.Done():
@@ -199,7 +200,14 @@ func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string
199200
// Continue processing
200201
}
201202

202-
spinner.UpdateMessage("Processing " + repo)
203+
spinner.UpdateMessage("Parsing " + repoString)
204+
205+
repo, err := github.ParseRepo(repoString)
206+
if err != nil {
207+
return fmt.Errorf("failed to parse repo: %w", err)
208+
}
209+
210+
spinner.UpdateMessage("Processing " + repo.String())
203211
Logger.Debug("Processing repository", "repo", repo)
204212

205213
// Process the repository
@@ -218,16 +226,7 @@ func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string
218226
}
219227

220228
// processRepository handles a single repository's PRs
221-
func processRepository(ctx context.Context, client *api.RESTClient, graphQlClient *api.GraphQLClient, spinner *Spinner, repo string) error {
222-
// Parse owner and repo name
223-
parts := strings.Split(repo, "/")
224-
if len(parts) != 2 {
225-
return fmt.Errorf("invalid repository format: %s", repo)
226-
}
227-
228-
owner := parts[0]
229-
repoName := parts[1]
230-
229+
func processRepository(ctx context.Context, client *api.RESTClient, graphQlClient *api.GraphQLClient, spinner *Spinner, repo github.Repo) error {
231230
// Check for cancellation
232231
select {
233232
case <-ctx.Done():
@@ -237,23 +236,9 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
237236
}
238237

239238
// Get open PRs for the repository
240-
var pulls []struct {
241-
Number int
242-
Title string
243-
Head struct {
244-
Ref string
245-
}
246-
Base struct {
247-
Ref string
248-
SHA string
249-
}
250-
Labels []struct {
251-
Name string
252-
}
253-
}
239+
var pulls github.Pulls
254240

255-
endpoint := fmt.Sprintf("repos/%s/%s/pulls?state=open", owner, repoName)
256-
if err := client.Get(endpoint, &pulls); err != nil {
241+
if err := client.Get(repo.PullsEndpoint(), &pulls); err != nil {
257242
return err
258243
}
259244

@@ -266,17 +251,8 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
266251
}
267252

268253
// Filter PRs based on criteria
269-
var matchedPRs []struct {
270-
Number int
271-
Title string
272-
Branch string
273-
Base string
274-
BaseSHA string
275-
}
276-
254+
var matchedPRs github.Pulls
277255
for _, pull := range pulls {
278-
branch := pull.Head.Ref
279-
280256
// Temporary workaround because passing structures is useless in this
281257
// context.
282258
// Eventually the []Labels should have better support.
@@ -286,12 +262,12 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
286262
}
287263

288264
// Check if PR matches all filtering criteria
289-
if !PrMatchesCriteria(branch, labels) {
265+
if !PrMatchesCriteria(pull.Head.Ref, labels) {
290266
continue
291267
}
292268

293269
// Check if PR meets additional requirements (CI, approval)
294-
meetsRequirements, err := PrMeetsRequirements(ctx, graphQlClient, owner, repoName, pull.Number)
270+
meetsRequirements, err := PrMeetsRequirements(ctx, graphQlClient, repo.Owner, repo.Repo, pull.Number)
295271
if err != nil {
296272
Logger.Warn("Failed to check PR requirements", "repo", repo, "pr", pull.Number, "error", err)
297273
continue
@@ -302,19 +278,7 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
302278
continue
303279
}
304280

305-
matchedPRs = append(matchedPRs, struct {
306-
Number int
307-
Title string
308-
Branch string
309-
Base string
310-
BaseSHA string
311-
}{
312-
Number: pull.Number,
313-
Title: pull.Title,
314-
Branch: branch,
315-
Base: pull.Base.Ref,
316-
BaseSHA: pull.Base.SHA,
317-
})
281+
matchedPRs = append(matchedPRs, pull)
318282
}
319283

320284
// Check if we have enough PRs to combine
@@ -325,10 +289,8 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
325289

326290
Logger.Debug("Matched PRs", "repo", repo, "count", len(matchedPRs))
327291

328-
// If we get here, we have enough PRs to combine
329-
330292
// Combine the PRs
331-
err := CombinePRs(ctx, graphQlClient, client, owner, repoName, matchedPRs)
293+
err := CombinePRs(ctx, graphQlClient, client, repo, matchedPRs)
332294
if err != nil {
333295
return fmt.Errorf("failed to combine PRs: %w", err)
334296
}

internal/github/github.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package github
2+
3+
type Ref struct {
4+
Ref string `json:"ref"`
5+
SHA string `json:"sha"`
6+
}
7+
8+
type Label struct {
9+
Name string `json:"name"`
10+
}
11+
12+
type Labels []Label
13+
14+
type Pull struct {
15+
Number int `json:"number"`
16+
Title string `json:"title"`
17+
Head Ref `json:"head"`
18+
Base Ref `json:"base"`
19+
Labels Labels `json:"labels"`
20+
}
21+
22+
type Pulls []Pull

0 commit comments

Comments
 (0)