Skip to content

Commit 720616a

Browse files
authored
feat: Add --draft flag for draft pull requests (#49)
1 parent 6c945da commit 720616a

File tree

9 files changed

+83
-10
lines changed

9 files changed

+83
-10
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ Oftentimes, you want finer-grained control over the exact repos you are going to
319319
320320
```
321321
git-xargs \
322-
--commit-mesage "Update copyright year" \
322+
--commit-message "Update copyright year" \
323323
--repos data/batch2.txt \
324324
"$(pwd)/scripts/update-copyright-year.sh"
325325
```
@@ -343,7 +343,7 @@ arguments:
343343
344344
```
345345
git-xargs \
346-
--commit-mesage "Update copyright year" \
346+
--commit-message "Update copyright year" \
347347
--repo gruntwork-io/terragrunt \
348348
--repo gruntwork-io/terratest \
349349
--repo gruntwork-io/cloud-nuke \
@@ -357,7 +357,7 @@ use by piping them in via `stdin`, separating repo names with whitespace or newl
357357
358358
```
359359
echo "gruntwork-io/terragrunt gruntwork-io/terratest" | git-xargs \
360-
--commit-mesage "Update copyright year" \
360+
--commit-message "Update copyright year" \
361361
"$(pwd)/scripts/update-copyright-year.sh"
362362
```
363363
@@ -377,6 +377,8 @@ echo "gruntwork-io/terragrunt gruntwork-io/terratest" | git-xargs \
377377
| `--skip-archived-repos` | If you want to exclude archived (read-only) repositories from the list of targeted repos, pass this flag. | Boolean | No |
378378
| `--dry-run` | If you are in the process of testing out `git-xargs` or your initial set of targeted repos, but you don't want to make any changes via the Github API (pushing your local changes or opening pull requests) you can pass the dry-run flag. This is useful because the output report will still tell you which repos would have been affected, without actually making changes via the Github API to your remote repositories. | Boolean | No |
379379
| `--max-concurrent-repos` | Limits the number of concurrent processed repositories. This is only useful if you encounter issues and need throttling when running on a very large number of repos. Default is `0` (Unlimited) | Integer | No |
380+
| `--draft` | Whether to open pull requests in draft mode. Draft pull requests are available for public GitHub repositories and private repositories in GitHub tiered accounts. See [Draft Pull Requests](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests#draft-pull-requests) for more details. | Boolean | No |
381+
380382
381383
## Best practices, tips and tricks
382384

cmd/git-xargs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
// to an internal representation of the data supplied by the user
2121
func parseGitXargsConfig(c *cli.Context) (*config.GitXargsConfig, error) {
2222
config := config.NewGitXargsConfig()
23+
config.Draft = c.Bool("draft")
2324
config.DryRun = c.Bool("dry-run")
2425
config.SkipPullRequests = c.Bool("skip-pull-requests")
2526
config.SkipArchivedRepos = c.Bool("skip-archived-repos")

common/common.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "github.com/urfave/cli"
44

55
const (
66
GithubOrgFlagName = "github-org"
7+
DraftPullRequestFlagName = "draft"
78
DryRunFlagName = "dry-run"
89
SkipPullRequestsFlagName = "skip-pull-requests"
910
SkipArchivedReposFlagName = "skip-archived-repos"
@@ -25,6 +26,10 @@ var (
2526
Name: GithubOrgFlagName,
2627
Usage: "The Github organization to fetch all repositories from.",
2728
}
29+
GenericDraftPullRequestFlag = cli.BoolFlag{
30+
Name: DraftPullRequestFlagName,
31+
Usage: "Whether to open pull requests in draft mode",
32+
}
2833
GenericDryRunFlag = cli.BoolFlag{
2934
Name: DryRunFlagName,
3035
Usage: "When dry-run is set to true, no local branch changes will pushed and no pull requests will be opened.",

config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
// GitXargsConfig is the internal representation of a given git-xargs run as specified by the user
1414
type GitXargsConfig struct {
15+
Draft bool
1516
DryRun bool
1617
SkipPullRequests bool
1718
SkipArchivedRepos bool
@@ -33,6 +34,7 @@ type GitXargsConfig struct {
3334
// NewGitXargsConfig sets reasonable defaults for a GitXargsConfig and returns a pointer to the config
3435
func NewGitXargsConfig() *GitXargsConfig {
3536
return &GitXargsConfig{
37+
Draft: false,
3638
DryRun: false,
3739
SkipPullRequests: false,
3840
SkipArchivedRepos: false,

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func setupApp() *cli.App {
6262
app.Flags = []cli.Flag{
6363
LogLevelFlag,
6464
common.GenericGithubOrgFlag,
65+
common.GenericDraftPullRequestFlag,
6566
common.GenericDryRunFlag,
6667
common.GenericSkipPullRequestFlag,
6768
common.GenericSkipArchivedReposFlag,

printer/printer.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ func PrintRepoReport(allEvents []types.AnnotatedEvent, runReport *types.RunRepor
8282
pullRequests = append(pullRequests, pr)
8383
}
8484

85+
var draftPullRequests []types.PullRequest
86+
87+
for repoName, prURL := range runReport.DraftPullRequests {
88+
pr := types.PullRequest{
89+
Repo: repoName,
90+
URL: prURL,
91+
}
92+
draftPullRequests = append(draftPullRequests, pr)
93+
}
94+
8595
if len(pullRequests) > 0 {
8696
fmt.Println()
8797
fmt.Println("*****************************************************")
@@ -93,4 +103,16 @@ func PrintRepoReport(allEvents []types.AnnotatedEvent, runReport *types.RunRepor
93103
fmt.Println()
94104

95105
}
106+
107+
if len(draftPullRequests) > 0 {
108+
fmt.Println()
109+
fmt.Println("*****************************************************")
110+
fmt.Println(" DRAFT PULL REQUESTS OPENED")
111+
fmt.Println("*****************************************************")
112+
pullRequestPrinter := tableprinter.New(os.Stdout)
113+
configurePrinterStyling(pullRequestPrinter)
114+
pullRequestPrinter.Print(draftPullRequests)
115+
fmt.Println()
116+
117+
}
96118
}

repository/repo-operations.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -442,30 +442,48 @@ func openPullRequest(config *config.GitXargsConfig, repo *github.Repository, bra
442442
Base: github.String(repoDefaultBranch),
443443
Body: github.String(descriptionToUse),
444444
MaintainerCanModify: github.Bool(true),
445+
Draft: github.Bool(config.Draft),
445446
}
446447

447-
// Make a pull request via the GitHub API
448-
pr, _, err := config.GithubClient.PullRequests.Create(context.Background(), *repo.GetOwner().Login, repo.GetName(), newPR)
448+
// Make a pull request via the Github API
449+
pr, resp, err := config.GithubClient.PullRequests.Create(context.Background(), *repo.GetOwner().Login, repo.GetName(), newPR)
450+
451+
prErrorMessage := "Error opening pull request"
452+
prDraftModeNotSupported := false
449453

450454
if err != nil {
455+
if resp.StatusCode == 422 {
456+
// Update the error to be more RepoDoesntSupportDraftPullRequestsErra Draft PR
457+
prErrorMessage = "Error opening pull request: draft PRs not supported for this repo. See https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests#draft-pull-requests"
458+
prDraftModeNotSupported = true
459+
}
460+
451461
logger.WithFields(logrus.Fields{
452462
"Error": err,
453463
"Head": branch,
454464
"Base": repoDefaultBranch,
455465
"Body": descriptionToUse,
456-
}).Debug("Error opening Pull request")
466+
}).Debug(prErrorMessage)
457467

458468
// Track pull request open failure
459-
config.Stats.TrackSingle(stats.PullRequestOpenErr, repo)
469+
if prDraftModeNotSupported {
470+
config.Stats.TrackSingle(stats.RepoDoesntSupportDraftPullRequestsErr, repo)
471+
} else {
472+
config.Stats.TrackSingle(stats.PullRequestOpenErr, repo)
473+
}
460474
return errors.WithStackTrace(err)
461475
}
462476

463477
logger.WithFields(logrus.Fields{
464478
"Pull Request URL": pr.GetHTMLURL(),
465479
}).Debug("Successfully opened pull request")
466480

467-
// Track successful opening of the pull request, extracting the HTML url to the PR itself for easier review
468-
config.Stats.TrackPullRequest(repo.GetName(), pr.GetHTMLURL())
481+
if config.Draft {
482+
config.Stats.TrackDraftPullRequest(repo.GetName(), pr.GetHTMLURL())
483+
} else {
484+
// Track successful opening of the pull request, extracting the HTML url to the PR itself for easier review
485+
config.Stats.TrackPullRequest(repo.GetName(), pr.GetHTMLURL())
486+
}
469487
return nil
470488
}
471489

stats/stats.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ const (
6868
BranchRemoteDidntExistYet types.Event = "branch-remote-didnt-exist-yet"
6969
// RepoFlagSuppliedRepoMalformed denotes a repo passed via the --repo flag that was malformed (perhaps missing it's Github org prefix) and therefore unprocessable
7070
RepoFlagSuppliedRepoMalformed types.Event = "repo-flag-supplied-repo-malformed"
71+
// RepoDoesntSupportDraftPullRequestsErr denotes a repo that is incompatible with the submitted pull request configuration
72+
RepoDoesntSupportDraftPullRequestsErr types.Event = "repo-not-compatible-with-pull-config"
7173
)
7274

7375
var allEvents = []types.AnnotatedEvent{
@@ -97,6 +99,7 @@ var allEvents = []types.AnnotatedEvent{
9799
{Event: BranchRemotePullFailed, Description: "Repos whose remote branches could not be successfully pulled"},
98100
{Event: BranchRemoteDidntExistYet, Description: "Repos whose specified branches did not exist on the remote, and so were first created locally"},
99101
{Event: RepoFlagSuppliedRepoMalformed, Description: "Repos passed via the --repo flag that were malformed (missing their Github org prefix?) and therefore unprocessable"},
102+
{Event: RepoDoesntSupportDraftPullRequestsErr, Description: "Repos that do not support Draft PRs (--draft flag was passed)"},
100103
}
101104

102105
// RunStats will be a stats-tracker class that keeps score of which repos were touched, which were considered for update, which had branches made, PRs made, which were missing workflows or contexts, or had out of date workflows syntax values, etc
@@ -105,6 +108,7 @@ type RunStats struct {
105108
repos map[types.Event][]*github.Repository
106109
skippedArchivedRepos map[types.Event][]*github.Repository
107110
pulls map[string]string
111+
draftpulls map[string]string
108112
command []string
109113
fileProvidedRepos []*types.AllowedRepo
110114
repoFlagProvidedRepos []*types.AllowedRepo
@@ -122,6 +126,7 @@ func NewStatsTracker() *RunStats {
122126
repos: make(map[types.Event][]*github.Repository),
123127
skippedArchivedRepos: make(map[types.Event][]*github.Repository),
124128
pulls: make(map[string]string),
129+
draftpulls: make(map[string]string),
125130
command: []string{},
126131
fileProvidedRepos: fileProvidedRepos,
127132
repoFlagProvidedRepos: repoFlagProvidedRepos,
@@ -163,6 +168,11 @@ func (r *RunStats) GetPullRequests() map[string]string {
163168
return r.pulls
164169
}
165170

171+
// GetDraftPullRequests returns the inner representation of the draft pull requests that were opened during the lifecycle of a given run
172+
func (r *RunStats) GetDraftPullRequests() map[string]string {
173+
return r.draftpulls
174+
}
175+
166176
// SetFileProvidedRepos sets the number of repos that were provided via file by the user on startup (as opposed to looked up via GitHub API via the --github-org flag)
167177
func (r *RunStats) SetFileProvidedRepos(fileProvidedRepos []*types.AllowedRepo) {
168178
for _, ar := range fileProvidedRepos {
@@ -218,12 +228,22 @@ func TrackEventIfMissing(slice []*github.Repository, repo *github.Repository) []
218228
return append(slice, repo)
219229
}
220230

231+
// TrackPullRequest stores the successful PR opening for the supplied Repo, at the supplied PR URL
232+
// This function is safe to call from concurrent goroutines
221233
func (r *RunStats) TrackPullRequest(repoName, prURL string) {
222234
defer r.mutex.Unlock()
223235
r.mutex.Lock()
224236
r.pulls[repoName] = prURL
225237
}
226238

239+
// TrackDraftPullRequest stores the successful Draft PR opening for the supplied Repo, at the supplied PR URL
240+
// This function is safe to call from concurrent goroutines
241+
func (r *RunStats) TrackDraftPullRequest(repoName, prURL string) {
242+
defer r.mutex.Unlock()
243+
r.mutex.Lock()
244+
r.draftpulls[repoName] = prURL
245+
}
246+
227247
// TrackMultiple accepts a types.Event and a slice of pointers to GitHub repos that will all be associated with that event
228248
func (r *RunStats) TrackMultiple(event types.Event, repos []*github.Repository) {
229249
for _, repo := range repos {
@@ -239,7 +259,8 @@ func (r *RunStats) GenerateRunReport() *types.RunReport {
239259
Command: r.command,
240260
SelectionMode: r.selectionMode,
241261
RuntimeSeconds: r.GetTotalRunSeconds(), FileProvidedRepos: r.GetFileProvidedRepos(),
242-
PullRequests: r.GetPullRequests(),
262+
PullRequests: r.GetPullRequests(),
263+
DraftPullRequests: r.GetDraftPullRequests(),
243264
}
244265
}
245266

types/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type RunReport struct {
2323
RuntimeSeconds int
2424
FileProvidedRepos []*AllowedRepo
2525
PullRequests map[string]string
26+
DraftPullRequests map[string]string
2627
}
2728

2829
// AnnotatedEvent is used in printing the final report. It contains the info to print a section's table - both its Event for looking up the tagged repos, and the human-legible description for printing above the table

0 commit comments

Comments
 (0)