cherrypick-interactive 1.1.0 → 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.
- package/biome.json +23 -0
- package/cli.backup.js +163 -156
- package/cli.js +652 -608
- 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
|
|
4
|
-
import inquirer from
|
|
5
|
-
import simpleGit from
|
|
6
|
-
import yargs from
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
54
|
-
|
|
53
|
+
const out = await git.raw(args);
|
|
54
|
+
return out.trim();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
async function getSubjects(branch) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
+
return devCommits.filter(({ subject }) => !mainSubjects.has(subject));
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
async function selectCommitsInteractive(missing) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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();
|