Skip to content

Commit ec0ad41

Browse files
committed
stats
1 parent 387747c commit ec0ad41

File tree

2 files changed

+166
-50
lines changed

2 files changed

+166
-50
lines changed

internal/cmd/combine_prs.go

Lines changed: 76 additions & 28 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

38-
// Delete any pre-existing working branch
38+
// 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,106 @@ 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
146+
}
147+
148+
// Keep CombinePRs for backward compatibility
149+
func CombinePRs(ctx context.Context, graphQlClient *api.GraphQLClient, restClient RESTClientInterface, repo github.Repo, pulls github.Pulls) error {
150+
_, _, _, err := CombinePRsWithStats(ctx, graphQlClient, restClient, repo, pulls)
151+
return err
104152
}
105153

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

internal/cmd/root.go

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

810
"github.com/cli/go-gh/v2/pkg/api"
911
"github.com/spf13/cobra"
@@ -39,6 +41,27 @@ var (
3941
noStats bool
4042
)
4143

44+
// StatsCollector tracks stats for the CLI run
45+
type StatsCollector struct {
46+
ReposProcessed int
47+
PRsCombined int
48+
PRsSkippedMergeConflict int
49+
PRsSkippedCriteria int
50+
PerRepoStats map[string]*RepoStats
51+
CombinedPRLinks []string
52+
StartTime time.Time
53+
EndTime time.Time
54+
}
55+
56+
type RepoStats struct {
57+
RepoName string
58+
CombinedCount int
59+
SkippedMergeConf int
60+
SkippedCriteria int
61+
CombinedPRLink string
62+
NotEnoughPRs bool
63+
}
64+
4265
// NewRootCmd creates the root command for the gh-combine CLI
4366
func NewRootCmd() *cobra.Command {
4467
rootCmd := &cobra.Command{
@@ -178,20 +201,27 @@ func runCombine(cmd *cobra.Command, args []string) error {
178201
return errors.New("no repositories specified")
179202
}
180203

204+
stats := &StatsCollector{
205+
PerRepoStats: make(map[string]*RepoStats),
206+
StartTime: time.Now(),
207+
}
208+
181209
// Execute combination logic
182-
if err := executeCombineCommand(ctx, spinner, repos); err != nil {
210+
if err := executeCombineCommand(ctx, spinner, repos, stats); err != nil {
183211
return fmt.Errorf("command execution failed: %w", err)
184212
}
213+
stats.EndTime = time.Now()
185214

186215
if !noStats {
187-
displayStatsSummary()
216+
spinner.Stop()
217+
displayStatsSummary(stats)
188218
}
189219

190220
return nil
191221
}
192222

193223
// executeCombineCommand performs the actual API calls and processing
194-
func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string) error {
224+
func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string, stats *StatsCollector) error {
195225
// Create GitHub API client
196226
restClient, err := api.DefaultRESTClient()
197227
if err != nil {
@@ -223,23 +253,27 @@ func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string
223253
spinner.UpdateMessage("Processing " + repo.String())
224254
Logger.Debug("Processing repository", "repo", repo)
225255

256+
if stats.PerRepoStats[repo.String()] == nil {
257+
stats.PerRepoStats[repo.String()] = &RepoStats{RepoName: repo.String()}
258+
}
259+
226260
// Process the repository
227-
if err := processRepository(ctx, restClient, graphQlClient, spinner, repo); err != nil {
261+
if err := processRepository(ctx, restClient, graphQlClient, spinner, repo, stats.PerRepoStats[repo.String()], stats); err != nil {
228262
if ctx.Err() != nil {
229263
// If the context was cancelled, stop processing
230264
return ctx.Err()
231265
}
232-
// Otherwise just log the error and continue
233266
Logger.Warn("Failed to process repository", "repo", repo, "error", err)
234267
continue
235268
}
269+
stats.ReposProcessed++
236270
}
237271

238272
return nil
239273
}
240274

241275
// processRepository handles a single repository's PRs
242-
func processRepository(ctx context.Context, client *api.RESTClient, graphQlClient *api.GraphQLClient, spinner *Spinner, repo github.Repo) error {
276+
func processRepository(ctx context.Context, client *api.RESTClient, graphQlClient *api.GraphQLClient, spinner *Spinner, repo github.Repo, repoStats *RepoStats, stats *StatsCollector) error {
243277
// Check for cancellation
244278
select {
245279
case <-ctx.Done():
@@ -273,6 +307,8 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
273307

274308
// Check if PR matches all filtering criteria
275309
if !PrMatchesCriteria(pull.Head.Ref, labels) {
310+
repoStats.SkippedCriteria++
311+
stats.PRsSkippedCriteria++
276312
continue
277313
}
278314

@@ -284,7 +320,8 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
284320
}
285321

286322
if !meetsRequirements {
287-
// Skip this PR as it doesn't meet CI/approval requirements
323+
repoStats.SkippedCriteria++
324+
stats.PRsSkippedCriteria++
288325
continue
289326
}
290327

@@ -294,6 +331,7 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
294331
// Check if we have enough PRs to combine
295332
if len(matchedPRs) < minimum {
296333
Logger.Debug("Not enough PRs match criteria", "repo", repo, "matched", len(matchedPRs), "required", minimum)
334+
repoStats.NotEnoughPRs = true
297335
return nil
298336
}
299337

@@ -304,12 +342,21 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
304342
RESTClientInterface
305343
}{client}
306344

307-
// Combine the PRs
308-
err = CombinePRs(ctx, graphQlClient, restClientWrapper, repo, matchedPRs)
345+
// Combine the PRs and collect stats
346+
combined, mergeConflicts, combinedPRLink, err := CombinePRsWithStats(ctx, graphQlClient, restClientWrapper, repo, matchedPRs)
309347
if err != nil {
310348
return fmt.Errorf("failed to combine PRs: %w", err)
311349
}
312350

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

315362
return nil
@@ -354,26 +401,47 @@ func fetchOpenPullRequests(ctx context.Context, client *api.RESTClient, repo git
354401
return allPulls, nil
355402
}
356403

357-
func displayStatsSummary() {
358-
// Example implementation of stats summary display
404+
// CombinePRsWithStats wraps CombinePRs to collect stats and return combined/skipped PRs and the combined PR link
405+
func CombinePRsWithStats(ctx context.Context, graphQlClient *api.GraphQLClient, restClient RESTClientInterface, repo github.Repo, pulls github.Pulls) (combined []string, mergeConflicts []string, combinedPRLink string, err error) {
406+
// ...existing code for CombinePRs...
407+
// This is a stub. You should move the logic from CombinePRs here and update it to collect combined, mergeConflicts, and the PR link.
408+
return nil, nil, "", CombinePRs(ctx, graphQlClient, restClient, repo, pulls)
409+
}
410+
411+
func displayStatsSummary(stats *StatsCollector) {
412+
elapsed := stats.EndTime.Sub(stats.StartTime)
359413
if noColor {
360414
fmt.Println("Stats Summary (Color Disabled):")
361415
} else {
362-
fmt.Println("\033[1;34mStats Summary:\033[0m") // Blue color for title
416+
fmt.Println("\033[1;34mStats Summary:\033[0m")
363417
}
364-
365-
// Example stats data
366-
fmt.Println("Repositories Processed: 5")
367-
fmt.Println("PRs Combined: 10")
368-
fmt.Println("PRs Skipped (Merge Conflicts): 2")
369-
fmt.Println("PRs Skipped (Criteria Not Met): 3")
370-
fmt.Println("Execution Time: 1m30s")
418+
fmt.Printf("Repositories Processed: %d\n", stats.ReposProcessed)
419+
fmt.Printf("PRs Combined: %d\n", stats.PRsCombined)
420+
fmt.Printf("PRs Skipped (Merge Conflicts): %d\n", stats.PRsSkippedMergeConflict)
421+
fmt.Printf("PRs Skipped (Criteria Not Met): %d\n", stats.PRsSkippedCriteria)
422+
fmt.Printf("Execution Time: %s\n", elapsed.Round(time.Second))
371423

372424
if !noColor {
373-
fmt.Println("\033[1;32mLinks to Combined PRs:\033[0m") // Green color for links section
425+
fmt.Println("\033[1;32mLinks to Combined PRs:\033[0m")
374426
} else {
375427
fmt.Println("Links to Combined PRs:")
376428
}
377-
fmt.Println("- https://github.com/owner/repo1/pull/123")
378-
fmt.Println("- https://github.com/owner/repo2/pull/456")
429+
for _, link := range stats.CombinedPRLinks {
430+
fmt.Println("-", link)
431+
}
432+
433+
fmt.Println("\nPer-Repository Details:")
434+
for _, repoStat := range stats.PerRepoStats {
435+
fmt.Printf(" %s\n", repoStat.RepoName)
436+
if repoStat.NotEnoughPRs {
437+
fmt.Println(" Not enough PRs to combine.")
438+
continue
439+
}
440+
fmt.Printf(" Combined: %d\n", repoStat.CombinedCount)
441+
fmt.Printf(" Skipped (Merge Conflicts): %d\n", repoStat.SkippedMergeConf)
442+
fmt.Printf(" Skipped (Criteria): %d\n", repoStat.SkippedCriteria)
443+
if repoStat.CombinedPRLink != "" {
444+
fmt.Printf(" Combined PR: %s\n", repoStat.CombinedPRLink)
445+
}
446+
}
379447
}

0 commit comments

Comments
 (0)