4
4
"context"
5
5
"errors"
6
6
"fmt"
7
+ "time"
7
8
8
9
"github.com/cli/go-gh/v2/pkg/api"
9
10
"github.com/spf13/cobra"
35
36
workingBranchSuffix string
36
37
dependabot bool
37
38
caseSensitiveLabels bool
39
+ noColor bool
40
+ noStats bool
38
41
)
39
42
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
+
40
64
// NewRootCmd creates the root command for the gh-combine CLI
41
65
func NewRootCmd () * cobra.Command {
42
66
rootCmd := & cobra.Command {
@@ -96,6 +120,8 @@ func NewRootCmd() *cobra.Command {
96
120
# Additional options
97
121
gh combine owner/repo --autoclose # Close source PRs when combined PR is merged
98
122
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
99
125
gh combine owner/repo --combine-branch-name combined-prs # Use a different name for the combined PR branch
100
126
gh combine owner/repo --working-branch-suffix -working # Use a different suffix for the working branch
101
127
gh combine owner/repo --update-branch # Update the branch of the combined PR` ,
@@ -127,6 +153,8 @@ func NewRootCmd() *cobra.Command {
127
153
rootCmd .Flags ().IntVar (& minimum , "minimum" , 2 , "Minimum number of PRs to combine" )
128
154
rootCmd .Flags ().StringVar (& defaultOwner , "owner" , "" , "Default owner for repositories (if not specified in repo name or missing from file inputs)" )
129
155
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" )
130
158
131
159
// Add deprecated flags for backward compatibility
132
160
// 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 {
172
200
return errors .New ("no repositories specified" )
173
201
}
174
202
203
+ stats := & StatsCollector {
204
+ PerRepoStats : make (map [string ]* RepoStats ),
205
+ StartTime : time .Now (),
206
+ }
207
+
175
208
// Execute combination logic
176
- if err := executeCombineCommand (ctx , spinner , repos ); err != nil {
209
+ if err := executeCombineCommand (ctx , spinner , repos , stats ); err != nil {
177
210
return fmt .Errorf ("command execution failed: %w" , err )
178
211
}
212
+ stats .EndTime = time .Now ()
213
+
214
+ if ! noStats {
215
+ spinner .Stop ()
216
+ displayStatsSummary (stats )
217
+ }
179
218
180
219
return nil
181
220
}
182
221
183
222
// 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 {
185
224
// Create GitHub API client
186
225
restClient , err := api .DefaultRESTClient ()
187
226
if err != nil {
@@ -213,23 +252,27 @@ func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string
213
252
spinner .UpdateMessage ("Processing " + repo .String ())
214
253
Logger .Debug ("Processing repository" , "repo" , repo )
215
254
255
+ if stats .PerRepoStats [repo .String ()] == nil {
256
+ stats .PerRepoStats [repo .String ()] = & RepoStats {RepoName : repo .String ()}
257
+ }
258
+
216
259
// 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 {
218
261
if ctx .Err () != nil {
219
262
// If the context was cancelled, stop processing
220
263
return ctx .Err ()
221
264
}
222
- // Otherwise just log the error and continue
223
265
Logger .Warn ("Failed to process repository" , "repo" , repo , "error" , err )
224
266
continue
225
267
}
268
+ stats .ReposProcessed ++
226
269
}
227
270
228
271
return nil
229
272
}
230
273
231
274
// 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 {
233
276
// Check for cancellation
234
277
select {
235
278
case <- ctx .Done ():
@@ -263,6 +306,8 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
263
306
264
307
// Check if PR matches all filtering criteria
265
308
if ! PrMatchesCriteria (pull .Head .Ref , labels ) {
309
+ repoStats .SkippedCriteria ++
310
+ stats .PRsSkippedCriteria ++
266
311
continue
267
312
}
268
313
@@ -274,7 +319,8 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
274
319
}
275
320
276
321
if ! meetsRequirements {
277
- // Skip this PR as it doesn't meet CI/approval requirements
322
+ repoStats .SkippedCriteria ++
323
+ stats .PRsSkippedCriteria ++
278
324
continue
279
325
}
280
326
@@ -284,6 +330,7 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
284
330
// Check if we have enough PRs to combine
285
331
if len (matchedPRs ) < minimum {
286
332
Logger .Debug ("Not enough PRs match criteria" , "repo" , repo , "matched" , len (matchedPRs ), "required" , minimum )
333
+ repoStats .NotEnoughPRs = true
287
334
return nil
288
335
}
289
336
@@ -294,12 +341,21 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
294
341
RESTClientInterface
295
342
}{client }
296
343
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 )
299
346
if err != nil {
300
347
return fmt .Errorf ("failed to combine PRs: %w" , err )
301
348
}
302
349
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
+
303
359
Logger .Debug ("Combined PRs" , "count" , len (matchedPRs ), "owner" , repo .Owner , "repo" , repo .Repo )
304
360
305
361
return nil
@@ -343,3 +399,41 @@ func fetchOpenPullRequests(ctx context.Context, client *api.RESTClient, repo git
343
399
344
400
return allPulls , nil
345
401
}
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 ("\n Per-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