Skip to content

Commit 28d98bb

Browse files
authored
Merge pull request #15 from github/stats-output
Stats output
2 parents 9578695 + b365c97 commit 28d98bb

File tree

3 files changed

+183
-35
lines changed

3 files changed

+183
-35
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,18 @@ gh combine owner/repo --ignore-labels wip,dependencies
144144
gh combine owner/repo --update-branch
145145
```
146146

147+
### Disable Stats Output
148+
149+
```bash
150+
gh combine owner/repo --no-stats
151+
```
152+
153+
### Disable Color
154+
155+
```bash
156+
gh combine owner/repo --no-color
157+
```
158+
147159
### Running with Debug Logging
148160

149161
```bash

internal/cmd/combine_prs.go

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,27 @@ type RESTClientInterface interface {
2020
Patch(endpoint string, body io.Reader, response interface{}) error
2121
}
2222

23-
func CombinePRs(ctx context.Context, graphQlClient *api.GraphQLClient, restClient RESTClientInterface, repo github.Repo, pulls github.Pulls) error {
24-
// Define the combined branch name
23+
// CombinePRsWithStats combines PRs and returns stats for summary output
24+
func CombinePRsWithStats(ctx context.Context, graphQlClient *api.GraphQLClient, restClient RESTClientInterface, repo github.Repo, pulls github.Pulls) (combined []string, mergeConflicts []string, combinedPRLink string, err error) {
2525
workingBranchName := combineBranchName + workingBranchSuffix
2626

27-
// Get the default branch of the repository
2827
repoDefaultBranch, err := getDefaultBranch(ctx, restClient, repo)
2928
if err != nil {
30-
return fmt.Errorf("failed to get default branch: %w", err)
29+
return nil, nil, "", fmt.Errorf("failed to get default branch: %w", err)
3130
}
3231

3332
baseBranchSHA, err := getBranchSHA(ctx, restClient, repo, repoDefaultBranch)
3433
if err != nil {
35-
return fmt.Errorf("failed to get SHA of main branch: %w", err)
34+
return nil, nil, "", fmt.Errorf("failed to get SHA of main branch: %w", err)
3635
}
36+
// Delete any pre-existing working branch
3737

3838
// Delete any pre-existing working branch
3939
err = deleteBranch(ctx, restClient, repo, workingBranchName)
4040
if err != nil {
4141
Logger.Debug("Working branch not found, continuing", "branch", workingBranchName)
42+
43+
// Delete any pre-existing combined branch
4244
}
4345

4446
// Delete any pre-existing combined branch
@@ -47,60 +49,100 @@ func CombinePRs(ctx context.Context, graphQlClient *api.GraphQLClient, restClien
4749
Logger.Debug("Combined branch not found, continuing", "branch", combineBranchName)
4850
}
4951

50-
// Create the combined branch
5152
err = createBranch(ctx, restClient, repo, combineBranchName, baseBranchSHA)
5253
if err != nil {
53-
return fmt.Errorf("failed to create combined branch: %w", err)
54+
return nil, nil, "", fmt.Errorf("failed to create combined branch: %w", err)
5455
}
55-
56-
// Create the working branch
5756
err = createBranch(ctx, restClient, repo, workingBranchName, baseBranchSHA)
5857
if err != nil {
59-
return fmt.Errorf("failed to create working branch: %w", err)
58+
return nil, nil, "", fmt.Errorf("failed to create working branch: %w", err)
6059
}
6160

62-
// Merge all PR branches into the working branch
63-
var combinedPRs []string
64-
var mergeFailedPRs []string
6561
for _, pr := range pulls {
6662
err := mergeBranch(ctx, restClient, repo, workingBranchName, pr.Head.Ref)
6763
if err != nil {
68-
// Check if the error is a 409 merge conflict
6964
if isMergeConflictError(err) {
70-
// Log merge conflicts at DEBUG level
7165
Logger.Debug("Merge conflict", "branch", pr.Head.Ref, "error", err)
7266
} else {
73-
// Log other errors at WARN level
7467
Logger.Warn("Failed to merge branch", "branch", pr.Head.Ref, "error", err)
7568
}
76-
mergeFailedPRs = append(mergeFailedPRs, fmt.Sprintf("#%d", pr.Number))
69+
mergeConflicts = append(mergeConflicts, fmt.Sprintf("#%d", pr.Number))
7770
} else {
7871
Logger.Debug("Merged branch", "branch", pr.Head.Ref)
79-
combinedPRs = append(combinedPRs, fmt.Sprintf("#%d - %s", pr.Number, pr.Title))
72+
combined = append(combined, fmt.Sprintf("#%d - %s", pr.Number, pr.Title))
8073
}
8174
}
8275

83-
// Update the combined branch to the latest commit of the working branch
8476
err = updateRef(ctx, restClient, repo, combineBranchName, workingBranchName)
8577
if err != nil {
86-
return fmt.Errorf("failed to update combined branch: %w", err)
78+
return combined, mergeConflicts, "", fmt.Errorf("failed to update combined branch: %w", err)
8779
}
88-
89-
// Delete the temporary working branch
9080
err = deleteBranch(ctx, restClient, repo, workingBranchName)
9181
if err != nil {
9282
Logger.Warn("Failed to delete working branch", "branch", workingBranchName, "error", err)
9383
}
9484

95-
// Create the combined PR
96-
prBody := generatePRBody(combinedPRs, mergeFailedPRs)
85+
prBody := generatePRBody(combined, mergeConflicts)
9786
prTitle := "Combined PRs"
98-
err = createPullRequest(ctx, restClient, repo, prTitle, combineBranchName, repoDefaultBranch, prBody, addLabels, addAssignees)
87+
prNumber, prErr := createPullRequestWithNumber(ctx, restClient, repo, prTitle, combineBranchName, repoDefaultBranch, prBody, addLabels, addAssignees)
88+
if prErr != nil {
89+
return combined, mergeConflicts, "", fmt.Errorf("failed to create combined PR: %w", prErr)
90+
}
91+
if prNumber > 0 {
92+
combinedPRLink = fmt.Sprintf("https://github.com/%s/%s/pull/%d", repo.Owner, repo.Repo, prNumber)
93+
}
94+
95+
return combined, mergeConflicts, combinedPRLink, nil
96+
}
97+
98+
// createPullRequestWithNumber creates a PR and returns its number
99+
func createPullRequestWithNumber(ctx context.Context, client RESTClientInterface, repo github.Repo, title, head, base, body string, labels, assignees []string) (int, error) {
100+
endpoint := fmt.Sprintf("repos/%s/%s/pulls", repo.Owner, repo.Repo)
101+
payload := map[string]interface{}{
102+
"title": title,
103+
"head": head,
104+
"base": base,
105+
"body": body,
106+
}
107+
108+
requestBody, err := encodePayload(payload)
99109
if err != nil {
100-
return fmt.Errorf("failed to create combined PR: %w", err)
110+
return 0, fmt.Errorf("failed to encode payload: %w", err)
101111
}
102112

103-
return nil
113+
var prResponse struct {
114+
Number int `json:"number"`
115+
}
116+
err = client.Post(endpoint, requestBody, &prResponse)
117+
if err != nil {
118+
return 0, fmt.Errorf("failed to create pull request: %w", err)
119+
}
120+
121+
if len(labels) > 0 {
122+
labelsEndpoint := fmt.Sprintf("repos/%s/%s/issues/%d/labels", repo.Owner, repo.Repo, prResponse.Number)
123+
labelsPayload, err := encodePayload(map[string][]string{"labels": labels})
124+
if err != nil {
125+
return prResponse.Number, fmt.Errorf("failed to encode labels payload: %w", err)
126+
}
127+
err = client.Post(labelsEndpoint, labelsPayload, nil)
128+
if err != nil {
129+
return prResponse.Number, fmt.Errorf("failed to add labels: %w", err)
130+
}
131+
}
132+
133+
if len(assignees) > 0 {
134+
assigneesEndpoint := fmt.Sprintf("repos/%s/%s/issues/%d/assignees", repo.Owner, repo.Repo, prResponse.Number)
135+
assigneesPayload, err := encodePayload(map[string][]string{"assignees": assignees})
136+
if err != nil {
137+
return prResponse.Number, fmt.Errorf("failed to encode assignees payload: %w", err)
138+
}
139+
err = client.Post(assigneesEndpoint, assigneesPayload, nil)
140+
if err != nil {
141+
return prResponse.Number, fmt.Errorf("failed to add assignees: %w", err)
142+
}
143+
}
144+
145+
return prResponse.Number, nil
104146
}
105147

106148
// isMergeConflictError checks if the error is a 409 Merge Conflict

internal/cmd/root.go

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"time"
78

89
"github.com/cli/go-gh/v2/pkg/api"
910
"github.com/spf13/cobra"
@@ -35,8 +36,31 @@ var (
3536
workingBranchSuffix string
3637
dependabot bool
3738
caseSensitiveLabels bool
39+
noColor bool
40+
noStats bool
3841
)
3942

43+
// StatsCollector tracks stats for the CLI run
44+
type StatsCollector struct {
45+
ReposProcessed int
46+
PRsCombined int
47+
PRsSkippedMergeConflict int
48+
PRsSkippedCriteria int
49+
PerRepoStats map[string]*RepoStats
50+
CombinedPRLinks []string
51+
StartTime time.Time
52+
EndTime time.Time
53+
}
54+
55+
type RepoStats struct {
56+
RepoName string
57+
CombinedCount int
58+
SkippedMergeConf int
59+
SkippedCriteria int
60+
CombinedPRLink string
61+
NotEnoughPRs bool
62+
}
63+
4064
// NewRootCmd creates the root command for the gh-combine CLI
4165
func NewRootCmd() *cobra.Command {
4266
rootCmd := &cobra.Command{
@@ -96,6 +120,8 @@ func NewRootCmd() *cobra.Command {
96120
# Additional options
97121
gh combine owner/repo --autoclose # Close source PRs when combined PR is merged
98122
gh combine owner/repo --base-branch main # Use a different base branch for the combined PR
123+
gh combine owner/repo --no-color # Disable color output
124+
gh combine owner/repo --no-stats # Disable stats summary display
99125
gh combine owner/repo --combine-branch-name combined-prs # Use a different name for the combined PR branch
100126
gh combine owner/repo --working-branch-suffix -working # Use a different suffix for the working branch
101127
gh combine owner/repo --update-branch # Update the branch of the combined PR`,
@@ -127,6 +153,8 @@ func NewRootCmd() *cobra.Command {
127153
rootCmd.Flags().IntVar(&minimum, "minimum", 2, "Minimum number of PRs to combine")
128154
rootCmd.Flags().StringVar(&defaultOwner, "owner", "", "Default owner for repositories (if not specified in repo name or missing from file inputs)")
129155
rootCmd.Flags().BoolVar(&caseSensitiveLabels, "case-sensitive-labels", false, "Use case-sensitive label matching")
156+
rootCmd.Flags().BoolVar(&noColor, "no-color", false, "Disable color output")
157+
rootCmd.Flags().BoolVar(&noStats, "no-stats", false, "Disable stats summary display")
130158

131159
// Add deprecated flags for backward compatibility
132160
// rootCmd.Flags().IntVar(&minimum, "min-combine", 2, "Minimum number of PRs to combine (deprecated, use --minimum)")
@@ -172,16 +200,27 @@ func runCombine(cmd *cobra.Command, args []string) error {
172200
return errors.New("no repositories specified")
173201
}
174202

203+
stats := &StatsCollector{
204+
PerRepoStats: make(map[string]*RepoStats),
205+
StartTime: time.Now(),
206+
}
207+
175208
// Execute combination logic
176-
if err := executeCombineCommand(ctx, spinner, repos); err != nil {
209+
if err := executeCombineCommand(ctx, spinner, repos, stats); err != nil {
177210
return fmt.Errorf("command execution failed: %w", err)
178211
}
212+
stats.EndTime = time.Now()
213+
214+
if !noStats {
215+
spinner.Stop()
216+
displayStatsSummary(stats)
217+
}
179218

180219
return nil
181220
}
182221

183222
// executeCombineCommand performs the actual API calls and processing
184-
func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string) error {
223+
func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string, stats *StatsCollector) error {
185224
// Create GitHub API client
186225
restClient, err := api.DefaultRESTClient()
187226
if err != nil {
@@ -213,23 +252,27 @@ func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string
213252
spinner.UpdateMessage("Processing " + repo.String())
214253
Logger.Debug("Processing repository", "repo", repo)
215254

255+
if stats.PerRepoStats[repo.String()] == nil {
256+
stats.PerRepoStats[repo.String()] = &RepoStats{RepoName: repo.String()}
257+
}
258+
216259
// Process the repository
217-
if err := processRepository(ctx, restClient, graphQlClient, spinner, repo); err != nil {
260+
if err := processRepository(ctx, restClient, graphQlClient, spinner, repo, stats.PerRepoStats[repo.String()], stats); err != nil {
218261
if ctx.Err() != nil {
219262
// If the context was cancelled, stop processing
220263
return ctx.Err()
221264
}
222-
// Otherwise just log the error and continue
223265
Logger.Warn("Failed to process repository", "repo", repo, "error", err)
224266
continue
225267
}
268+
stats.ReposProcessed++
226269
}
227270

228271
return nil
229272
}
230273

231274
// processRepository handles a single repository's PRs
232-
func processRepository(ctx context.Context, client *api.RESTClient, graphQlClient *api.GraphQLClient, spinner *Spinner, repo github.Repo) error {
275+
func processRepository(ctx context.Context, client *api.RESTClient, graphQlClient *api.GraphQLClient, spinner *Spinner, repo github.Repo, repoStats *RepoStats, stats *StatsCollector) error {
233276
// Check for cancellation
234277
select {
235278
case <-ctx.Done():
@@ -263,6 +306,8 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
263306

264307
// Check if PR matches all filtering criteria
265308
if !PrMatchesCriteria(pull.Head.Ref, labels) {
309+
repoStats.SkippedCriteria++
310+
stats.PRsSkippedCriteria++
266311
continue
267312
}
268313

@@ -274,7 +319,8 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
274319
}
275320

276321
if !meetsRequirements {
277-
// Skip this PR as it doesn't meet CI/approval requirements
322+
repoStats.SkippedCriteria++
323+
stats.PRsSkippedCriteria++
278324
continue
279325
}
280326

@@ -284,6 +330,7 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
284330
// Check if we have enough PRs to combine
285331
if len(matchedPRs) < minimum {
286332
Logger.Debug("Not enough PRs match criteria", "repo", repo, "matched", len(matchedPRs), "required", minimum)
333+
repoStats.NotEnoughPRs = true
287334
return nil
288335
}
289336

@@ -294,12 +341,21 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
294341
RESTClientInterface
295342
}{client}
296343

297-
// Combine the PRs
298-
err = CombinePRs(ctx, graphQlClient, restClientWrapper, repo, matchedPRs)
344+
// Combine the PRs and collect stats
345+
combined, mergeConflicts, combinedPRLink, err := CombinePRsWithStats(ctx, graphQlClient, restClientWrapper, repo, matchedPRs)
299346
if err != nil {
300347
return fmt.Errorf("failed to combine PRs: %w", err)
301348
}
302349

350+
repoStats.CombinedCount = len(combined)
351+
repoStats.SkippedMergeConf = len(mergeConflicts)
352+
repoStats.CombinedPRLink = combinedPRLink
353+
stats.PRsCombined += len(combined)
354+
stats.PRsSkippedMergeConflict += len(mergeConflicts)
355+
if combinedPRLink != "" {
356+
stats.CombinedPRLinks = append(stats.CombinedPRLinks, combinedPRLink)
357+
}
358+
303359
Logger.Debug("Combined PRs", "count", len(matchedPRs), "owner", repo.Owner, "repo", repo.Repo)
304360

305361
return nil
@@ -343,3 +399,41 @@ func fetchOpenPullRequests(ctx context.Context, client *api.RESTClient, repo git
343399

344400
return allPulls, nil
345401
}
402+
403+
func displayStatsSummary(stats *StatsCollector) {
404+
elapsed := stats.EndTime.Sub(stats.StartTime)
405+
if noColor {
406+
fmt.Println("Stats Summary (Color Disabled):")
407+
} else {
408+
fmt.Println("\033[1;34mStats Summary:\033[0m")
409+
}
410+
fmt.Printf("Repositories Processed: %d\n", stats.ReposProcessed)
411+
fmt.Printf("PRs Combined: %d\n", stats.PRsCombined)
412+
fmt.Printf("PRs Skipped (Merge Conflicts): %d\n", stats.PRsSkippedMergeConflict)
413+
fmt.Printf("PRs Skipped (Criteria Not Met): %d\n", stats.PRsSkippedCriteria)
414+
fmt.Printf("Execution Time: %s\n", elapsed.Round(time.Second))
415+
416+
if !noColor {
417+
fmt.Println("\033[1;32mLinks to Combined PRs:\033[0m")
418+
} else {
419+
fmt.Println("Links to Combined PRs:")
420+
}
421+
for _, link := range stats.CombinedPRLinks {
422+
fmt.Println("-", link)
423+
}
424+
425+
fmt.Println("\nPer-Repository Details:")
426+
for _, repoStat := range stats.PerRepoStats {
427+
fmt.Printf(" %s\n", repoStat.RepoName)
428+
if repoStat.NotEnoughPRs {
429+
fmt.Println(" Not enough PRs to combine.")
430+
continue
431+
}
432+
fmt.Printf(" Combined: %d\n", repoStat.CombinedCount)
433+
fmt.Printf(" Skipped (Merge Conflicts): %d\n", repoStat.SkippedMergeConf)
434+
fmt.Printf(" Skipped (Criteria): %d\n", repoStat.SkippedCriteria)
435+
if repoStat.CombinedPRLink != "" {
436+
fmt.Printf(" Combined PR: %s\n", repoStat.CombinedPRLink)
437+
}
438+
}
439+
}

0 commit comments

Comments
 (0)