Skip to content

feat: add --dry-run flag #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ Combine all open pull requests in a repository that are created by dependabot:
gh combine owner/repo --dependabot
```

### In Dry Run Mode

You can run in dry run mode to see what would happen without actually creating a pull request or combining any pull requests:

```bash
gh combine owner/repo --dry-run
```

### With Passing CI

Combine multiple pull requests together but only if their CI checks are passing:
Expand Down
117 changes: 69 additions & 48 deletions internal/cmd/combine_prs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,76 +20,97 @@ type RESTClientInterface interface {
Patch(endpoint string, body io.Reader, response interface{}) error
}

// CombineOpts holds options for combining PRs
// Use this struct to pass options to CombinePRsWithStats and related functions
// This makes the code more maintainable and clear
type CombineOpts struct {
Noop bool
Command string
Repo github.Repo
Pulls github.Pulls
}

// CombinePRsWithStats combines PRs and returns stats for summary output
func CombinePRsWithStats(ctx context.Context, graphQlClient *api.GraphQLClient, restClient RESTClientInterface, repo github.Repo, pulls github.Pulls, command string) (combined []string, mergeConflicts []string, combinedPRLink string, err error) {
func CombinePRsWithStats(ctx context.Context, graphQlClient *api.GraphQLClient, restClient RESTClientInterface, opts CombineOpts) (combined []string, mergeConflicts []string, combinedPRLink string, err error) {
workingBranchName := combineBranchName + workingBranchSuffix

repoDefaultBranch, err := getDefaultBranch(ctx, restClient, repo)
repoDefaultBranch, err := getDefaultBranch(ctx, restClient, opts.Repo)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to get default branch: %w", err)
}

baseBranchSHA, err := getBranchSHA(ctx, restClient, repo, repoDefaultBranch)
baseBranchSHA, err := getBranchSHA(ctx, restClient, opts.Repo, repoDefaultBranch)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to get SHA of main branch: %w", err)
}
// Delete any pre-existing working branch

// Delete any pre-existing working branch
err = deleteBranch(ctx, restClient, repo, workingBranchName)
if err != nil {
Logger.Debug("Working branch not found, continuing", "branch", workingBranchName)

// Delete any pre-existing combined branch
if opts.Noop {
Logger.Debug("Dry-run mode enabled. No changes will be made.")
Logger.Debug("Simulating branch operations", "workingBranch", workingBranchName, "defaultBranch", repoDefaultBranch)
}

// Delete any pre-existing combined branch
err = deleteBranch(ctx, restClient, repo, combineBranchName)
if err != nil {
Logger.Debug("Combined branch not found, continuing", "branch", combineBranchName)
}
if !opts.Noop {
err = deleteBranch(ctx, restClient, opts.Repo, workingBranchName)
if err != nil {
Logger.Debug("Working branch not found, continuing", "branch", workingBranchName)
}

err = createBranch(ctx, restClient, repo, combineBranchName, baseBranchSHA)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to create combined branch: %w", err)
}
err = createBranch(ctx, restClient, repo, workingBranchName, baseBranchSHA)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to create working branch: %w", err)
}
err = deleteBranch(ctx, restClient, opts.Repo, combineBranchName)
if err != nil {
Logger.Debug("Combined branch not found, continuing", "branch", combineBranchName)
}

for _, pr := range pulls {
err := mergeBranch(ctx, restClient, repo, workingBranchName, pr.Head.Ref)
err = createBranch(ctx, restClient, opts.Repo, combineBranchName, baseBranchSHA)
if err != nil {
if isMergeConflictError(err) {
Logger.Debug("Merge conflict", "branch", pr.Head.Ref, "error", err)
return nil, nil, "", fmt.Errorf("failed to create combined branch: %w", err)
}

err = createBranch(ctx, restClient, opts.Repo, workingBranchName, baseBranchSHA)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to create working branch: %w", err)
}
}

for _, pr := range opts.Pulls {
if opts.Noop {
Logger.Debug("Simulating merge of branch", "branch", pr.Head.Ref)
combined = append(combined, fmt.Sprintf("#%d - %s", pr.Number, pr.Title))
} else {
err := mergeBranch(ctx, restClient, opts.Repo, workingBranchName, pr.Head.Ref)
if err != nil {
if isMergeConflictError(err) {
Logger.Debug("Merge conflict", "branch", pr.Head.Ref, "error", err)
} else {
Logger.Warn("Failed to merge branch", "branch", pr.Head.Ref, "error", err)
}
mergeConflicts = append(mergeConflicts, fmt.Sprintf("#%d", pr.Number))
} else {
Logger.Warn("Failed to merge branch", "branch", pr.Head.Ref, "error", err)
Logger.Debug("Merged branch", "branch", pr.Head.Ref)
combined = append(combined, fmt.Sprintf("#%d - %s", pr.Number, pr.Title))
}
mergeConflicts = append(mergeConflicts, fmt.Sprintf("#%d", pr.Number))
} else {
Logger.Debug("Merged branch", "branch", pr.Head.Ref)
combined = append(combined, fmt.Sprintf("#%d - %s", pr.Number, pr.Title))
}
}

err = updateRef(ctx, restClient, repo, combineBranchName, workingBranchName)
if err != nil {
return combined, mergeConflicts, "", fmt.Errorf("failed to update combined branch: %w", err)
}
err = deleteBranch(ctx, restClient, repo, workingBranchName)
if err != nil {
Logger.Warn("Failed to delete working branch", "branch", workingBranchName, "error", err)
}
if !opts.Noop {
err = updateRef(ctx, restClient, opts.Repo, combineBranchName, workingBranchName)
if err != nil {
return combined, mergeConflicts, "", fmt.Errorf("failed to update combined branch: %w", err)
}

prBody := generatePRBody(combined, mergeConflicts, command)
prTitle := "Combined PRs"
prNumber, prErr := createPullRequestWithNumber(ctx, restClient, repo, prTitle, combineBranchName, repoDefaultBranch, prBody, addLabels, addAssignees)
if prErr != nil {
return combined, mergeConflicts, "", fmt.Errorf("failed to create combined PR: %w", prErr)
}
if prNumber > 0 {
combinedPRLink = fmt.Sprintf("https://github.com/%s/%s/pull/%d", repo.Owner, repo.Repo, prNumber)
err = deleteBranch(ctx, restClient, opts.Repo, workingBranchName)
if err != nil {
Logger.Warn("Failed to delete working branch", "branch", workingBranchName, "error", err)
}

prBody := generatePRBody(combined, mergeConflicts, opts.Command)
prTitle := "Combined PRs"
prNumber, prErr := createPullRequestWithNumber(ctx, restClient, opts.Repo, prTitle, combineBranchName, repoDefaultBranch, prBody, addLabels, addAssignees)
if prErr != nil {
return combined, mergeConflicts, "", fmt.Errorf("failed to create combined PR: %w", prErr)
}
if prNumber > 0 {
combinedPRLink = fmt.Sprintf("https://github.com/%s/%s/pull/%d", opts.Repo.Owner, opts.Repo.Repo, prNumber)
}
}

return combined, mergeConflicts, combinedPRLink, nil
Expand Down
10 changes: 8 additions & 2 deletions internal/cmd/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,20 @@ func displaySummaryTable(stats *StatsCollector) {
21, // Fixed width for the skipped column
)

summaryRowPRCount := interface{}(len(stats.CombinedPRLinks))

if summaryRowPRCount == 0 && dryRun {
summaryRowPRCount = "DRY RUN"
}

// Generate the summary row
summaryRow := fmt.Sprintf(
"│ %-13d │ %-13d │ %s%s │ %-13d │",
"│ %-13d │ %-13d │ %s%s │ %-13v │",
stats.ReposProcessed,
stats.PRsCombined,
skippedSummaryText,
summaryPadding,
len(stats.CombinedPRLinks),
summaryRowPRCount,
)

// Print the summary table
Expand Down
17 changes: 15 additions & 2 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (
noColor bool
noStats bool
outputFormat string
dryRun bool
)

// StatsCollector tracks stats for the CLI run
Expand Down Expand Up @@ -114,6 +115,7 @@ func NewRootCmd() *cobra.Command {
gh combine owner/repo --add-assignees octocat,hubot # Assign users to the new PR

# Additional options
gh combine owner/repo --dry-run # Simulate the actions without making any changes
gh combine owner/repo --autoclose # Close source PRs when combined PR is merged
gh combine owner/repo --base-branch main # Use a different base branch for the combined PR
gh combine owner/repo --no-color # Disable color output
Expand Down Expand Up @@ -154,6 +156,7 @@ func NewRootCmd() *cobra.Command {
rootCmd.Flags().BoolVar(&noColor, "no-color", false, "Disable color output")
rootCmd.Flags().BoolVar(&noStats, "no-stats", false, "Disable stats summary display")
rootCmd.Flags().StringVar(&outputFormat, "output", "table", "Output format: table, plain, or json")
rootCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Simulate the actions without making any changes")

// Add deprecated flags for backward compatibility
// rootCmd.Flags().IntVar(&minimum, "min-combine", 2, "Minimum number of PRs to combine (deprecated, use --minimum)")
Expand Down Expand Up @@ -336,9 +339,16 @@ func processRepository(ctx context.Context, client *api.RESTClient, graphQlClien
RESTClientInterface
}{client}

// Combine the PRs and collect stats
commandString := buildCommandString([]string{repo.String()})
combined, mergeConflicts, combinedPRLink, err := CombinePRsWithStats(ctx, graphQlClient, restClientWrapper, repo, matchedPRs, commandString)

opts := CombineOpts{
Noop: dryRun,
Command: commandString,
Repo: repo,
Pulls: matchedPRs,
}

combined, mergeConflicts, combinedPRLink, err := CombinePRsWithStats(ctx, graphQlClient, restClientWrapper, opts)
if err != nil {
return fmt.Errorf("failed to combine PRs: %w", err)
}
Expand Down Expand Up @@ -477,6 +487,9 @@ func buildCommandString(args []string) string {
if outputFormat != "table" && outputFormat != "" {
cmd = append(cmd, "--output", outputFormat)
}
if dryRun {
cmd = append(cmd, "--dry-run")
}

return strings.Join(cmd, " ")
}
Loading