cherrypick-interactive 1.4.3 β 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -19
- package/cli.js +174 -45
- package/package.json +4 -6
- package/cli.backup.js +0 -193
package/README.md
CHANGED
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
## π§ Motivation
|
|
8
8
|
|
|
9
9
|
When you maintain long-lived branches like `dev` and `main`, keeping them in sync can get messy.
|
|
10
|
-
Sometimes you rebase, sometimes you cherry-pick, sometimes you merge release branches β and every time, it
|
|
10
|
+
Sometimes you rebase, sometimes you cherry-pick, sometimes you merge release branches β and every time, it's easy to lose track of which commits actually made it into production.
|
|
11
11
|
|
|
12
12
|
**This CLI solves that pain point:**
|
|
13
13
|
|
|
14
14
|
- It compares two branches (e.g. `origin/dev` vs `origin/main`)
|
|
15
15
|
- Lists commits in `dev` that are *not yet* in `main`
|
|
16
16
|
- Lets you choose which ones to cherry-pick interactively
|
|
17
|
+
- Handles merge conflicts with an interactive resolution wizard
|
|
18
|
+
- Preserves original commit messages perfectly (even with squashed commits)
|
|
17
19
|
- (Optionally) bumps your semantic version, creates a release branch, updates `package.json`, and opens a GitHub draft PR for review
|
|
18
20
|
|
|
19
21
|
No manual `git log` diffing. No risky merges. No guesswork.
|
|
@@ -25,6 +27,8 @@ No manual `git log` diffing. No risky merges. No guesswork.
|
|
|
25
27
|
- π Finds commits in `dev` not present in `main`
|
|
26
28
|
- ποΈ Lets you select which commits to cherry-pick (or pick all)
|
|
27
29
|
- πͺ Cherry-picks in the correct order (oldest β newest)
|
|
30
|
+
- βοΈ **Interactive conflict resolution wizard** with multiple strategies
|
|
31
|
+
- π― **Preserves exact commit messages** from squashed commits
|
|
28
32
|
- πͺ Detects **semantic version bump** (`major`, `minor`, `patch`) from conventional commits
|
|
29
33
|
- π§© Creates a `release/x.y.z` branch from `main`
|
|
30
34
|
- π§Ύ Generates a Markdown changelog from commits
|
|
@@ -48,7 +52,12 @@ npm install -g cherrypick-interactive
|
|
|
48
52
|
## π Quick Start
|
|
49
53
|
|
|
50
54
|
```bash
|
|
51
|
-
cherrypick-interactive
|
|
55
|
+
cherrypick-interactive \
|
|
56
|
+
--semantic-versioning \
|
|
57
|
+
--version-file ./package.json \
|
|
58
|
+
--create-release \
|
|
59
|
+
--push-release \
|
|
60
|
+
--draft-pr
|
|
52
61
|
```
|
|
53
62
|
|
|
54
63
|
β
This will:
|
|
@@ -57,7 +66,7 @@ cherrypick-interactive --semantic-versioning --version-file ./package.json
|
|
|
57
66
|
3. Let you select which to cherry-pick
|
|
58
67
|
4. Compute the next version from commit messages
|
|
59
68
|
5. Create `release/<next-version>` from `main`
|
|
60
|
-
6. Cherry-pick the selected commits
|
|
69
|
+
6. Cherry-pick the selected commits (with conflict resolution if needed)
|
|
61
70
|
7. Update your `package.json` version and commit it
|
|
62
71
|
8. Push the branch and open a **draft PR** on GitHub
|
|
63
72
|
|
|
@@ -71,7 +80,7 @@ cherrypick-interactive --semantic-versioning --version-file ./package.json
|
|
|
71
80
|
cherrypick-interactive
|
|
72
81
|
```
|
|
73
82
|
|
|
74
|
-
Lists commits in `origin/dev` that aren
|
|
83
|
+
Lists commits in `origin/dev` that aren't in `origin/main`, filtered by the last week.
|
|
75
84
|
|
|
76
85
|
### 2. Cherry-pick all missing commits automatically
|
|
77
86
|
|
|
@@ -85,6 +94,22 @@ cherrypick-interactive --all-yes
|
|
|
85
94
|
cherrypick-interactive --dry-run
|
|
86
95
|
```
|
|
87
96
|
|
|
97
|
+
### 4. Filter commits by pattern
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
cherrypick-interactive --ignore-commits "^chore\(deps\)|^ci:"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Excludes commits starting with `chore(deps)` or `ci:` from the selection list.
|
|
104
|
+
|
|
105
|
+
### 5. Ignore certain commits from semantic versioning
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
cherrypick-interactive --ignore-semver "bump|dependencies"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Treats commits containing "bump" or "dependencies" as chores (no version bump).
|
|
112
|
+
|
|
88
113
|
---
|
|
89
114
|
|
|
90
115
|
## βοΈ Options
|
|
@@ -97,13 +122,15 @@ cherrypick-interactive --dry-run
|
|
|
97
122
|
| `--no-fetch` | Skip `git fetch --prune` | `false` |
|
|
98
123
|
| `--all-yes` | Cherry-pick all missing commits without prompt | `false` |
|
|
99
124
|
| `--dry-run` | Show what would happen without applying changes | `false` |
|
|
100
|
-
| `--semantic-versioning` | Detect semantic version bump from commits | `
|
|
125
|
+
| `--semantic-versioning` | Detect semantic version bump from commits | `true` |
|
|
101
126
|
| `--current-version` | Current version (if not reading from file) | β |
|
|
102
|
-
| `--version-file` | Path to `package.json` (to read & update version) |
|
|
103
|
-
| `--create-release` | Create `release/x.y.z` branch from `main` | `
|
|
127
|
+
| `--version-file` | Path to `package.json` (to read & update version) | `./package.json` |
|
|
128
|
+
| `--create-release` | Create `release/x.y.z` branch from `main` | `true` |
|
|
104
129
|
| `--push-release` | Push release branch to origin | `true` |
|
|
105
130
|
| `--draft-pr` | Create the GitHub PR as a draft | `false` |
|
|
106
131
|
| `--version-commit-message` | Template for version bump commit | `chore(release): bump version to {{version}}` |
|
|
132
|
+
| `--ignore-semver` | Comma-separated regex patterns to ignore for semver | β |
|
|
133
|
+
| `--ignore-commits` | Comma-separated regex patterns to exclude commits | β |
|
|
107
134
|
|
|
108
135
|
---
|
|
109
136
|
|
|
@@ -117,6 +144,39 @@ The tool analyzes commit messages using **Conventional Commits**:
|
|
|
117
144
|
| `feat:` | `feat(ui): add dark mode` | **minor** |
|
|
118
145
|
| `fix:` / `perf:` | `fix(api): correct pagination offset` | **patch** |
|
|
119
146
|
|
|
147
|
+
Use `--ignore-semver` to treat certain commits as chores:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
cherrypick-interactive --ignore-semver "^chore\(deps\)|bump|merge"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## βοΈ Interactive Conflict Resolution
|
|
156
|
+
|
|
157
|
+
When cherry-picking encounters conflicts, the tool provides an **interactive wizard**:
|
|
158
|
+
|
|
159
|
+
### Conflict Resolution Options:
|
|
160
|
+
|
|
161
|
+
**Per-file resolution:**
|
|
162
|
+
- **Use ours** β Keep the current branch's version
|
|
163
|
+
- **Use theirs** β Accept the cherry-picked commit's version
|
|
164
|
+
- **Open in editor** β Manually resolve conflicts in your editor
|
|
165
|
+
- **Show diff** β View the conflicting changes
|
|
166
|
+
- **Mark resolved** β Stage the file as-is
|
|
167
|
+
|
|
168
|
+
**Bulk actions:**
|
|
169
|
+
- **Use ours for ALL** β Apply current branch's version to all conflicts
|
|
170
|
+
- **Use theirs for ALL** β Accept cherry-picked version for all conflicts
|
|
171
|
+
- **Stage ALL** β Mark all files as resolved
|
|
172
|
+
- **Launch mergetool** β Use Git's configured merge tool
|
|
173
|
+
|
|
174
|
+
### Key Features:
|
|
175
|
+
|
|
176
|
+
β
**Preserves original commit messages** β Even when resolving conflicts, the commit message from the original commit in `dev` is maintained exactly
|
|
177
|
+
β
**Handles squashed commits** β Works correctly with squashed commits that contain multiple changes
|
|
178
|
+
β
**Resume cherry-picking** β After resolving conflicts, automatically continues with remaining commits
|
|
179
|
+
|
|
120
180
|
---
|
|
121
181
|
|
|
122
182
|
## πͺ΅ Example Run
|
|
@@ -129,18 +189,47 @@ Comparing subjects since 1 week ago
|
|
|
129
189
|
Dev: origin/dev
|
|
130
190
|
Main: origin/main
|
|
131
191
|
|
|
132
|
-
Select commits to cherry-pick (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
192
|
+
β Select commits to cherry-pick (8 missing):
|
|
193
|
+
β #86c6105k9 - Add missing ATS configs in plists. (#1077)
|
|
194
|
+
β chore(deps): bump actions/checkout from 3 to 6 (#1079)
|
|
195
|
+
β #86c6q8y5r - Separete Corporate and Individual Registration (#1081)
|
|
196
|
+
β #86c5wbbuc - Refactor splash related store initializations (#1082)
|
|
197
|
+
β #86c6x2u20 - Remove deprecated reward center components (#1085)
|
|
198
|
+
β #0 - Upgrade ruby version in workflows. (#1087)
|
|
199
|
+
|
|
200
|
+
β· Semver ignored (pattern: /bump/i): (chore(deps): bump actions/checkout from 3 to 6 (#1079))
|
|
201
|
+
β· Semver ignored (pattern: /bump/i): (#86c5wbbuc - Refactor splash related store initializations (#1082))
|
|
136
202
|
|
|
137
203
|
Semantic Versioning
|
|
138
|
-
Current:
|
|
204
|
+
Current: 4.36.0 Detected bump: minor Next: 4.37.0
|
|
205
|
+
|
|
206
|
+
Creating release/4.37.0 from origin/main...
|
|
207
|
+
β Ready on release/4.37.0. Cherry-picking will apply here.
|
|
208
|
+
|
|
209
|
+
Cherry-picking 6 commit(s) onto main (oldest β newest)...
|
|
210
|
+
|
|
211
|
+
β cherry-picked (7ba30af) #86c6105k9 - Add missing ATS configs in plists. (#1077)
|
|
212
|
+
β cherry-picked (bbc70ed) chore(deps): bump actions/checkout from 3 to 6 (#1079)
|
|
213
|
+
β cherry-picked (287dbad) #86c6q8y5r - Separete Corporate and Individual Registration (#1081)
|
|
139
214
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
215
|
+
β Cherry-pick has conflicts on 8eb07cb78148866d769684730d154e5cbeb2f331 (8eb07cb).
|
|
216
|
+
Conflicted files:
|
|
217
|
+
- android/app/build.gradle
|
|
218
|
+
- ios/bilira_wallet.xcodeproj/project.pbxproj
|
|
219
|
+
|
|
220
|
+
β Choose how to proceed: Resolve conflicts now
|
|
221
|
+
β Select a file to resolve or a global action: Use theirs for ALL
|
|
222
|
+
|
|
223
|
+
No conflicted files reported by git.
|
|
224
|
+
β cherry-picked (8eb07cb) #86c5wbbuc - Refactor splash related store initializations (#1082)
|
|
225
|
+
β cherry-picked (3ae27ee) #86c6x2u20 - Remove deprecated reward center components (#1085)
|
|
226
|
+
β cherry-picked (66d0419) #0 - Upgrade ruby version in workflows. (#1087)
|
|
227
|
+
|
|
228
|
+
Summary β applied: 6, skipped: 0
|
|
229
|
+
|
|
230
|
+
Updating package.json version β 4.37.0 ...
|
|
231
|
+
β package.json updated and committed: chore(release): bump version to 4.37.0
|
|
232
|
+
β
Pull request created for release/4.37.0 β main
|
|
144
233
|
```
|
|
145
234
|
|
|
146
235
|
---
|
|
@@ -150,21 +239,68 @@ Creating release/1.21.0 from origin/main...
|
|
|
150
239
|
If your team:
|
|
151
240
|
- Rebases or cherry-picks from `dev` β `main`
|
|
152
241
|
- Uses temporary release branches
|
|
242
|
+
- Works with squashed commits
|
|
243
|
+
- Needs to handle merge conflicts gracefully
|
|
153
244
|
- Tracks semantic versions via commits
|
|
154
245
|
|
|
155
246
|
β¦this CLI saves time and reduces errors.
|
|
156
247
|
It automates a tedious, error-prone manual process into a single command that behaves like `yarn upgrade-interactive`, but for Git commits.
|
|
157
248
|
|
|
249
|
+
**Special features:**
|
|
250
|
+
- β
Preserves exact commit messages (critical for squashed commits)
|
|
251
|
+
- β
Interactive conflict resolution without leaving the terminal
|
|
252
|
+
- β
Smart pattern-based filtering for commits and version detection
|
|
253
|
+
- β
Automatic changelog generation
|
|
254
|
+
|
|
158
255
|
---
|
|
159
256
|
|
|
160
257
|
## π§° Requirements
|
|
161
258
|
|
|
162
259
|
- Node.js β₯ 18
|
|
163
|
-
-
|
|
260
|
+
- Git β₯ 2.0
|
|
261
|
+
- **GitHub CLI (`gh`)** β *Optional, only required if using `--push-release`*
|
|
262
|
+
- Install from: https://cli.github.com/
|
|
263
|
+
- The tool will check if `gh` is installed and offer to continue without it
|
|
164
264
|
- A clean working directory (no uncommitted changes)
|
|
165
265
|
|
|
166
266
|
---
|
|
167
267
|
|
|
268
|
+
## π― Best Practices
|
|
269
|
+
|
|
270
|
+
### 1. Use `--ignore-commits` to filter noise
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
cherrypick-interactive --ignore-commits "^ci:|^chore\(deps\):|Merge branch"
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Exclude CI updates, dependency bumps, and merge commits from selection.
|
|
277
|
+
|
|
278
|
+
### 2. Use `--ignore-semver` for version accuracy
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
cherrypick-interactive --ignore-semver "bump|dependencies|merge"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Prevent certain commits from affecting semantic version calculation.
|
|
285
|
+
|
|
286
|
+
### 3. Always use `--draft-pr` for review
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
cherrypick-interactive --draft-pr
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Creates draft PRs so your team can review before merging.
|
|
293
|
+
|
|
294
|
+
### 4. Test with `--dry-run` first
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
cherrypick-interactive --dry-run
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
See what would happen without making any changes.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
168
304
|
## π§Ύ License
|
|
169
305
|
|
|
170
306
|
**MIT** β free to use, modify, and distribute.
|
|
@@ -178,9 +314,44 @@ It automates a tedious, error-prone manual process into a single command that be
|
|
|
178
314
|
```bash
|
|
179
315
|
node cli.js --dry-run
|
|
180
316
|
```
|
|
181
|
-
3. Test edge cases before submitting PRs
|
|
317
|
+
3. Test edge cases before submitting PRs:
|
|
318
|
+
- Squashed commits with conflicts
|
|
319
|
+
- Empty cherry-picks
|
|
320
|
+
- Multiple conflict resolutions
|
|
182
321
|
4. Please follow Conventional Commits for your changes.
|
|
183
322
|
|
|
184
323
|
---
|
|
185
324
|
|
|
186
|
-
|
|
325
|
+
## π Troubleshooting
|
|
326
|
+
|
|
327
|
+
### "GitHub CLI (gh) is not installed"
|
|
328
|
+
The tool automatically checks for `gh` CLI when using `--push-release`. If not found, you'll be prompted to:
|
|
329
|
+
- Install it from https://cli.github.com/ and try again
|
|
330
|
+
- Or continue without creating a PR (the release branch will still be pushed)
|
|
331
|
+
|
|
332
|
+
You can also run without `--push-release` to skip PR creation entirely:
|
|
333
|
+
```bash
|
|
334
|
+
cherrypick-interactive --create-release --no-push-release
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### "Cherry-pick has conflicts"
|
|
338
|
+
Use the interactive wizard to resolve conflicts file-by-file or in bulk.
|
|
339
|
+
|
|
340
|
+
### "Commit message changed after conflict resolution"
|
|
341
|
+
This issue has been fixed! The tool now preserves the original commit message using `git commit -C <hash>`.
|
|
342
|
+
|
|
343
|
+
### "Version not detected correctly"
|
|
344
|
+
Use `--ignore-semver` to exclude commits that shouldn't affect versioning:
|
|
345
|
+
```bash
|
|
346
|
+
cherrypick-interactive --ignore-semver "bump|chore\(deps\)"
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### "Too many commits to review"
|
|
350
|
+
Use `--ignore-commits` to filter out noise, or adjust `--since` to a shorter time window:
|
|
351
|
+
```bash
|
|
352
|
+
cherrypick-interactive --since "3 days ago" --ignore-commits "^ci:|^docs:"
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
> Created to make release management simpler and safer for teams who value clean Git history, predictable deployments, and efficient conflict resolution.
|
package/cli.js
CHANGED
|
@@ -99,11 +99,16 @@ const argv = yargs(hideBin(process.argv))
|
|
|
99
99
|
default: 'chore(release): bump version to {{version}}',
|
|
100
100
|
describe: 'Commit message template for version bump. Use {{version}} placeholder.',
|
|
101
101
|
})
|
|
102
|
-
.option('semver
|
|
102
|
+
.option('ignore-semver', {
|
|
103
103
|
type: 'string',
|
|
104
104
|
describe:
|
|
105
105
|
'Comma-separated regex patterns. If a commit message matches any, it will be treated as a chore for semantic versioning.',
|
|
106
106
|
})
|
|
107
|
+
.option('ignore-commits', {
|
|
108
|
+
type: 'string',
|
|
109
|
+
describe:
|
|
110
|
+
'Comma-separated regex patterns. If a commit message matches any, it will be omitted from the commit list.',
|
|
111
|
+
})
|
|
107
112
|
.wrap(200)
|
|
108
113
|
.help()
|
|
109
114
|
.alias('h', 'help')
|
|
@@ -126,7 +131,7 @@ async function getSubjects(branch) {
|
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
async function getDevCommits(branch, since) {
|
|
129
|
-
const out = await gitRaw(['log', '--no-merges',
|
|
134
|
+
const out = await gitRaw(['log', '--no-merges', `--since=${since}`, '--pretty=%H %s', branch]);
|
|
130
135
|
|
|
131
136
|
if (!out) {
|
|
132
137
|
return [];
|
|
@@ -184,7 +189,7 @@ async function handleCherryPickConflict(hash) {
|
|
|
184
189
|
|
|
185
190
|
const { action } = await inquirer.prompt([
|
|
186
191
|
{
|
|
187
|
-
type: '
|
|
192
|
+
type: 'select',
|
|
188
193
|
name: 'action',
|
|
189
194
|
message: 'Choose how to proceed:',
|
|
190
195
|
choices: [
|
|
@@ -262,7 +267,7 @@ async function showConflictsList() {
|
|
|
262
267
|
}
|
|
263
268
|
err(chalk.yellow('Conflicted files:'));
|
|
264
269
|
for (const f of files) {
|
|
265
|
-
err(
|
|
270
|
+
err(` - ${f}`);
|
|
266
271
|
}
|
|
267
272
|
return files;
|
|
268
273
|
}
|
|
@@ -270,7 +275,7 @@ async function showConflictsList() {
|
|
|
270
275
|
async function resolveSingleFileWizard(file) {
|
|
271
276
|
const { action } = await inquirer.prompt([
|
|
272
277
|
{
|
|
273
|
-
type: '
|
|
278
|
+
type: 'select',
|
|
274
279
|
name: 'action',
|
|
275
280
|
message: `How to resolve "${file}"?`,
|
|
276
281
|
choices: [
|
|
@@ -335,7 +340,7 @@ async function conflictsResolutionWizard(hash) {
|
|
|
335
340
|
err(chalk.yellow('The previous cherry-pick is now empty.'));
|
|
336
341
|
const { emptyAction } = await inquirer.prompt([
|
|
337
342
|
{
|
|
338
|
-
type: '
|
|
343
|
+
type: 'select',
|
|
339
344
|
name: 'emptyAction',
|
|
340
345
|
message: 'No staged changes for this pick. Choose next step:',
|
|
341
346
|
choices: [
|
|
@@ -352,8 +357,9 @@ async function conflictsResolutionWizard(hash) {
|
|
|
352
357
|
return 'skipped';
|
|
353
358
|
}
|
|
354
359
|
if (emptyAction === 'empty-commit') {
|
|
355
|
-
const
|
|
356
|
-
await gitRaw(['commit', '--allow-empty', '-m',
|
|
360
|
+
const fullMessage = await gitRaw(['show', '--format=%B', '-s', hash]);
|
|
361
|
+
await gitRaw(['commit', '--allow-empty', '-m', fullMessage]);
|
|
362
|
+
const subject = await gitRaw(['show', '--format=%s', '-s', 'HEAD']);
|
|
357
363
|
log(`${chalk.green('β')} (empty) cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
358
364
|
return 'continued';
|
|
359
365
|
}
|
|
@@ -364,8 +370,9 @@ async function conflictsResolutionWizard(hash) {
|
|
|
364
370
|
// (re-loop)
|
|
365
371
|
} else {
|
|
366
372
|
try {
|
|
367
|
-
|
|
368
|
-
|
|
373
|
+
// Use -C to copy the original commit message
|
|
374
|
+
await gitRaw(['commit', '-C', hash]);
|
|
375
|
+
const subject = await gitRaw(['show', '--format=%s', '-s', 'HEAD']);
|
|
369
376
|
log(`${chalk.green('β')} cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
370
377
|
return 'continued';
|
|
371
378
|
} catch (e) {
|
|
@@ -378,7 +385,7 @@ async function conflictsResolutionWizard(hash) {
|
|
|
378
385
|
|
|
379
386
|
const { choice } = await inquirer.prompt([
|
|
380
387
|
{
|
|
381
|
-
type: '
|
|
388
|
+
type: 'select',
|
|
382
389
|
name: 'choice',
|
|
383
390
|
message: 'Select a file to resolve or a global action:',
|
|
384
391
|
pageSize: Math.min(20, Math.max(8, files.length + 5)),
|
|
@@ -432,7 +439,7 @@ async function conflictsResolutionWizard(hash) {
|
|
|
432
439
|
err(chalk.yellow('No staged changes found for this cherry-pick.'));
|
|
433
440
|
const { emptyAction } = await inquirer.prompt([
|
|
434
441
|
{
|
|
435
|
-
type: '
|
|
442
|
+
type: 'select',
|
|
436
443
|
name: 'emptyAction',
|
|
437
444
|
message: 'This pick seems empty. Choose next step:',
|
|
438
445
|
choices: [
|
|
@@ -448,9 +455,11 @@ async function conflictsResolutionWizard(hash) {
|
|
|
448
455
|
log(chalk.yellow(`β· Skipped empty pick ${chalk.dim(`(${hash.slice(0, 7)})`)}`));
|
|
449
456
|
return 'skipped';
|
|
450
457
|
}
|
|
458
|
+
|
|
451
459
|
if (emptyAction === 'empty-commit') {
|
|
452
|
-
const
|
|
453
|
-
await gitRaw(['commit', '--allow-empty', '-m',
|
|
460
|
+
const fullMessage = await gitRaw(['show', '--format=%B', '-s', hash]);
|
|
461
|
+
await gitRaw(['commit', '--allow-empty', '-m', fullMessage]);
|
|
462
|
+
const subject = await gitRaw(['show', '--format=%s', '-s', 'HEAD']);
|
|
454
463
|
log(`${chalk.green('β')} (empty) cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
455
464
|
return 'continued';
|
|
456
465
|
}
|
|
@@ -461,8 +470,9 @@ async function conflictsResolutionWizard(hash) {
|
|
|
461
470
|
}
|
|
462
471
|
|
|
463
472
|
try {
|
|
464
|
-
|
|
465
|
-
|
|
473
|
+
// Use -C to copy the original commit message
|
|
474
|
+
await gitRaw(['commit', '-C', hash]);
|
|
475
|
+
const subject = await gitRaw(['show', '--format=%s', '-s', 'HEAD']);
|
|
466
476
|
log(`${chalk.green('β')} cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
467
477
|
return 'continued';
|
|
468
478
|
} catch (e) {
|
|
@@ -585,21 +595,16 @@ async function computeSemanticBumpForCommits(hashes, gitRawFn, semverignore) {
|
|
|
585
595
|
}
|
|
586
596
|
|
|
587
597
|
const levels = [];
|
|
598
|
+
const semverIgnorePatterns = parseSemverIgnore(semverignore);
|
|
588
599
|
|
|
589
600
|
for (const h of hashes) {
|
|
590
601
|
const msg = await gitRawFn(['show', '--format=%B', '-s', h]);
|
|
591
602
|
const subject = msg.split(/\r?\n/)[0].trim();
|
|
592
603
|
let level = classifySingleCommit(msg);
|
|
593
604
|
|
|
594
|
-
// πΉ Apply --semverignore
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (semverIgnorePatterns.length > 0) {
|
|
598
|
-
const matched = matchesAnyPattern(subject, semverIgnorePatterns);
|
|
599
|
-
if (matched) {
|
|
600
|
-
// Treat as "chore" (no version bump influence)
|
|
601
|
-
level = null;
|
|
602
|
-
}
|
|
605
|
+
// πΉ Apply --semverignore (treat matched commits as chores). Use full message (subject + body).
|
|
606
|
+
if (semverIgnorePatterns.length > 0 && matchesAnyPattern(msg, semverIgnorePatterns)) {
|
|
607
|
+
level = null;
|
|
603
608
|
}
|
|
604
609
|
|
|
605
610
|
if (level) {
|
|
@@ -613,6 +618,34 @@ async function computeSemanticBumpForCommits(hashes, gitRawFn, semverignore) {
|
|
|
613
618
|
|
|
614
619
|
async function main() {
|
|
615
620
|
try {
|
|
621
|
+
// Check if gh CLI is installed when push-release is enabled
|
|
622
|
+
if (argv['push-release']) {
|
|
623
|
+
const ghInstalled = await checkGhCli();
|
|
624
|
+
if (!ghInstalled) {
|
|
625
|
+
err(chalk.yellow('\nβ οΈ GitHub CLI (gh) is not installed or not in PATH.'));
|
|
626
|
+
err(chalk.gray(' The --push-release flag requires gh CLI to create pull requests.\n'));
|
|
627
|
+
err(chalk.cyan(' Install it from: https://cli.github.com/'));
|
|
628
|
+
err(chalk.cyan(' Or run without --push-release to skip PR creation.\n'));
|
|
629
|
+
|
|
630
|
+
const { proceed } = await inquirer.prompt([
|
|
631
|
+
{
|
|
632
|
+
type: 'confirm',
|
|
633
|
+
name: 'proceed',
|
|
634
|
+
message: 'Continue without creating a PR?',
|
|
635
|
+
default: false,
|
|
636
|
+
},
|
|
637
|
+
]);
|
|
638
|
+
|
|
639
|
+
if (!proceed) {
|
|
640
|
+
log(chalk.yellow('Aborted by user.'));
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Disable push-release if user chooses to continue
|
|
645
|
+
argv['push-release'] = false;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
616
649
|
if (!argv['no-fetch']) {
|
|
617
650
|
log(chalk.gray('Fetching remotes (git fetch --prune)...'));
|
|
618
651
|
await git.fetch(['--prune']);
|
|
@@ -628,18 +661,26 @@ async function main() {
|
|
|
628
661
|
|
|
629
662
|
const missing = filterMissing(devCommits, mainSubjects);
|
|
630
663
|
|
|
631
|
-
|
|
664
|
+
// Filter out commits matching --ignore-commits patterns
|
|
665
|
+
const ignoreCommitsPatterns = parseIgnoreCommits(argv['ignore-commits']);
|
|
666
|
+
const filteredMissing =
|
|
667
|
+
ignoreCommitsPatterns.length > 0
|
|
668
|
+
? missing.filter(({ subject }) => !shouldIgnoreCommit(subject, ignoreCommitsPatterns))
|
|
669
|
+
: missing;
|
|
670
|
+
|
|
671
|
+
if (filteredMissing.length === 0) {
|
|
632
672
|
log(chalk.green('β
No missing commits found in the selected window.'));
|
|
633
673
|
return;
|
|
634
674
|
}
|
|
635
675
|
|
|
636
|
-
const
|
|
676
|
+
const semverIgnore = argv['ignore-semver'];
|
|
677
|
+
const indexByHash = new Map(filteredMissing.map((c, i) => [c.hash, i])); // 0=newest, larger=older
|
|
637
678
|
|
|
638
679
|
let selected;
|
|
639
680
|
if (argv['all-yes']) {
|
|
640
|
-
selected =
|
|
681
|
+
selected = filteredMissing.map((m) => m.hash);
|
|
641
682
|
} else {
|
|
642
|
-
selected = await selectCommitsInteractive(
|
|
683
|
+
selected = await selectCommitsInteractive(filteredMissing);
|
|
643
684
|
if (!selected.length) {
|
|
644
685
|
log(chalk.yellow('No commits selected. Exiting.'));
|
|
645
686
|
return;
|
|
@@ -669,7 +710,7 @@ async function main() {
|
|
|
669
710
|
}
|
|
670
711
|
|
|
671
712
|
// Bump is based on the commits you are about to apply (selected).
|
|
672
|
-
const bump = await computeSemanticBumpForCommits(bottomToTop, gitRaw,
|
|
713
|
+
const bump = await computeSemanticBumpForCommits(bottomToTop, gitRaw, semverIgnore);
|
|
673
714
|
|
|
674
715
|
computedNextVersion = bump ? incrementVersion(argv['current-version'], bump) : argv['current-version'];
|
|
675
716
|
|
|
@@ -691,14 +732,14 @@ async function main() {
|
|
|
691
732
|
}
|
|
692
733
|
|
|
693
734
|
const releaseBranch = `release/${computedNextVersion}`;
|
|
694
|
-
await ensureBranchDoesNotExistLocally(releaseBranch);
|
|
695
735
|
const startPoint = argv.main; // e.g., 'origin/main' or a local ref
|
|
736
|
+
await ensureReleaseBranchFresh(releaseBranch, startPoint);
|
|
696
737
|
|
|
697
738
|
const changelogBody = await buildChangelogBody({
|
|
698
739
|
version: computedNextVersion,
|
|
699
740
|
hashes: bottomToTop,
|
|
700
741
|
gitRawFn: gitRaw,
|
|
701
|
-
semverIgnore
|
|
742
|
+
semverIgnore, // raw flag value
|
|
702
743
|
});
|
|
703
744
|
|
|
704
745
|
await fsPromises.writeFile('RELEASE_CHANGELOG.md', changelogBody, 'utf8');
|
|
@@ -787,12 +828,53 @@ main();
|
|
|
787
828
|
* Utils
|
|
788
829
|
*/
|
|
789
830
|
|
|
790
|
-
async function
|
|
831
|
+
async function ensureReleaseBranchFresh(branchName, startPoint) {
|
|
791
832
|
const branches = await git.branchLocal();
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
833
|
+
const localExists = branches.all.includes(branchName);
|
|
834
|
+
const remoteRef = await gitRaw(['ls-remote', '--heads', 'origin', branchName]);
|
|
835
|
+
const remoteExists = Boolean(remoteRef);
|
|
836
|
+
|
|
837
|
+
if (!localExists && !remoteExists) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const { action } = await inquirer.prompt([
|
|
842
|
+
{
|
|
843
|
+
type: 'select',
|
|
844
|
+
name: 'action',
|
|
845
|
+
message: `Release branch "${branchName}" already exists${localExists ? ' locally' : ''}${
|
|
846
|
+
remoteExists ? ' on origin' : ''
|
|
847
|
+
}. How do you want to proceed? (override, abort)`,
|
|
848
|
+
choices: [
|
|
849
|
+
{ name: 'Override (delete existing branch and recreate)', value: 'override' },
|
|
850
|
+
{ name: 'Abort', value: 'abort' },
|
|
851
|
+
],
|
|
852
|
+
},
|
|
853
|
+
]);
|
|
854
|
+
|
|
855
|
+
if (action === 'abort') {
|
|
856
|
+
throw new Error(`Aborted: release branch "${branchName}" already exists.`);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Ensure we are not on the branch before deleting local copy
|
|
860
|
+
if (localExists) {
|
|
861
|
+
if (branches.current === branchName) {
|
|
862
|
+
const target = startPoint || 'HEAD';
|
|
863
|
+
log(chalk.gray(`Switching to ${target} before deleting ${branchName}...`));
|
|
864
|
+
await gitRaw(['checkout', target]);
|
|
865
|
+
}
|
|
866
|
+
await gitRaw(['branch', '-D', branchName]);
|
|
867
|
+
log(chalk.yellow(`β· Deleted existing local branch ${branchName}`));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (remoteExists) {
|
|
871
|
+
try {
|
|
872
|
+
await gitRaw(['push', 'origin', '--delete', branchName]);
|
|
873
|
+
log(chalk.yellow(`β· Deleted existing remote branch origin/${branchName}`));
|
|
874
|
+
} catch (e) {
|
|
875
|
+
err(chalk.red(`Failed to delete remote branch origin/${branchName}: ${e.message || e}`));
|
|
876
|
+
throw e;
|
|
877
|
+
}
|
|
796
878
|
}
|
|
797
879
|
}
|
|
798
880
|
|
|
@@ -815,8 +897,8 @@ async function buildChangelogBody({ version, hashes, gitRawFn, semverIgnore }) {
|
|
|
815
897
|
// normal classification first
|
|
816
898
|
let level = classifySingleCommit(msg);
|
|
817
899
|
|
|
818
|
-
// β¬ Apply semver
|
|
819
|
-
const matched = matchesAnyPattern(
|
|
900
|
+
// β¬ Apply ignore-semver logic
|
|
901
|
+
const matched = matchesAnyPattern(msg, semverIgnorePatterns); // evaluate against full message
|
|
820
902
|
if (matched) {
|
|
821
903
|
level = null; // drop it into "Other"
|
|
822
904
|
}
|
|
@@ -861,6 +943,14 @@ function stripOrigin(ref) {
|
|
|
861
943
|
return ref.startsWith('origin/') ? ref.slice('origin/'.length) : ref;
|
|
862
944
|
}
|
|
863
945
|
|
|
946
|
+
async function checkGhCli() {
|
|
947
|
+
return new Promise((resolve) => {
|
|
948
|
+
const p = spawn('gh', ['--version'], { stdio: 'pipe' });
|
|
949
|
+
p.on('error', () => resolve(false));
|
|
950
|
+
p.on('close', (code) => resolve(code === 0));
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
864
954
|
async function runGh(args) {
|
|
865
955
|
return new Promise((resolve, reject) => {
|
|
866
956
|
const p = spawn('gh', args, { stdio: 'inherit' });
|
|
@@ -874,14 +964,14 @@ async function readJson(filePath) {
|
|
|
874
964
|
}
|
|
875
965
|
|
|
876
966
|
async function writeJson(filePath, data) {
|
|
877
|
-
const text = JSON.stringify(data, null, 2)
|
|
967
|
+
const text = `${JSON.stringify(data, null, 2)}\n`;
|
|
878
968
|
await fsPromises.writeFile(filePath, text, 'utf8');
|
|
879
969
|
}
|
|
880
970
|
|
|
881
971
|
/** Read package.json version; throw if missing */
|
|
882
972
|
async function getPkgVersion(pkgPath) {
|
|
883
973
|
const pkg = await readJson(pkgPath);
|
|
884
|
-
const v = pkg
|
|
974
|
+
const v = pkg?.version;
|
|
885
975
|
if (!v) {
|
|
886
976
|
throw new Error(`No "version" field found in ${pkgPath}`);
|
|
887
977
|
}
|
|
@@ -900,16 +990,55 @@ function parseSemverIgnore(argvValue) {
|
|
|
900
990
|
return argvValue
|
|
901
991
|
.split(',')
|
|
902
992
|
.map((p) => p.trim())
|
|
993
|
+
.filter(Boolean)
|
|
994
|
+
.map((pattern) => {
|
|
995
|
+
try {
|
|
996
|
+
return new RegExp(pattern, 'i'); // case-insensitive on full message
|
|
997
|
+
} catch (e) {
|
|
998
|
+
err(chalk.red(`Invalid --ignore-semver pattern "${pattern}": ${e.message || e}`));
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
})
|
|
903
1002
|
.filter(Boolean);
|
|
904
1003
|
}
|
|
905
1004
|
|
|
906
1005
|
function matchesAnyPattern(text, patterns) {
|
|
907
1006
|
if (!patterns || patterns.length === 0) return false;
|
|
908
1007
|
|
|
909
|
-
const
|
|
1008
|
+
const matched = patterns.find((rx) => rx.test(text));
|
|
1009
|
+
if (matched) {
|
|
1010
|
+
log(chalk.cyan(`β· Semver ignored (pattern: /${matched.source}/i): ${chalk.dim(`(${text.split('\n')[0]})`)}`));
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function parseIgnoreCommits(argvValue) {
|
|
1018
|
+
if (!argvValue) return [];
|
|
1019
|
+
return argvValue
|
|
1020
|
+
.split(',')
|
|
1021
|
+
.map((p) => p.trim())
|
|
1022
|
+
.filter(Boolean)
|
|
1023
|
+
.map((pattern) => {
|
|
1024
|
+
try {
|
|
1025
|
+
return new RegExp(pattern, 'i'); // case-insensitive matching
|
|
1026
|
+
} catch (e) {
|
|
1027
|
+
err(chalk.red(`Invalid --ignore-commits pattern "${pattern}": ${e.message || e}`));
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
})
|
|
1031
|
+
.filter(Boolean);
|
|
1032
|
+
}
|
|
910
1033
|
|
|
911
|
-
|
|
912
|
-
|
|
1034
|
+
function shouldIgnoreCommit(subject, patterns) {
|
|
1035
|
+
if (!patterns || patterns.length === 0) return false;
|
|
1036
|
+
|
|
1037
|
+
const matched = patterns.find((rx) => rx.test(subject));
|
|
1038
|
+
if (matched) {
|
|
1039
|
+
log(chalk.gray(`β· Ignoring commit (pattern: /${matched.source}/i): ${chalk.dim(subject.slice(0, 80))}`));
|
|
1040
|
+
return true;
|
|
913
1041
|
}
|
|
914
|
-
|
|
1042
|
+
|
|
1043
|
+
return false;
|
|
915
1044
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cherrypick-interactive",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Interactively cherry-pick commits that are in dev but not in main, using subject-based comparison.",
|
|
5
5
|
"main": "cli.js",
|
|
6
|
-
"bin":
|
|
7
|
-
"cherrypick-interactive": "cli.js"
|
|
8
|
-
},
|
|
6
|
+
"bin": "cli.js",
|
|
9
7
|
"type": "module",
|
|
10
8
|
"scripts": {
|
|
11
9
|
"lint": "biome lint .",
|
|
@@ -19,9 +17,9 @@
|
|
|
19
17
|
},
|
|
20
18
|
"dependencies": {
|
|
21
19
|
"chalk": "^5.6.2",
|
|
22
|
-
"inquirer": "^
|
|
20
|
+
"inquirer": "^13.0.2",
|
|
23
21
|
"semver": "^7.7.3",
|
|
24
|
-
"simple-git": "^3.
|
|
22
|
+
"simple-git": "^3.30.0",
|
|
25
23
|
"update-notifier": "^7.3.1",
|
|
26
24
|
"yargs": "^18.0.0"
|
|
27
25
|
},
|
package/cli.backup.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import chalk from "chalk";
|
|
4
|
-
import inquirer from "inquirer";
|
|
5
|
-
import simpleGit from "simple-git";
|
|
6
|
-
import yargs from "yargs";
|
|
7
|
-
|
|
8
|
-
import { hideBin } from "yargs/helpers";
|
|
9
|
-
|
|
10
|
-
const git = simpleGit();
|
|
11
|
-
|
|
12
|
-
const argv = yargs(hideBin(process.argv))
|
|
13
|
-
.scriptName("cherrypick-interactive")
|
|
14
|
-
.usage("$0 [options]")
|
|
15
|
-
.option("dev", {
|
|
16
|
-
type: "string",
|
|
17
|
-
default: "origin/dev",
|
|
18
|
-
describe: "Source branch (contains commits you want).",
|
|
19
|
-
})
|
|
20
|
-
.option("main", {
|
|
21
|
-
type: "string",
|
|
22
|
-
default: "origin/main",
|
|
23
|
-
describe: "Comparison branch (commits present here will be filtered out).",
|
|
24
|
-
})
|
|
25
|
-
.option("since", {
|
|
26
|
-
type: "string",
|
|
27
|
-
default: "1 week ago",
|
|
28
|
-
describe: 'Time window passed to git --since (e.g. "2 weeks ago", "1 month ago").',
|
|
29
|
-
})
|
|
30
|
-
.option("no-fetch", {
|
|
31
|
-
type: "boolean",
|
|
32
|
-
default: false,
|
|
33
|
-
describe: "Skip 'git fetch --prune'.",
|
|
34
|
-
})
|
|
35
|
-
.option("all-yes", {
|
|
36
|
-
type: "boolean",
|
|
37
|
-
default: false,
|
|
38
|
-
describe: "Non-interactive: cherry-pick ALL missing commits (oldest β newest).",
|
|
39
|
-
})
|
|
40
|
-
.option("dry-run", {
|
|
41
|
-
type: "boolean",
|
|
42
|
-
default: false,
|
|
43
|
-
describe: "Print what would be cherry-picked and exit.",
|
|
44
|
-
})
|
|
45
|
-
.help()
|
|
46
|
-
.alias("h", "help")
|
|
47
|
-
.alias("v", "version").argv;
|
|
48
|
-
|
|
49
|
-
const log = (...a) => console.log(...a);
|
|
50
|
-
const err = (...a) => console.error(...a);
|
|
51
|
-
|
|
52
|
-
async function gitRaw(args) {
|
|
53
|
-
const out = await git.raw(args);
|
|
54
|
-
return out.trim();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function getSubjects(branch) {
|
|
58
|
-
const out = await gitRaw(["log", "--no-merges", "--pretty=%s", branch]);
|
|
59
|
-
if (!out) {
|
|
60
|
-
return new Set();
|
|
61
|
-
}
|
|
62
|
-
return new Set(out.split("\n").filter(Boolean));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function getDevCommits(branch, since) {
|
|
66
|
-
const out = await gitRaw(["log", "--no-merges", "--since=" + since, "--pretty=%H %s", branch]);
|
|
67
|
-
|
|
68
|
-
if (!out) {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
return out.split("\n").map((line) => {
|
|
72
|
-
const firstSpace = line.indexOf(" ");
|
|
73
|
-
const hash = line.slice(0, firstSpace);
|
|
74
|
-
const subject = line.slice(firstSpace + 1);
|
|
75
|
-
return { hash, subject };
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function filterMissing(devCommits, mainSubjects) {
|
|
80
|
-
return devCommits.filter(({ subject }) => !mainSubjects.has(subject));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function selectCommitsInteractive(missing) {
|
|
84
|
-
const choices = [
|
|
85
|
-
new inquirer.Separator(chalk.gray("ββ Newest commits ββ")),
|
|
86
|
-
...missing.map(({ hash, subject }, idx) => {
|
|
87
|
-
// display-only trim to avoid accidental leading spaces
|
|
88
|
-
const displaySubject = subject.replace(/^[\s\u00A0]+/, "");
|
|
89
|
-
return {
|
|
90
|
-
name: `${chalk.dim(`(${hash.slice(0, 7)})`)} ${displaySubject}`,
|
|
91
|
-
value: hash,
|
|
92
|
-
short: displaySubject,
|
|
93
|
-
idx, // we keep index for oldestβnewest ordering later
|
|
94
|
-
};
|
|
95
|
-
}),
|
|
96
|
-
new inquirer.Separator(chalk.gray("ββ Oldest commits ββ")),
|
|
97
|
-
];
|
|
98
|
-
|
|
99
|
-
const { selected } = await inquirer.prompt([
|
|
100
|
-
{
|
|
101
|
-
type: "checkbox",
|
|
102
|
-
name: "selected",
|
|
103
|
-
message: `Select commits to cherry-pick (${missing.length} missing):`,
|
|
104
|
-
choices,
|
|
105
|
-
pageSize: Math.min(20, Math.max(8, missing.length)),
|
|
106
|
-
},
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
return selected;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function cherryPickSequential(hashes) {
|
|
113
|
-
for (const hash of hashes) {
|
|
114
|
-
try {
|
|
115
|
-
await gitRaw(["cherry-pick", hash]);
|
|
116
|
-
const subject = await gitRaw(["show", "--format=%s", "-s", hash]);
|
|
117
|
-
log(`${chalk.green("β")} cherry-picked ${chalk.dim(`(${hash.slice(0, 7)})`)} ${subject}`);
|
|
118
|
-
} catch (e) {
|
|
119
|
-
err(chalk.red(`β Cherry-pick failed on ${hash}`));
|
|
120
|
-
err(chalk.yellow("Resolve conflicts, then run:"));
|
|
121
|
-
err(chalk.yellow(" git add -A && git cherry-pick --continue"));
|
|
122
|
-
err(chalk.yellow("Or abort:"));
|
|
123
|
-
err(chalk.yellow(" git cherry-pick --abort"));
|
|
124
|
-
throw e;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function main() {
|
|
130
|
-
try {
|
|
131
|
-
if (!argv["no-fetch"]) {
|
|
132
|
-
log(chalk.gray("Fetching remotes (git fetch --prune)..."));
|
|
133
|
-
await git.fetch(["--prune"]);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const currentBranch = (await gitRaw(["rev-parse", "--abbrev-ref", "HEAD"])) || "HEAD";
|
|
137
|
-
|
|
138
|
-
log(chalk.gray(`Comparing subjects since ${argv.since}`));
|
|
139
|
-
log(chalk.gray(`Dev: ${argv.dev}`));
|
|
140
|
-
log(chalk.gray(`Main: ${argv.main}`));
|
|
141
|
-
|
|
142
|
-
const [devCommits, mainSubjects] = await Promise.all([
|
|
143
|
-
getDevCommits(argv.dev, argv.since),
|
|
144
|
-
getSubjects(argv.main),
|
|
145
|
-
]);
|
|
146
|
-
|
|
147
|
-
const missing = filterMissing(devCommits, mainSubjects);
|
|
148
|
-
|
|
149
|
-
if (missing.length === 0) {
|
|
150
|
-
log(chalk.green("β
No missing commits found in the selected window."));
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Prepare bottomβtop ordering support
|
|
155
|
-
const indexByHash = new Map(missing.map((c, i) => [c.hash, i])); // 0=newest, larger=older
|
|
156
|
-
|
|
157
|
-
let selected;
|
|
158
|
-
if (argv.yes) {
|
|
159
|
-
selected = missing.map((m) => m.hash);
|
|
160
|
-
} else {
|
|
161
|
-
selected = await selectCommitsInteractive(missing);
|
|
162
|
-
if (!selected.length) {
|
|
163
|
-
log(chalk.yellow("No commits selected. Exiting."));
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Bottom β Top (oldest β newest)
|
|
169
|
-
const bottomToTop = [...selected].sort((a, b) => indexByHash.get(b) - indexByHash.get(a));
|
|
170
|
-
|
|
171
|
-
if (argv.dry_run || argv["dry-run"]) {
|
|
172
|
-
log(chalk.cyan("\n--dry-run: would cherry-pick (oldest β newest):"));
|
|
173
|
-
for (const h of bottomToTop) {
|
|
174
|
-
const subj = await gitRaw(["show", "--format=%s", "-s", h]);
|
|
175
|
-
log(`- ${chalk.dim(`(${h.slice(0, 7)})`)} ${subj}`);
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
log(
|
|
181
|
-
chalk.cyan(
|
|
182
|
-
`\nCherry-picking ${bottomToTop.length} commit(s) onto ${currentBranch} (oldest β newest)...\n`,
|
|
183
|
-
),
|
|
184
|
-
);
|
|
185
|
-
await cherryPickSequential(bottomToTop);
|
|
186
|
-
log(chalk.green(`\nβ
Done on ${currentBranch}`));
|
|
187
|
-
} catch (e) {
|
|
188
|
-
err(chalk.red(`\nβ Error: ${e.message || e}`));
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
main();
|