Skip to content

Commit 6c231e7

Browse files
authored
chore(devtools): QOL improvements for cherry-pick script (#6620)
1 parent bac751d commit 6c231e7

File tree

2 files changed

+149
-47
lines changed

2 files changed

+149
-47
lines changed

tools/ods/cmd/cherry-pick.go

Lines changed: 146 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"bufio"
45
"fmt"
56
"os"
67
"os/exec"
@@ -14,39 +15,55 @@ import (
1415
// CherryPickOptions holds options for the cherry-pick command
1516
type CherryPickOptions struct {
1617
Releases []string
18+
DryRun bool
19+
Yes bool
1720
}
1821

1922
// NewCherryPickCommand creates a new cherry-pick command
2023
func NewCherryPickCommand() *cobra.Command {
2124
opts := &CherryPickOptions{}
2225

2326
cmd := &cobra.Command{
24-
Use: "cherry-pick <commit-sha>",
25-
Short: "Cherry-pick a commit to a release branch",
26-
Long: `Cherry-pick a commit to a release branch and create a PR.
27+
Use: "cherry-pick <commit-sha> [<commit-sha>...]",
28+
Short: "Cherry-pick one or more commits to a release branch",
29+
Long: `Cherry-pick one or more commits to a release branch and create a PR.
2730
2831
This command will:
29-
1. Find the nearest stable version tag (v*.*.* if --release not specified)
30-
2. Fetch the corresponding release branch (release/vMAJOR.MINOR)
31-
3. Create a hotfix branch with the cherry-picked commit
32+
1. Find the nearest stable version tag
33+
2. Fetch the corresponding release branch(es)
34+
3. Create a hotfix branch with the cherry-picked commit(s)
3235
4. Push and create a PR using the GitHub CLI
3336
5. Switch back to the original branch
3437
35-
The --release flag can be specified multiple times to cherry-pick to multiple release branches.`,
36-
Args: cobra.ExactArgs(1),
38+
Multiple commits will be cherry-picked in the order specified, similar to git cherry-pick.
39+
The --release flag can be specified multiple times to cherry-pick to multiple release branches.
40+
Example usage:
41+
42+
$ ods cherry-pick foo123 bar456 --release 2.5 --release 2.6`,
43+
Args: cobra.MinimumNArgs(1),
3744
Run: func(cmd *cobra.Command, args []string) {
3845
runCherryPick(cmd, args, opts)
3946
},
4047
}
4148

4249
cmd.Flags().StringSliceVar(&opts.Releases, "release", []string{}, "Release version(s) to cherry-pick to (e.g., 1.0, v1.1). 'v' prefix is optional. Can be specified multiple times.")
50+
cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false, "Perform all local operations but skip pushing to remote and creating PRs")
51+
cmd.Flags().BoolVar(&opts.Yes, "yes", false, "Skip confirmation prompts and automatically proceed")
4352

4453
return cmd
4554
}
4655

4756
func runCherryPick(cmd *cobra.Command, args []string, opts *CherryPickOptions) {
48-
commitSHA := args[0]
49-
log.Debugf("Cherry-picking commit: %s", commitSHA)
57+
commitSHAs := args
58+
if len(commitSHAs) == 1 {
59+
log.Debugf("Cherry-picking commit: %s", commitSHAs[0])
60+
} else {
61+
log.Debugf("Cherry-picking %d commits: %s", len(commitSHAs), strings.Join(commitSHAs, ", "))
62+
}
63+
64+
if opts.DryRun {
65+
log.Warning("=== DRY RUN MODE: No remote operations will be performed ===")
66+
}
5067

5168
// Save the current branch to switch back later
5269
originalBranch, err := getCurrentBranch()
@@ -55,10 +72,25 @@ func runCherryPick(cmd *cobra.Command, args []string, opts *CherryPickOptions) {
5572
}
5673
log.Debugf("Original branch: %s", originalBranch)
5774

58-
// Get the short SHA for branch naming
59-
shortSHA := commitSHA
60-
if len(shortSHA) > 8 {
61-
shortSHA = shortSHA[:8]
75+
// Get the short SHA(s) for branch naming
76+
var branchSuffix string
77+
if len(commitSHAs) == 1 {
78+
shortSHA := commitSHAs[0]
79+
if len(shortSHA) > 8 {
80+
shortSHA = shortSHA[:8]
81+
}
82+
branchSuffix = shortSHA
83+
} else {
84+
// For multiple commits, use first-last notation
85+
firstSHA := commitSHAs[0]
86+
lastSHA := commitSHAs[len(commitSHAs)-1]
87+
if len(firstSHA) > 8 {
88+
firstSHA = firstSHA[:8]
89+
}
90+
if len(lastSHA) > 8 {
91+
lastSHA = lastSHA[:8]
92+
}
93+
branchSuffix = fmt.Sprintf("%s-%s", firstSHA, lastSHA)
6294
}
6395

6496
// Determine which releases to target
@@ -68,84 +100,121 @@ func runCherryPick(cmd *cobra.Command, args []string, opts *CherryPickOptions) {
68100
for _, rel := range opts.Releases {
69101
releases = append(releases, normalizeVersion(rel))
70102
}
71-
log.Infof("Using specified release versions: %v", releases)
103+
log.Debugf("Using specified release versions: %v", releases)
72104
} else {
73-
// Find the nearest stable tag
74-
version, err := findNearestStableTag(commitSHA)
105+
// Find the nearest stable tag using the first commit
106+
version, err := findNearestStableTag(commitSHAs[0])
75107
if err != nil {
76108
log.Fatalf("Failed to find nearest stable tag: %v", err)
77109
}
110+
111+
// Prompt user for confirmation
112+
if !opts.Yes {
113+
if !promptYesNo(fmt.Sprintf("Auto-detected release version: %s. Continue? (yes/no): ", version)) {
114+
log.Info("If you want to cherry-pick to a different release, use the --release flag. Exiting...")
115+
return
116+
}
117+
} else {
118+
log.Infof("Auto-detected release version: %s", version)
119+
}
120+
78121
releases = []string{version}
79-
log.Infof("Auto-detected release version: %s", version)
80122
}
81123

82-
// Get commit message for PR title
83-
commitMsg, err := getCommitMessage(commitSHA)
84-
if err != nil {
85-
log.Warnf("Failed to get commit message, using default title: %v", err)
86-
commitMsg = fmt.Sprintf("Hotfix: cherry-pick %s", shortSHA)
124+
// Get commit message(s) for PR title
125+
var prTitle string
126+
if len(commitSHAs) == 1 {
127+
commitMsg, err := getCommitMessage(commitSHAs[0])
128+
if err != nil {
129+
log.Warnf("Failed to get commit message, using default title: %v", err)
130+
shortSHA := commitSHAs[0]
131+
if len(shortSHA) > 8 {
132+
shortSHA = shortSHA[:8]
133+
}
134+
prTitle = fmt.Sprintf("chore(hotfix): cherry-pick %s", shortSHA)
135+
} else {
136+
prTitle = commitMsg
137+
}
138+
} else {
139+
// For multiple commits, use a generic title
140+
prTitle = fmt.Sprintf("chore(hotfix): cherry-pick %d commits", len(commitSHAs))
87141
}
88142

89143
// Process each release
90144
prURLs := []string{}
91145
for _, release := range releases {
92-
log.Infof("\n--- Processing release %s ---", release)
93-
prURL, err := cherryPickToRelease(commitSHA, shortSHA, release, commitMsg)
146+
log.Infof("Processing release %s", release)
147+
prTitleWithRelease := fmt.Sprintf("%s to release %s", prTitle, release)
148+
prURL, err := cherryPickToRelease(commitSHAs, branchSuffix, release, prTitleWithRelease, opts.DryRun)
94149
if err != nil {
95150
// Switch back to original branch before exiting on error
96-
if checkoutErr := runGitCommand("checkout", originalBranch); checkoutErr != nil {
97-
log.Warnf("Failed to switch back to original branch: %v", checkoutErr)
151+
if switchErr := runGitCommand("switch", "--quiet", originalBranch); switchErr != nil {
152+
log.Warnf("Failed to switch back to original branch: %v", switchErr)
98153
}
99154
log.Fatalf("Failed to cherry-pick to release %s: %v", release, err)
100155
}
101-
prURLs = append(prURLs, prURL)
156+
if prURL != "" {
157+
prURLs = append(prURLs, prURL)
158+
}
102159
}
103160

104161
// Switch back to the original branch
105-
log.Infof("\nSwitching back to original branch: %s", originalBranch)
106-
if err := runGitCommand("checkout", originalBranch); err != nil {
162+
log.Infof("Switching back to original branch: %s", originalBranch)
163+
if err := runGitCommand("switch", "--quiet", originalBranch); err != nil {
107164
log.Warnf("Failed to switch back to original branch: %v", err)
108165
}
109166

110167
// Print all PR URLs
111-
log.Info("\n=== Summary ===")
112168
for i, prURL := range prURLs {
113169
log.Infof("PR %d: %s", i+1, prURL)
114170
}
115171
}
116172

117-
// cherryPickToRelease cherry-picks a commit to a specific release branch
118-
func cherryPickToRelease(commitSHA, shortSHA, version, commitMsg string) (string, error) {
173+
// cherryPickToRelease cherry-picks one or more commits to a specific release branch
174+
func cherryPickToRelease(commitSHAs []string, branchSuffix, version, prTitle string, dryRun bool) (string, error) {
119175
releaseBranch := fmt.Sprintf("release/%s", version)
120-
hotfixBranch := fmt.Sprintf("hotfix/%s-%s", shortSHA, version)
176+
hotfixBranch := fmt.Sprintf("hotfix/%s-%s", branchSuffix, version)
121177

122178
// Fetch the release branch
123179
log.Infof("Fetching release branch: %s", releaseBranch)
124-
if err := runGitCommand("fetch", "origin", releaseBranch); err != nil {
180+
if err := runGitCommand("fetch", "--prune", "--quiet", "origin", releaseBranch); err != nil {
125181
return "", fmt.Errorf("failed to fetch release branch %s: %w", releaseBranch, err)
126182
}
127183

128184
// Create the hotfix branch from the release branch
129185
log.Infof("Creating hotfix branch: %s", hotfixBranch)
130-
if err := runGitCommand("checkout", "-b", hotfixBranch, fmt.Sprintf("origin/%s", releaseBranch)); err != nil {
186+
if err := runGitCommand("checkout", "--quiet", "-b", hotfixBranch, fmt.Sprintf("origin/%s", releaseBranch)); err != nil {
131187
return "", fmt.Errorf("failed to create hotfix branch: %w", err)
132188
}
133189

134-
// Cherry-pick the commit
135-
log.Infof("Cherry-picking commit: %s", commitSHA)
136-
if err := runGitCommand("cherry-pick", commitSHA); err != nil {
137-
return "", fmt.Errorf("failed to cherry-pick commit: %w", err)
190+
// Cherry-pick the commits
191+
if len(commitSHAs) == 1 {
192+
log.Infof("Cherry-picking commit: %s", commitSHAs[0])
193+
} else {
194+
log.Infof("Cherry-picking %d commits: %s", len(commitSHAs), strings.Join(commitSHAs, " "))
195+
}
196+
197+
// Build git cherry-pick command with all commits
198+
cherryPickArgs := append([]string{"cherry-pick"}, commitSHAs...)
199+
if err := runGitCommand(cherryPickArgs...); err != nil {
200+
return "", fmt.Errorf("failed to cherry-pick commits: %w", err)
201+
}
202+
203+
if dryRun {
204+
log.Warnf("[DRY RUN] Would push hotfix branch: %s", hotfixBranch)
205+
log.Warnf("[DRY RUN] Would create PR from %s to %s", hotfixBranch, releaseBranch)
206+
return "", nil
138207
}
139208

140209
// Push the hotfix branch
141210
log.Infof("Pushing hotfix branch: %s", hotfixBranch)
142-
if err := runGitCommand("push", "-u", "origin", hotfixBranch); err != nil {
211+
if err := runGitCommand("push", "--quiet", "-u", "origin", hotfixBranch); err != nil {
143212
return "", fmt.Errorf("failed to push hotfix branch: %w", err)
144213
}
145214

146215
// Create PR using GitHub CLI
147216
log.Info("Creating PR...")
148-
prURL, err := createPR(hotfixBranch, releaseBranch, commitMsg, commitSHA)
217+
prURL, err := createPR(hotfixBranch, releaseBranch, prTitle, commitSHAs)
149218
if err != nil {
150219
return "", fmt.Errorf("failed to create PR: %w", err)
151220
}
@@ -154,12 +223,32 @@ func cherryPickToRelease(commitSHA, shortSHA, version, commitMsg string) (string
154223
return prURL, nil
155224
}
156225

226+
// promptYesNo prompts the user with a yes/no question and returns true for yes, false for no
227+
func promptYesNo(prompt string) bool {
228+
reader := bufio.NewReader(os.Stdin)
229+
for {
230+
fmt.Print(prompt)
231+
response, err := reader.ReadString('\n')
232+
if err != nil {
233+
log.Fatalf("Failed to read input: %v", err)
234+
}
235+
response = strings.TrimSpace(strings.ToLower(response))
236+
if response == "yes" || response == "y" || response == "" {
237+
return true
238+
}
239+
if response == "no" || response == "n" {
240+
return false
241+
}
242+
fmt.Println("Please enter 'yes' or 'no'")
243+
}
244+
}
245+
157246
// getCurrentBranch returns the name of the current git branch
158247
func getCurrentBranch() (string, error) {
159-
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
248+
cmd := exec.Command("git", "branch", "--show-current")
160249
output, err := cmd.Output()
161250
if err != nil {
162-
return "", fmt.Errorf("git rev-parse failed: %w", err)
251+
return "", fmt.Errorf("git branch failed: %w", err)
163252
}
164253
return strings.TrimSpace(string(output)), nil
165254
}
@@ -198,7 +287,9 @@ func findNearestStableTag(commitSHA string) (string, error) {
198287
func runGitCommand(args ...string) error {
199288
log.Debugf("Running: git %s", strings.Join(args, " "))
200289
cmd := exec.Command("git", args...)
201-
cmd.Stdout = os.Stdout
290+
if log.IsLevelEnabled(log.DebugLevel) {
291+
cmd.Stdout = os.Stdout
292+
}
202293
cmd.Stderr = os.Stderr
203294
return cmd.Run()
204295
}
@@ -214,8 +305,16 @@ func getCommitMessage(commitSHA string) (string, error) {
214305
}
215306

216307
// createPR creates a pull request using the GitHub CLI
217-
func createPR(headBranch, baseBranch, title, commitSHA string) (string, error) {
218-
body := fmt.Sprintf("Cherry-pick of commit %s to %s branch.", commitSHA, baseBranch)
308+
func createPR(headBranch, baseBranch, title string, commitSHAs []string) (string, error) {
309+
var body string
310+
if len(commitSHAs) == 1 {
311+
body = fmt.Sprintf("Cherry-pick of commit %s to %s branch.", commitSHAs[0], baseBranch)
312+
} else {
313+
body = fmt.Sprintf("Cherry-pick of %d commits to %s branch:\n\n", len(commitSHAs), baseBranch)
314+
for _, sha := range commitSHAs {
315+
body += fmt.Sprintf("- %s\n", sha)
316+
}
317+
}
219318

220319
cmd := exec.Command("gh", "pr", "create",
221320
"--base", baseBranch,

tools/ods/cmd/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ func NewRootCommand() *cobra.Command {
3131
} else {
3232
log.SetLevel(log.InfoLevel)
3333
}
34+
log.SetFormatter(&log.TextFormatter{
35+
DisableTimestamp: true,
36+
})
3437
},
3538
Version: fmt.Sprintf("%s\ncommit %s", Version, Commit),
3639
}

0 commit comments

Comments
 (0)