cherrypick-interactive 1.0.1 → 1.2.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.
Files changed (4) hide show
  1. package/biome.json +23 -0
  2. package/cli.backup.js +163 -156
  3. package/cli.js +685 -405
  4. package/package.json +14 -11
package/biome.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3
+ "formatter": {
4
+ "enabled": true,
5
+ "indentStyle": "space",
6
+ "indentWidth": 2,
7
+ "lineWidth": 100,
8
+ "ignore": ["node_modules", "yarn.lock"]
9
+ },
10
+ "linter": {
11
+ "enabled": true,
12
+ "rules": {
13
+ "recommended": true
14
+ }
15
+ },
16
+ "organizeImports": {
17
+ "enabled": true
18
+ },
19
+ "vcs": {
20
+ "enabled": true,
21
+ "clientKind": "git"
22
+ }
23
+ }
package/cli.backup.js CHANGED
@@ -1,186 +1,193 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import chalk from 'chalk'
4
- import inquirer from 'inquirer'
5
- import simpleGit from 'simple-git'
6
- import yargs from 'yargs'
3
+ import chalk from "chalk";
4
+ import inquirer from "inquirer";
5
+ import simpleGit from "simple-git";
6
+ import yargs from "yargs";
7
7
 
8
- import { hideBin } from 'yargs/helpers'
8
+ import { hideBin } from "yargs/helpers";
9
9
 
10
- const git = simpleGit()
10
+ const git = simpleGit();
11
11
 
12
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)
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
51
 
52
52
  async function gitRaw(args) {
53
- const out = await git.raw(args)
54
- return out.trim()
53
+ const out = await git.raw(args);
54
+ return out.trim();
55
55
  }
56
56
 
57
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))
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
63
  }
64
64
 
65
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
- })
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
77
  }
78
78
 
79
79
  function filterMissing(devCommits, mainSubjects) {
80
- return devCommits.filter(({ subject }) => !mainSubjects.has(subject))
80
+ return devCommits.filter(({ subject }) => !mainSubjects.has(subject));
81
81
  }
82
82
 
83
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
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
110
  }
111
111
 
112
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
- }
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;
126
125
  }
126
+ }
127
127
  }
128
128
 
129
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([getDevCommits(argv.dev, argv.since), getSubjects(argv.main)])
143
-
144
- const missing = filterMissing(devCommits, mainSubjects)
145
-
146
- if (missing.length === 0) {
147
- log(chalk.green('✅ No missing commits found in the selected window.'))
148
- return
149
- }
150
-
151
- // Prepare bottom→top ordering support
152
- const indexByHash = new Map(missing.map((c, i) => [c.hash, i])) // 0=newest, larger=older
153
-
154
- let selected
155
- if (argv.yes) {
156
- selected = missing.map((m) => m.hash)
157
- } else {
158
- selected = await selectCommitsInteractive(missing)
159
- if (!selected.length) {
160
- log(chalk.yellow('No commits selected. Exiting.'))
161
- return
162
- }
163
- }
164
-
165
- // Bottom → Top (oldest → newest)
166
- const bottomToTop = [...selected].sort((a, b) => indexByHash.get(b) - indexByHash.get(a))
167
-
168
- if (argv.dry_run || argv['dry-run']) {
169
- log(chalk.cyan('\n--dry-run: would cherry-pick (oldest → newest):'))
170
- for (const h of bottomToTop) {
171
- const subj = await gitRaw(['show', '--format=%s', '-s', h])
172
- log(`- ${chalk.dim(`(${h.slice(0, 7)})`)} ${subj}`)
173
- }
174
- return
175
- }
176
-
177
- log(chalk.cyan(`\nCherry-picking ${bottomToTop.length} commit(s) onto ${currentBranch} (oldest → newest)...\n`))
178
- await cherryPickSequential(bottomToTop)
179
- log(chalk.green(`\n✅ Done on ${currentBranch}`))
180
- } catch (e) {
181
- err(chalk.red(`\n❌ Error: ${e.message || e}`))
182
- process.exit(1)
130
+ try {
131
+ if (!argv["no-fetch"]) {
132
+ log(chalk.gray("Fetching remotes (git fetch --prune)..."));
133
+ await git.fetch(["--prune"]);
183
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
+ }
184
191
  }
185
192
 
186
- main()
193
+ main();