cherrypick-interactive 1.11.0 โ†’ 1.13.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 CHANGED
@@ -1,174 +1,288 @@
1
- # cherrypick-interactive
1
+ # ๐Ÿชถ cherrypick-interactive
2
2
 
3
- Cherry-pick missing commits from one branch to another โ€” interactively and safely.
3
+ ### Cherry-pick missing commits from `dev` to `main` โ€” interactively and safely.
4
4
 
5
- ## What it does
5
+ ---
6
6
 
7
- Compares two branches, shows what's missing, lets you pick which commits to move over, and handles the rest: conflicts, versioning, changelog, PR.
7
+ ## ๐Ÿšง Motivation
8
+
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's easy to lose track of which commits actually made it into production.
11
+
12
+ **This CLI solves that pain point:**
13
+
14
+ - It compares two branches (e.g. `origin/dev` vs `origin/main`)
15
+ - Lists commits in `dev` that are *not yet* in `main`
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)
19
+ - (Optionally) bumps your semantic version, creates a release branch, updates `package.json`, and opens a GitHub draft PR for review
20
+
21
+ No manual `git log` diffing. No risky merges. No guesswork.
22
+
23
+ ---
24
+
25
+ ## ๐Ÿงญ What it does
26
+
27
+ - ๐Ÿ” Finds commits in `dev` not present in `main`
28
+ - ๐Ÿ—‚๏ธ Lets you select which commits to cherry-pick (or pick all)
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
32
+ - ๐Ÿช„ Detects **semantic version bump** (`major`, `minor`, `patch`) from conventional commits
33
+ - ๐Ÿงฉ Creates a `release/x.y.z` branch from `main`
34
+ - ๐Ÿงพ Generates a Markdown changelog from commits
35
+ - ๐Ÿ”— Links ticket IDs to your issue tracker (ClickUp, Jira, Linear, or custom)
36
+ - ๐Ÿ–ฅ๏ธ Rich **TUI dashboard** with diff preview, search, and keyboard shortcuts
37
+ - ๐Ÿค– **CI mode** for fully non-interactive pipeline execution
38
+ - โ†ฉ๏ธ **Undo / rollback** with checkpoint-based session recovery
39
+ - ๐Ÿ“‹ **Changelog preview** before cherry-pick with confirmation gate
40
+ - โš ๏ธ **Dependency detection** warns when selected commits depend on unselected ones
41
+ - ๐Ÿ’พ **Profiles** to save and reuse CLI flag combinations
42
+ - ๐Ÿงฐ Optionally:
43
+ - updates `package.json` version
44
+ - commits and pushes it
45
+ - opens a **GitHub PR** (draft or normal)
46
+
47
+ ---
48
+
49
+ ## ๐Ÿ“ฆ Installation
8
50
 
9
51
  ```bash
10
- npx cherrypick-interactive
52
+ npm install -g cherrypick-interactive
11
53
  ```
12
54
 
13
- ## Install
55
+ (You can also run it directly without installing globally using `npx`.)
56
+
57
+ ---
58
+
59
+ ## ๐Ÿš€ Quick Start
14
60
 
15
61
  ```bash
16
- npm install -g cherrypick-interactive
62
+ cherrypick-interactive \
63
+ --semantic-versioning \
64
+ --version-file ./package.json \
65
+ --create-release \
66
+ --push-release \
67
+ --draft-pr
17
68
  ```
18
69
 
19
- ## Quick Start
70
+ โœ… This will:
71
+ 1. Fetch `origin/dev` and `origin/main`
72
+ 2. List commits in `dev` missing from `main`
73
+ 3. Let you select which to cherry-pick (TUI dashboard with diff preview)
74
+ 4. Detect potential dependencies between commits
75
+ 5. Show a changelog preview with version bump info
76
+ 6. Compute the next version from commit messages
77
+ 7. Create `release/<next-version>` from `main`
78
+ 8. Cherry-pick the selected commits (with conflict resolution if needed)
79
+ 9. Update your `package.json` version and commit it
80
+ 10. Push the branch and open a **draft PR** on GitHub
81
+
82
+ ---
83
+
84
+ ## ๐Ÿงฉ Common Use Cases
85
+
86
+ ### 1. Compare branches manually
20
87
 
21
88
  ```bash
22
- # Interactive selection with all defaults
23
89
  cherrypick-interactive
90
+ ```
24
91
 
25
- # Preview without applying
26
- cherrypick-interactive --dry-run
92
+ Lists commits in `origin/dev` that aren't in `origin/main`, filtered by the last week.
27
93
 
28
- # Pick everything, no prompts
94
+ ### 2. Cherry-pick all missing commits automatically
95
+
96
+ ```bash
29
97
  cherrypick-interactive --all-yes
30
98
  ```
31
99
 
32
- Full workflow with release branch and PR:
100
+ ### 3. Preview changes without applying them
33
101
 
34
102
  ```bash
35
- cherrypick-interactive \
36
- --semantic-versioning \
37
- --version-file ./package.json \
38
- --create-release \
39
- --push-release \
40
- --draft-pr
103
+ cherrypick-interactive --dry-run
41
104
  ```
42
105
 
43
- ## Features
44
-
45
- **Core:**
46
- - Finds commits in source branch not present in target branch
47
- - Interactive commit selection (TUI dashboard or simple checkbox)
48
- - Cherry-picks in correct order (oldest to newest)
49
- - Interactive conflict resolution wizard (per-file or bulk)
50
- - Preserves original commit messages
51
-
52
- **Versioning & Release:**
53
- - Detects semantic version bump from conventional commits
54
- - Creates `release/x.y.z` branch
55
- - Generates markdown changelog
56
- - Updates `package.json` version
57
- - Opens GitHub PR (draft or normal)
58
-
59
- **Profiles:**
60
- - Save and reuse CLI flag combinations
61
- - `--save-profile hotfix` saves current flags
62
- - `--profile hotfix` loads them back
63
- - `--list-profiles` shows available profiles
64
- - Stored in `.cherrypickrc.json`
65
-
66
- **Tracker Integration:**
67
- - Links ticket IDs in changelog to your issue tracker
68
- - Built-in presets: `--tracker clickup`, `--tracker jira`, `--tracker linear`
69
- - Custom patterns: `--ticket-pattern "#([a-z0-9]+)" --tracker-url "https://app.clickup.com/t/{{id}}"`
70
- - ReDoS-safe regex validation
71
-
72
- **CI Mode:**
73
- - `--ci` for fully non-interactive execution
74
- - `--conflict-strategy ours|theirs|skip|fail`
75
- - `--format json` for structured output (stdout=JSON, stderr=logs)
76
- - Distinct exit codes: 0=success, 1=conflict, 2=no commits, 3=auth error, 4=dependency
77
-
78
- **Dependency Detection:**
79
- - Warns when selected commits depend on unselected ones (file-level heuristic)
80
- - Options: include missing commits, go back to selection, or continue anyway
81
- - `--dependency-strategy warn|fail|ignore`
82
-
83
- **Undo / Rollback:**
84
- - `--undo` resets release branch to pre-cherry-pick state
85
- - Checkpoint saved automatically before each session
86
- - Validates branch integrity before reset
87
- - Uses `--force-with-lease` (not `--force`)
106
+ ### 4. Filter commits by pattern
88
107
 
89
- **Changelog Preview:**
90
- - Shows full changelog before cherry-pick starts
91
- - Includes version bump info and ticket links
92
- - Confirmation defaults to No โ€” must explicitly approve
108
+ ```bash
109
+ cherrypick-interactive --ignore-commits "^chore\(deps\)|^ci:"
110
+ ```
93
111
 
94
- ## Options
112
+ Excludes commits starting with `chore(deps)` or `ci:` from the selection list.
95
113
 
96
- ### Cherry-pick
114
+ ### 5. Ignore certain commits from semantic versioning
97
115
 
98
- | Flag | Default | Description |
99
- |------|---------|-------------|
100
- | `--dev` | `origin/dev` | Source branch |
101
- | `--main` | `origin/main` | Target branch |
102
- | `--since` | `1 week ago` | Time window for commits |
103
- | `--no-fetch` | `false` | Skip `git fetch --prune` |
104
- | `--all-yes` | `false` | Select all commits without prompt |
105
- | `--ignore-commits` | โ€” | Regex patterns to exclude commits |
116
+ ```bash
117
+ cherrypick-interactive --ignore-semver "bump|dependencies"
118
+ ```
106
119
 
107
- ### Version
120
+ Treats commits containing "bump" or "dependencies" as chores (no version bump).
108
121
 
109
- | Flag | Default | Description |
110
- |------|---------|-------------|
111
- | `--semantic-versioning` | `true` | Auto-detect version bump |
112
- | `--current-version` | โ€” | Current X.Y.Z version |
113
- | `--version-file` | `./package.json` | Read/write version from file |
114
- | `--version-commit-message` | `chore(release): bump version to {{version}}` | Commit message template |
115
- | `--ignore-semver` | โ€” | Regex patterns to exclude from versioning |
122
+ ### 6. Use a saved profile
116
123
 
117
- ### Release
124
+ ```bash
125
+ # Save your flags once
126
+ cherrypick-interactive --save-profile hotfix --dev origin/develop --main origin/release --since "2 weeks ago"
118
127
 
119
- | Flag | Default | Description |
120
- |------|---------|-------------|
121
- | `--create-release` | `true` | Create release branch |
122
- | `--push-release` | `true` | Push and create PR |
123
- | `--draft-pr` | `false` | Create PR as draft |
128
+ # Reuse anytime
129
+ cherrypick-interactive --profile hotfix
130
+ ```
131
+
132
+ ### 7. Run in CI/CD pipeline
133
+
134
+ ```bash
135
+ cherrypick-interactive --ci --conflict-strategy theirs --format json > result.json
136
+ ```
137
+
138
+ ### 8. Link ticket IDs in changelog
139
+
140
+ ```bash
141
+ cherrypick-interactive --tracker clickup --tracker-url "https://app.clickup.com/t/{{id}}"
142
+ ```
143
+
144
+ ### 9. Undo the last cherry-pick session
145
+
146
+ ```bash
147
+ cherrypick-interactive --undo
148
+ ```
149
+
150
+ ---
151
+
152
+ ## โš™๏ธ Options
153
+
154
+ ### Cherry-pick options
155
+
156
+ | Flag | Description | Default |
157
+ |------|--------------|----------|
158
+ | `--dev` | Source branch (commits to copy) | `origin/dev` |
159
+ | `--main` | Target branch (commits already merged here will be skipped) | `origin/main` |
160
+ | `--since` | Git time window filter (e.g. `"2 weeks ago"`) | `1 week ago` |
161
+ | `--no-fetch` | Skip `git fetch --prune` | `false` |
162
+ | `--all-yes` | Cherry-pick all missing commits without prompt | `false` |
163
+ | `--ignore-commits` | Comma-separated regex patterns to exclude commits | โ€” |
164
+
165
+ ### Version options
166
+
167
+ | Flag | Description | Default |
168
+ |------|--------------|----------|
169
+ | `--semantic-versioning` | Detect semantic version bump from commits | `true` |
170
+ | `--current-version` | Current version (if not reading from file) | โ€” |
171
+ | `--version-file` | Path to `package.json` (to read & update version) | `./package.json` |
172
+ | `--version-commit-message` | Template for version bump commit | `chore(release): bump version to {{version}}` |
173
+ | `--ignore-semver` | Comma-separated regex patterns to ignore for semver | โ€” |
124
174
 
125
- ### CI
175
+ ### Release options
126
176
 
127
- | Flag | Default | Description |
128
- |------|---------|-------------|
129
- | `--ci` | `false` | Non-interactive mode |
130
- | `--conflict-strategy` | `fail` | `fail`, `ours`, `theirs`, `skip` |
131
- | `--format` | `text` | `text` or `json` |
132
- | `--dependency-strategy` | `warn` | `warn`, `fail`, `ignore` |
177
+ | Flag | Description | Default |
178
+ |------|--------------|----------|
179
+ | `--create-release` | Create `release/x.y.z` branch from `main` | `true` |
180
+ | `--push-release` | Push release branch to origin | `true` |
181
+ | `--draft-pr` | Create the GitHub PR as a draft | `false` |
133
182
 
134
- ### Tracker
183
+ ### CI options
135
184
 
136
- | Flag | Default | Description |
137
- |------|---------|-------------|
138
- | `--tracker` | โ€” | Preset: `clickup`, `jira`, `linear` |
139
- | `--ticket-pattern` | โ€” | Custom regex (one capture group) |
140
- | `--tracker-url` | โ€” | URL template with `{{id}}` |
185
+ | Flag | Description | Default |
186
+ |------|--------------|----------|
187
+ | `--ci` | Enable fully non-interactive mode | `false` |
188
+ | `--conflict-strategy` | How to handle conflicts: `fail`, `ours`, `theirs`, `skip` | `fail` |
189
+ | `--format` | Output format: `text` or `json` | `text` |
190
+ | `--dependency-strategy` | How to handle dependencies: `warn`, `fail`, `ignore` | `warn` |
141
191
 
142
- ### Profile
192
+ ### Tracker options
143
193
 
144
- | Flag | Default | Description |
145
- |------|---------|-------------|
146
- | `--profile` | โ€” | Load named profile |
147
- | `--save-profile` | โ€” | Save current flags as profile |
148
- | `--list-profiles` | `false` | List profiles and exit |
194
+ | Flag | Description | Default |
195
+ |------|--------------|----------|
196
+ | `--tracker` | Built-in preset: `clickup`, `jira`, `linear` | โ€” |
197
+ | `--ticket-pattern` | Custom regex to capture ticket ID (one capture group) | โ€” |
198
+ | `--tracker-url` | URL template with `{{id}}` placeholder | โ€” |
149
199
 
150
- ### Session
200
+ ### Profile options
151
201
 
152
- | Flag | Default | Description |
153
- |------|---------|-------------|
154
- | `--undo` | `false` | Rollback to pre-cherry-pick state |
202
+ | Flag | Description | Default |
203
+ |------|--------------|----------|
204
+ | `--profile` | Load a named profile from `.cherrypickrc.json` | โ€” |
205
+ | `--save-profile` | Save current CLI flags as a named profile | โ€” |
206
+ | `--list-profiles` | List available profiles and exit | `false` |
155
207
 
156
- ### UI
208
+ ### Session options
157
209
 
158
- | Flag | Default | Description |
159
- |------|---------|-------------|
160
- | `--no-tui` | `false` | Use simple checkbox instead of TUI |
161
- | `--dry-run` | `false` | Preview without applying |
210
+ | Flag | Description | Default |
211
+ |------|--------------|----------|
212
+ | `--undo` | Reset release branch to pre-cherry-pick state | `false` |
162
213
 
163
- ## CI/CD Usage
214
+ ### UI options
164
215
 
165
- ```yaml
166
- - run: npx cherrypick-interactive --ci --conflict-strategy theirs --format json > result.json
216
+ | Flag | Description | Default |
217
+ |------|--------------|----------|
218
+ | `--no-tui` | Disable TUI dashboard, use simple checkbox instead | `false` |
219
+ | `--dry-run` | Show what would happen without applying changes | `false` |
220
+
221
+ ---
222
+
223
+ ## ๐Ÿง  How Semantic Versioning Works
224
+
225
+ The tool analyzes commit messages using **Conventional Commits**:
226
+
227
+ | Prefix | Example | Bump |
228
+ |---------|----------|------|
229
+ | `BREAKING CHANGE:` | `feat(auth): BREAKING CHANGE: require MFA` | **major** |
230
+ | `feat:` | `feat(ui): add dark mode` | **minor** |
231
+ | `fix:` / `perf:` | `fix(api): correct pagination offset` | **patch** |
232
+
233
+ Use `--ignore-semver` to treat certain commits as chores:
234
+
235
+ ```bash
236
+ cherrypick-interactive --ignore-semver "^chore\(deps\)|bump|merge"
167
237
  ```
168
238
 
169
- Exit codes: `0` success, `1` conflict, `2` no commits, `3` auth error, `4` dependency issue.
239
+ ---
240
+
241
+ ## โš”๏ธ Interactive Conflict Resolution
242
+
243
+ When cherry-picking encounters conflicts, the tool provides an **interactive wizard**:
244
+
245
+ ### Conflict Resolution Options:
246
+
247
+ **Per-file resolution:**
248
+ - **Use ours** โ€” Keep the current branch's version
249
+ - **Use theirs** โ€” Accept the cherry-picked commit's version
250
+ - **Open in editor** โ€” Manually resolve conflicts in your editor
251
+ - **Show diff** โ€” View the conflicting changes
252
+ - **Mark resolved** โ€” Stage the file as-is
253
+
254
+ **Bulk actions:**
255
+ - **Use ours for ALL** โ€” Apply current branch's version to all conflicts
256
+ - **Use theirs for ALL** โ€” Accept cherry-picked version for all conflicts
257
+ - **Stage ALL** โ€” Mark all files as resolved
258
+ - **Launch mergetool** โ€” Use Git's configured merge tool
259
+
260
+ In CI mode, `--conflict-strategy` handles conflicts automatically (`ours`, `theirs`, `skip`, or `fail`).
261
+
262
+ ---
263
+
264
+ ## ๐Ÿ–ฅ๏ธ TUI Dashboard
265
+
266
+ The commit selection screen features a rich terminal UI:
267
+
268
+ - **Arrow keys / j/k** โ€” Navigate commits
269
+ - **Space** โ€” Toggle selection
270
+ - **a / n** โ€” Select all / deselect all
271
+ - **/** โ€” Search/filter commits by message
272
+ - **d** โ€” Full diff overlay (Esc to return)
273
+ - **p** โ€” Toggle preview pane
274
+ - **Enter** โ€” Confirm selection
275
+ - **q** โ€” Quit (with confirmation if commits are selected)
276
+
277
+ Each commit shows its hash, subject, and relative date. Selected commits are highlighted in green.
278
+
279
+ Falls back to simple `inquirer` checkbox on: Windows, small terminals, CI, or with `--no-tui`.
280
+
281
+ ---
170
282
 
171
- ## Profiles
283
+ ## ๐Ÿ’พ Profiles
284
+
285
+ Save and reuse CLI flag combinations:
172
286
 
173
287
  ```bash
174
288
  # Save
@@ -177,8 +291,11 @@ cherrypick-interactive --save-profile hotfix --dev origin/develop --main origin/
177
291
  # Use
178
292
  cherrypick-interactive --profile hotfix
179
293
 
180
- # Override
294
+ # Override a single flag
181
295
  cherrypick-interactive --profile hotfix --since "3 days ago"
296
+
297
+ # List all profiles
298
+ cherrypick-interactive --list-profiles
182
299
  ```
183
300
 
184
301
  Config stored in `.cherrypickrc.json`:
@@ -199,22 +316,203 @@ Config stored in `.cherrypickrc.json`:
199
316
  }
200
317
  ```
201
318
 
202
- ## Conflict Resolution
319
+ ---
320
+
321
+ ## ๐Ÿ”— Tracker Integration
322
+
323
+ Link ticket IDs in your changelog to your issue tracker:
324
+
325
+ ```bash
326
+ # Built-in presets
327
+ cherrypick-interactive --tracker clickup --tracker-url "https://app.clickup.com/t/{{id}}"
328
+ cherrypick-interactive --tracker jira --tracker-url "https://team.atlassian.net/browse/{{id}}"
329
+ cherrypick-interactive --tracker linear --tracker-url "https://linear.app/my-team/issue/{{id}}"
330
+
331
+ # Custom pattern
332
+ cherrypick-interactive --ticket-pattern "#([a-z0-9]+)" --tracker-url "https://app.clickup.com/t/{{id}}"
333
+ ```
334
+
335
+ Commit `#86c8w62wx - Fix login bug` becomes `[#86c8w62wx](https://app.clickup.com/t/86c8w62wx) - Fix login bug` in the changelog.
336
+
337
+ ---
338
+
339
+ ## ๐Ÿค– CI Mode
340
+
341
+ Run fully non-interactive in CI/CD pipelines:
342
+
343
+ ```bash
344
+ cherrypick-interactive --ci --conflict-strategy theirs --format json > result.json
345
+ ```
346
+
347
+ **Exit codes:**
348
+
349
+ | Code | Meaning |
350
+ |------|---------|
351
+ | `0` | Success |
352
+ | `1` | Conflict (with `--conflict-strategy fail`) |
353
+ | `2` | No commits found |
354
+ | `3` | Auth / push error |
355
+ | `4` | Dependency issue (with `--dependency-strategy fail`) |
356
+
357
+ **JSON output** goes to stdout, all logs go to stderr. Colors auto-disabled in JSON mode.
358
+
359
+ ---
360
+
361
+ ## โ†ฉ๏ธ Undo / Rollback
362
+
363
+ Made a mistake? Roll back the entire cherry-pick session:
364
+
365
+ ```bash
366
+ cherrypick-interactive --undo
367
+ ```
368
+
369
+ - Checkpoint saved automatically before each session
370
+ - Validates branch integrity before reset (ancestor check + divergence detection)
371
+ - Uses `--force-with-lease` (not `--force`)
372
+ - Option to re-open commit selection after undo
373
+
374
+ ---
375
+
376
+ ## ๐Ÿงน Why This Helps
377
+
378
+ If your team:
379
+ - Rebases or cherry-picks from `dev` โ†’ `main`
380
+ - Uses temporary release branches
381
+ - Works with squashed commits
382
+ - Needs to handle merge conflicts gracefully
383
+ - Tracks semantic versions via commits
384
+
385
+ โ€ฆthis CLI saves time and reduces errors.
386
+ It automates a tedious, error-prone manual process into a single command that behaves like `yarn upgrade-interactive`, but for Git commits.
387
+
388
+ **Special features:**
389
+ - โœ… Preserves exact commit messages (critical for squashed commits)
390
+ - โœ… Interactive conflict resolution without leaving the terminal
391
+ - โœ… Smart pattern-based filtering for commits and version detection
392
+ - โœ… Automatic changelog generation with ticket linking
393
+ - โœ… TUI dashboard with diff preview and keyboard shortcuts
394
+ - โœ… CI mode with structured JSON output and distinct exit codes
395
+ - โœ… Undo/rollback with safety checks
396
+ - โœ… Reusable profiles for common workflows
397
+
398
+ ---
399
+
400
+ ## ๐Ÿงฐ Requirements
401
+
402
+ - Node.js โ‰ฅ 20
403
+ - Git โ‰ฅ 2.0
404
+ - **GitHub CLI (`gh`)** โ€” *Optional, only required if using `--push-release`*
405
+ - Install from: https://cli.github.com/
406
+ - The tool will check if `gh` is installed and offer to continue without it
407
+ - A clean working directory (no uncommitted changes)
408
+
409
+ ---
410
+
411
+ ## ๐ŸŽฏ Best Practices
412
+
413
+ ### 1. Use `--ignore-commits` to filter noise
414
+
415
+ ```bash
416
+ cherrypick-interactive --ignore-commits "^ci:|^chore\(deps\):|Merge branch"
417
+ ```
418
+
419
+ Exclude CI updates, dependency bumps, and merge commits from selection.
420
+
421
+ ### 2. Use `--ignore-semver` for version accuracy
422
+
423
+ ```bash
424
+ cherrypick-interactive --ignore-semver "bump|dependencies|merge"
425
+ ```
426
+
427
+ Prevent certain commits from affecting semantic version calculation.
428
+
429
+ ### 3. Always use `--draft-pr` for review
430
+
431
+ ```bash
432
+ cherrypick-interactive --draft-pr
433
+ ```
434
+
435
+ Creates draft PRs so your team can review before merging.
436
+
437
+ ### 4. Test with `--dry-run` first
438
+
439
+ ```bash
440
+ cherrypick-interactive --dry-run
441
+ ```
442
+
443
+ See what would happen without making any changes.
444
+
445
+ ### 5. Save your workflow as a profile
446
+
447
+ ```bash
448
+ cherrypick-interactive --save-profile release --dev origin/dev --main origin/main --since "1 month ago" --draft-pr
449
+ ```
450
+
451
+ Then just run `cherrypick-interactive --profile release` every time.
452
+
453
+ ---
454
+
455
+ ## ๐Ÿงพ License
456
+
457
+ **MIT** โ€” free to use, modify, and distribute.
458
+
459
+ ---
460
+
461
+ ## ๐Ÿง‘โ€๐Ÿ’ป Contributing
462
+
463
+ 1. Clone the repo
464
+ 2. Install dependencies: `yarn install`
465
+ 3. Run locally:
466
+ ```bash
467
+ node cli.js --dry-run
468
+ ```
469
+ 4. Run tests:
470
+ ```bash
471
+ yarn test
472
+ ```
473
+ 5. Test edge cases before submitting PRs:
474
+ - Squashed commits with conflicts
475
+ - Empty cherry-picks
476
+ - Multiple conflict resolutions
477
+ 6. Please follow Conventional Commits for your changes.
478
+
479
+ ---
203
480
 
204
- When conflicts occur, the tool offers:
481
+ ## ๐Ÿ› Troubleshooting
205
482
 
206
- **Per-file:** use ours, use theirs, open in editor, show diff, mark resolved
483
+ ### "GitHub CLI (gh) is not installed"
484
+ The tool automatically checks for `gh` CLI when using `--push-release`. If not found, you'll be prompted to:
485
+ - Install it from https://cli.github.com/ and try again
486
+ - Or continue without creating a PR (the release branch will still be pushed)
207
487
 
208
- **Bulk:** use ours for all, use theirs for all, stage all, launch mergetool
488
+ You can also run without `--push-release` to skip PR creation entirely:
489
+ ```bash
490
+ cherrypick-interactive --create-release --no-push-release
491
+ ```
492
+
493
+ ### "Cherry-pick has conflicts"
494
+ Use the interactive wizard to resolve conflicts file-by-file or in bulk. In CI, use `--conflict-strategy`.
209
495
 
210
- In CI mode, `--conflict-strategy` handles this automatically.
496
+ ### "Commit message changed after conflict resolution"
497
+ This issue has been fixed! The tool now preserves the original commit message using `git commit -C <hash>`.
498
+
499
+ ### "Version not detected correctly"
500
+ Use `--ignore-semver` to exclude commits that shouldn't affect versioning:
501
+ ```bash
502
+ cherrypick-interactive --ignore-semver "bump|chore\(deps\)"
503
+ ```
211
504
 
212
- ## Requirements
505
+ ### "Too many commits to review"
506
+ Use `--ignore-commits` to filter out noise, or adjust `--since` to a shorter time window:
507
+ ```bash
508
+ cherrypick-interactive --since "3 days ago" --ignore-commits "^ci:|^docs:"
509
+ ```
213
510
 
214
- - Node.js >= 20
215
- - Git >= 2.0
216
- - GitHub CLI (`gh`) โ€” optional, for `--push-release`
511
+ ### "Want to undo a cherry-pick session"
512
+ ```bash
513
+ cherrypick-interactive --undo
514
+ ```
217
515
 
218
- ## License
516
+ ---
219
517
 
220
- MIT
518
+ > 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
@@ -34,7 +34,7 @@ if (upd && semver.valid(upd.latest) && semver.valid(pkg.version) && semver.gt(up
34
34
 
35
35
  // Skip interactive prompt in CI or non-TTY
36
36
  if (process.stdout.isTTY && !process.env.CI) {
37
- const { shouldUpdate } = await inquirer.prompt([
37
+ const { shouldUpdate } = await prompt([
38
38
  {
39
39
  type: 'confirm',
40
40
  name: 'shouldUpdate',
@@ -263,6 +263,22 @@ const ciResult = {
263
263
  pr: { url: null },
264
264
  };
265
265
 
266
+ /**
267
+ * Wrap inquirer.prompt to handle Ctrl+C gracefully.
268
+ * Instead of throwing "User force closed the prompt", exit cleanly.
269
+ */
270
+ async function prompt(questions) {
271
+ try {
272
+ return await inquirer.prompt(questions);
273
+ } catch (e) {
274
+ if (e.name === 'ExitPromptError' || e.message?.includes('force closed')) {
275
+ log(chalk.yellow('\nAborted by user.'));
276
+ process.exit(0);
277
+ }
278
+ throw e;
279
+ }
280
+ }
281
+
266
282
  class ExitError extends Error {
267
283
  constructor(message, exitCode = 1) {
268
284
  super(message);
@@ -318,7 +334,7 @@ async function selectCommitsInteractive(missing) {
318
334
  ];
319
335
  const termHeight = process.stdout.rows || 24; // fallback for non-TTY environments
320
336
 
321
- const { selected } = await inquirer.prompt([
337
+ const { selected } = await prompt([
322
338
  {
323
339
  type: 'checkbox',
324
340
  name: 'selected',
@@ -375,7 +391,7 @@ async function handleCherryPickConflict(hash) {
375
391
  err(chalk.red(`\nโœ– Cherry-pick has conflicts on ${hash} (${shortSha(hash)}).`));
376
392
  await showConflictsList(); // prints conflicted files (if any)
377
393
 
378
- const { action } = await inquirer.prompt([
394
+ const { action } = await prompt([
379
395
  {
380
396
  type: 'select',
381
397
  name: 'action',
@@ -461,7 +477,7 @@ async function showConflictsList() {
461
477
  }
462
478
 
463
479
  async function resolveSingleFileWizard(file) {
464
- const { action } = await inquirer.prompt([
480
+ const { action } = await prompt([
465
481
  {
466
482
  type: 'select',
467
483
  name: 'action',
@@ -491,7 +507,7 @@ async function resolveSingleFileWizard(file) {
491
507
  log(chalk.cyan(`Opening ${file} in ${editor}...`));
492
508
  await runBin(editor, [file]);
493
509
  // user edits and saves, so now they can stage
494
- const { stageNow } = await inquirer.prompt([
510
+ const { stageNow } = await prompt([
495
511
  {
496
512
  type: 'confirm',
497
513
  name: 'stageNow',
@@ -526,7 +542,7 @@ async function conflictsResolutionWizard(hash) {
526
542
  // If there are no conflicted files, either continue or detect empty pick
527
543
  if (await isEmptyCherryPick()) {
528
544
  err(chalk.yellow('The previous cherry-pick is now empty.'));
529
- const { emptyAction } = await inquirer.prompt([
545
+ const { emptyAction } = await prompt([
530
546
  {
531
547
  type: 'select',
532
548
  name: 'emptyAction',
@@ -571,7 +587,7 @@ async function conflictsResolutionWizard(hash) {
571
587
  }
572
588
  }
573
589
 
574
- const { choice } = await inquirer.prompt([
590
+ const { choice } = await prompt([
575
591
  {
576
592
  type: 'select',
577
593
  name: 'choice',
@@ -625,7 +641,7 @@ async function conflictsResolutionWizard(hash) {
625
641
  // If nothing is staged, treat as empty pick and prompt
626
642
  if (!(await hasStagedChanges())) {
627
643
  err(chalk.yellow('No staged changes found for this cherry-pick.'));
628
- const { emptyAction } = await inquirer.prompt([
644
+ const { emptyAction } = await prompt([
629
645
  {
630
646
  type: 'select',
631
647
  name: 'emptyAction',
@@ -852,7 +868,7 @@ async function saveProfile(name, flags) {
852
868
  config.profiles = config.profiles || {};
853
869
 
854
870
  if (config.profiles[name]) {
855
- const { overwrite } = await inquirer.prompt([
871
+ const { overwrite } = await prompt([
856
872
  {
857
873
  type: 'confirm',
858
874
  name: 'overwrite',
@@ -1012,7 +1028,7 @@ async function handleUndo() {
1012
1028
  // Warn if on a different branch
1013
1029
  if (currentBranch !== session.branch) {
1014
1030
  log(chalk.yellow(`โš  You are on "${currentBranch}" but the session was created on "${session.branch}".`));
1015
- const { switchBranch } = await inquirer.prompt([
1031
+ const { switchBranch } = await prompt([
1016
1032
  { type: 'confirm', name: 'switchBranch', message: `Switch to ${session.branch}?`, default: true },
1017
1033
  ]);
1018
1034
  if (switchBranch) {
@@ -1049,7 +1065,7 @@ async function handleUndo() {
1049
1065
  log(` Commits to discard: ${commitsSinceCheckpoint}`);
1050
1066
  log(chalk.gray(' This is an all-or-nothing rollback โ€” individual commits cannot be selectively removed.\n'));
1051
1067
 
1052
- const { proceed } = await inquirer.prompt([
1068
+ const { proceed } = await prompt([
1053
1069
  { type: 'confirm', name: 'proceed', message: 'Continue?', default: false },
1054
1070
  ]);
1055
1071
 
@@ -1077,7 +1093,7 @@ async function handleUndo() {
1077
1093
  log(chalk.green(`\nBranch ${session.branch} has been reset to ${shortSha(session.checkpoint)}. You can now re-select commits.`));
1078
1094
 
1079
1095
  // Offer to re-open selection
1080
- const { reopen } = await inquirer.prompt([
1096
+ const { reopen } = await prompt([
1081
1097
  { type: 'confirm', name: 'reopen', message: 'Re-open commit selection?', default: true },
1082
1098
  ]);
1083
1099
 
@@ -1292,7 +1308,7 @@ async function main() {
1292
1308
  err(chalk.cyan(' Install it from: https://cli.github.com/'));
1293
1309
  err(chalk.cyan(' Or run without --push-release to skip PR creation.\n'));
1294
1310
 
1295
- const { proceed } = await inquirer.prompt([
1311
+ const { proceed } = await prompt([
1296
1312
  {
1297
1313
  type: 'confirm',
1298
1314
  name: 'proceed',
@@ -1382,7 +1398,7 @@ async function main() {
1382
1398
  // warn: already logged above, continue
1383
1399
  } else {
1384
1400
  const missingHashes = [...new Set(deps.map((d) => d.dependency))];
1385
- const { choice } = await inquirer.prompt([
1401
+ const { choice } = await prompt([
1386
1402
  {
1387
1403
  type: 'list',
1388
1404
  name: 'choice',
@@ -1401,7 +1417,7 @@ async function main() {
1401
1417
  const subj = await gitRaw(['show', '--format=%s', '-s', h]);
1402
1418
  log(` + ${chalk.dim(`(${shortSha(h)})`)} ${subj}`);
1403
1419
  }
1404
- const { confirm } = await inquirer.prompt([
1420
+ const { confirm } = await prompt([
1405
1421
  { type: 'confirm', name: 'confirm', message: 'Add these commits?', default: true },
1406
1422
  ]);
1407
1423
  if (confirm) {
@@ -1487,7 +1503,7 @@ async function main() {
1487
1503
 
1488
1504
  // Confirmation (skip in CI)
1489
1505
  if (!argv.ci && !argv['all-yes']) {
1490
- const { proceed } = await inquirer.prompt([
1506
+ const { proceed } = await prompt([
1491
1507
  {
1492
1508
  type: 'confirm',
1493
1509
  name: 'proceed',
@@ -1652,7 +1668,7 @@ async function ensureReleaseBranchFresh(branchName, startPoint) {
1652
1668
  return;
1653
1669
  }
1654
1670
 
1655
- const { action } = await inquirer.prompt([
1671
+ const { action } = await prompt([
1656
1672
  {
1657
1673
  type: 'select',
1658
1674
  name: 'action',
@@ -1800,7 +1816,7 @@ async function getPkgVersion(pkgPath) {
1800
1816
  let pkg = await readJson(pkgPath);
1801
1817
  if (!pkg) {
1802
1818
  log(chalk.yellow(`โš  ${pkgPath} not found.`));
1803
- const { shouldCreate } = await inquirer.prompt([
1819
+ const { shouldCreate } = await prompt([
1804
1820
  {
1805
1821
  type: 'confirm',
1806
1822
  name: 'shouldCreate',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cherrypick-interactive",
3
- "version": "1.11.0",
3
+ "version": "1.13.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
6
  "bin": "cli.js",
@@ -15,7 +15,11 @@
15
15
  "check": "biome check .",
16
16
  "fix": "biome check --write .",
17
17
  "release": "npm publish --access public",
18
- "test": "node --test test/**/*.test.js"
18
+ "test": "node --test test/**/*.test.js",
19
+ "prepare": "simple-git-hooks"
20
+ },
21
+ "simple-git-hooks": {
22
+ "pre-push": "yarn test"
19
23
  },
20
24
  "engines": {
21
25
  "node": ">=20"
@@ -33,7 +37,8 @@
33
37
  "yargs": "^18.0.0"
34
38
  },
35
39
  "devDependencies": {
36
- "@biomejs/biome": "^1.9.4"
40
+ "@biomejs/biome": "^1.9.4",
41
+ "simple-git-hooks": "^2.13.1"
37
42
  },
38
43
  "keywords": [
39
44
  "git",
package/src/tui/App.js CHANGED
@@ -14,6 +14,7 @@ export function App({ commits, gitRawFn, devBranch, mainBranch, since, onDone })
14
14
  const [filterText, setFilterText] = useState('');
15
15
  const [isSearching, setIsSearching] = useState(false);
16
16
  const [searchInput, setSearchInput] = useState('');
17
+ const [showPreview, setShowPreview] = useState(true);
17
18
  const [showDiff, setShowDiff] = useState(false);
18
19
  const [diffText, setDiffText] = useState('');
19
20
  const [confirmQuit, setConfirmQuit] = useState(false);
@@ -28,7 +29,7 @@ export function App({ commits, gitRawFn, devBranch, mainBranch, since, onDone })
28
29
  useEffect(() => {
29
30
  if (!currentCommit) return;
30
31
  let cancelled = false;
31
- gitRawFn(['show', '--stat', '--format=', currentCommit.hash]).then((text) => {
32
+ gitRawFn(['show', '--stat', '--format=', '--color=always', currentCommit.hash]).then((text) => {
32
33
  if (!cancelled) setPreviewText(text.trim());
33
34
  }).catch(() => {
34
35
  if (!cancelled) setPreviewText('(unable to load preview)');
@@ -114,7 +115,7 @@ export function App({ commits, gitRawFn, devBranch, mainBranch, since, onDone })
114
115
  if (currentCommit) {
115
116
  setShowDiff(true);
116
117
  setDiffText('Loading...');
117
- gitRawFn(['show', '--stat', '-p', currentCommit.hash]).then((text) => {
118
+ gitRawFn(['show', '--stat', '-p', '--color=always', currentCommit.hash]).then((text) => {
118
119
  setDiffText(text.trim());
119
120
  }).catch(() => {
120
121
  setDiffText('(unable to load diff)');
@@ -122,6 +123,11 @@ export function App({ commits, gitRawFn, devBranch, mainBranch, since, onDone })
122
123
  }
123
124
  }
124
125
 
126
+ // Toggle preview
127
+ else if (input === 'p') {
128
+ setShowPreview((v) => !v);
129
+ }
130
+
125
131
  // Confirm
126
132
  else if (key.return) {
127
133
  const selectedHashes = [...selected];
@@ -185,10 +191,12 @@ export function App({ commits, gitRawFn, devBranch, mainBranch, since, onDone })
185
191
  isSearching=${isSearching}
186
192
  selectedCount=${selected.size}
187
193
  />
188
- <${Preview}
189
- previewText=${previewText}
190
- hash=${currentCommit?.hash}
191
- />
194
+ ${showPreview ? html`
195
+ <${Preview}
196
+ previewText=${previewText}
197
+ hash=${currentCommit?.hash}
198
+ />
199
+ ` : null}
192
200
  </${Box}>
193
201
  `;
194
202
  }
package/src/tui/KeyBar.js CHANGED
@@ -1,20 +1,30 @@
1
1
  import { Text, Box } from 'ink';
2
2
  import { html } from './html.js';
3
3
 
4
+ function Key({ k, label }) {
5
+ return html`<${Text}><${Text} color="cyan" bold>[${k}]</${Text}><${Text} color="gray"> ${label} </${Text}></${Text}>`;
6
+ }
7
+
4
8
  export function KeyBar({ isSearching, selectedCount }) {
5
9
  if (isSearching) {
6
10
  return html`
7
11
  <${Box} paddingX=${1}>
8
- <${Text} color="yellow">[Esc] cancel search [Enter] confirm filter</${Text}>
12
+ <${Key} k="Esc" label="cancel" />
13
+ <${Key} k="Enter" label="confirm filter" />
9
14
  </${Box}>
10
15
  `;
11
16
  }
12
17
 
13
18
  return html`
14
- <${Box} paddingX=${1}>
15
- <${Text} color="dim">
16
- [space] toggle [a] all [n] none [/] search [d] diff [enter] confirm (${selectedCount}) [q] quit
17
- </${Text}>
19
+ <${Box} paddingX=${1} flexWrap="wrap">
20
+ <${Key} k="space" label="toggle" />
21
+ <${Key} k="a" label="all" />
22
+ <${Key} k="n" label="none" />
23
+ <${Key} k="/" label="search" />
24
+ <${Key} k="d" label="diff" />
25
+ <${Key} k="p" label="preview" />
26
+ <${Text}><${Text} color="green" bold>[enter]</${Text}><${Text} color="gray"> confirm (${selectedCount}) </${Text}></${Text}>
27
+ <${Text}><${Text} color="red" bold>[q]</${Text}><${Text} color="gray"> quit</${Text}></${Text}>
18
28
  </${Box}>
19
29
  `;
20
30
  }