cherrypick-interactive 1.12.0 โ 1.13.1
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 +435 -137
- package/cli.js +39 -19
- package/package.json +1 -1
- package/src/tui/KeyBar.js +15 -5
package/README.md
CHANGED
|
@@ -1,174 +1,288 @@
|
|
|
1
|
-
# cherrypick-interactive
|
|
1
|
+
# ๐ชถ cherrypick-interactive
|
|
2
2
|
|
|
3
|
-
Cherry-pick missing commits from
|
|
3
|
+
### Cherry-pick missing commits from `dev` to `main` โ interactively and safely.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
52
|
+
npm install -g cherrypick-interactive
|
|
11
53
|
```
|
|
12
54
|
|
|
13
|
-
|
|
55
|
+
(You can also run it directly without installing globally using `npx`.)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## ๐ Quick Start
|
|
14
60
|
|
|
15
61
|
```bash
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
+
### 2. Cherry-pick all missing commits automatically
|
|
95
|
+
|
|
96
|
+
```bash
|
|
29
97
|
cherrypick-interactive --all-yes
|
|
30
98
|
```
|
|
31
99
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
- Confirmation defaults to No โ must explicitly approve
|
|
108
|
+
```bash
|
|
109
|
+
cherrypick-interactive --ignore-commits "^chore\(deps\)|^ci:"
|
|
110
|
+
```
|
|
93
111
|
|
|
94
|
-
|
|
112
|
+
Excludes commits starting with `chore(deps)` or `ci:` from the selection list.
|
|
95
113
|
|
|
96
|
-
###
|
|
114
|
+
### 5. Ignore certain commits from semantic versioning
|
|
97
115
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
120
|
+
Treats commits containing "bump" or "dependencies" as chores (no version bump).
|
|
108
121
|
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
###
|
|
175
|
+
### Release options
|
|
126
176
|
|
|
127
|
-
| Flag |
|
|
128
|
-
|
|
129
|
-
| `--
|
|
130
|
-
| `--
|
|
131
|
-
| `--
|
|
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
|
-
###
|
|
183
|
+
### CI options
|
|
135
184
|
|
|
136
|
-
| Flag |
|
|
137
|
-
|
|
138
|
-
| `--
|
|
139
|
-
| `--
|
|
140
|
-
| `--
|
|
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
|
-
###
|
|
192
|
+
### Tracker options
|
|
143
193
|
|
|
144
|
-
| Flag |
|
|
145
|
-
|
|
146
|
-
| `--
|
|
147
|
-
| `--
|
|
148
|
-
| `--
|
|
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
|
-
###
|
|
200
|
+
### Profile options
|
|
151
201
|
|
|
152
|
-
| Flag |
|
|
153
|
-
|
|
154
|
-
| `--
|
|
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
|
-
###
|
|
208
|
+
### Session options
|
|
157
209
|
|
|
158
|
-
| Flag |
|
|
159
|
-
|
|
160
|
-
| `--
|
|
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
|
-
|
|
214
|
+
### UI options
|
|
164
215
|
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
481
|
+
## ๐ Troubleshooting
|
|
205
482
|
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
-
|
|
511
|
+
### "Want to undo a cherry-pick session"
|
|
512
|
+
```bash
|
|
513
|
+
cherrypick-interactive --undo
|
|
514
|
+
```
|
|
217
515
|
|
|
218
|
-
|
|
516
|
+
---
|
|
219
517
|
|
|
220
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1420
|
+
const { confirm } = await prompt([
|
|
1405
1421
|
{ type: 'confirm', name: 'confirm', message: 'Add these commits?', default: true },
|
|
1406
1422
|
]);
|
|
1407
1423
|
if (confirm) {
|
|
@@ -1487,12 +1503,16 @@ async function main() {
|
|
|
1487
1503
|
|
|
1488
1504
|
// Confirmation (skip in CI)
|
|
1489
1505
|
if (!argv.ci && !argv['all-yes']) {
|
|
1490
|
-
|
|
1506
|
+
// Flush stdin buffer to prevent TUI's Enter keypress from leaking into this prompt
|
|
1507
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1508
|
+
if (process.stdin.readable) process.stdin.read();
|
|
1509
|
+
|
|
1510
|
+
const { proceed } = await prompt([
|
|
1491
1511
|
{
|
|
1492
1512
|
type: 'confirm',
|
|
1493
1513
|
name: 'proceed',
|
|
1494
1514
|
message: 'Proceed with cherry-pick?',
|
|
1495
|
-
default:
|
|
1515
|
+
default: true,
|
|
1496
1516
|
},
|
|
1497
1517
|
]);
|
|
1498
1518
|
if (!proceed) {
|
|
@@ -1652,7 +1672,7 @@ async function ensureReleaseBranchFresh(branchName, startPoint) {
|
|
|
1652
1672
|
return;
|
|
1653
1673
|
}
|
|
1654
1674
|
|
|
1655
|
-
const { action } = await
|
|
1675
|
+
const { action } = await prompt([
|
|
1656
1676
|
{
|
|
1657
1677
|
type: 'select',
|
|
1658
1678
|
name: 'action',
|
|
@@ -1800,7 +1820,7 @@ async function getPkgVersion(pkgPath) {
|
|
|
1800
1820
|
let pkg = await readJson(pkgPath);
|
|
1801
1821
|
if (!pkg) {
|
|
1802
1822
|
log(chalk.yellow(`โ ${pkgPath} not found.`));
|
|
1803
|
-
const { shouldCreate } = await
|
|
1823
|
+
const { shouldCreate } = await prompt([
|
|
1804
1824
|
{
|
|
1805
1825
|
type: 'confirm',
|
|
1806
1826
|
name: 'shouldCreate',
|
package/package.json
CHANGED
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
|
-
<${
|
|
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
|
-
<${
|
|
16
|
-
|
|
17
|
-
|
|
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
|
}
|