Skip to content

Commit 618676d

Browse files
OttoAllmendingerllm-git
andcommitted
feat(scripts): implement cherry-pick for vendor-github-repo script
Add cherry-pick capability for vendored repos, enabling patches to be automatically applied during the vendor process. Implement file removal functionality to clean up unnecessary dev and test files. Improve commit handling and command structure. Issue: BTC-2143 Co-authored-by: llm-git <[email protected]>
1 parent 98cebdb commit 618676d

File tree

1 file changed

+147
-31
lines changed

1 file changed

+147
-31
lines changed

scripts/vendor-github-repo.ts

Lines changed: 147 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,61 @@
1-
import { ChildProcess, execFile } from 'child_process';
1+
import { execa, ResultPromise } from 'execa';
22
import * as fs from 'fs/promises';
33
import * as tmp from 'tmp';
44
import * as yargs from 'yargs';
55

6-
function isErrorExists(e: NodeJS.ErrnoException): boolean {
7-
return e.code === 'EEXIST';
8-
}
9-
106
type GithubSource = {
117
org: string;
128
repo: string;
139
} & ({ tag: string } | { ref: string });
1410

15-
async function wait(p: ChildProcess): Promise<void> {
16-
p.stderr?.pipe(process.stderr);
17-
p.stdout?.pipe(process.stdout);
18-
return new Promise((resolve, reject) => {
19-
p.on('exit', (code) => {
20-
if (code === 0) {
21-
resolve();
22-
} else {
23-
reject(new Error(`Process exited with code ${code}`));
24-
}
25-
});
11+
export function getDescription(source: GithubSource): string {
12+
if ('tag' in source) {
13+
return `${source.org}/${source.repo}#${source.tag}`;
14+
} else if ('ref' in source) {
15+
return `${source.org}/${source.repo}#${source.ref}`;
16+
} else {
17+
throw new Error('Invalid GithubSource, must have either tag or ref');
18+
}
19+
}
20+
21+
export type VendorConfig = GithubSource & {
22+
targetDir: string;
23+
removeFiles: string[];
24+
/** Git commit range with patches */
25+
cherryPick: {
26+
start: string;
27+
end: string;
28+
} | null;
29+
};
30+
31+
function isErrorExists(e: NodeJS.ErrnoException): boolean {
32+
return e.code === 'EEXIST';
33+
}
34+
35+
async function gitCommit({ path, title, message }: { path: string; title: string; message: string }): Promise<void> {
36+
await execa('git', ['add', path]);
37+
await execa('git', ['commit', path, '-m', title, '-m', message, '--no-verify'], {
38+
stdio: 'inherit',
39+
});
40+
}
41+
42+
async function gitCherryPick(revRange: { start: string; end: string }): Promise<void> {
43+
const range = `${revRange.start}^..${revRange.end}`;
44+
await execa('git', ['cherry-pick', range], {
45+
stdio: 'inherit',
46+
});
47+
}
48+
49+
async function removeDevAndTestFiles(removePaths: string[], targetDir: string): Promise<void> {
50+
for (const path of removePaths) {
51+
console.log(`Removing dev/test file: ${path}`);
52+
const fullPath = `${targetDir}/${path}`;
53+
await fs.rm(fullPath, { recursive: true, force: true });
54+
}
55+
await gitCommit({
56+
path: targetDir,
57+
title: 'chore: remove dev and test files',
58+
message: 'This commit removes unnecessary development and test files from the vendor directory.',
2659
});
2760
}
2861

@@ -58,6 +91,12 @@ async function fetchArchive(lib: GithubSource, outfile: string): Promise<void> {
5891
await fs.writeFile(outfile, Buffer.from(await result.arrayBuffer()));
5992
}
6093

94+
function pipe(cmd: ResultPromise): ResultPromise {
95+
cmd.stdout?.pipe(process.stdout);
96+
cmd.stderr?.pipe(process.stderr);
97+
return cmd;
98+
}
99+
61100
async function extractArchive(archivePath: string, targetDir: string): Promise<void> {
62101
try {
63102
await fs.mkdir(targetDir, { recursive: true });
@@ -66,18 +105,42 @@ async function extractArchive(archivePath: string, targetDir: string): Promise<v
66105
throw e;
67106
}
68107
}
69-
await wait(execFile('tar', ['-C', targetDir, '--strip-components', '1', '-xzf', archivePath]));
108+
await pipe(execa('tar', ['-C', targetDir, '--strip-components', '1', '-xzf', archivePath]));
70109
}
71110

72-
type VendorConfig = GithubSource & {
73-
targetDir: string;
74-
};
75-
76-
async function main(cfgs: VendorConfig[]) {
111+
async function cmdVendor(
112+
cfgs: VendorConfig[],
113+
opts: {
114+
removeFiles: boolean;
115+
cherryPick: boolean;
116+
}
117+
) {
77118
for (const cfg of cfgs) {
119+
const desc = getDescription(cfg);
78120
const archivePath = getArchivePath(cfg);
79121
await fetchArchive(cfg, archivePath);
80122
await extractArchive(archivePath, cfg.targetDir);
123+
await gitCommit({
124+
path: cfg.targetDir,
125+
title: `chore: vendor ${desc}`,
126+
message: `This commit was generated by the vendor-github-repo script.`,
127+
});
128+
129+
if (cfg.removeFiles.length === 0) {
130+
console.log('No files to remove for', cfg.repo);
131+
} else if (opts.removeFiles) {
132+
await removeDevAndTestFiles(cfg.removeFiles, cfg.targetDir);
133+
} else {
134+
console.log(`Skipping removal of dev/test files for ${cfg.repo}, use --removeFiles to enable.`);
135+
}
136+
137+
if (cfg.cherryPick === null) {
138+
console.log(`No patch set defined for ${cfg.repo}, skipping rebase.`);
139+
} else if (opts.removeFiles) {
140+
await gitCherryPick(cfg.cherryPick);
141+
} else {
142+
console.log(`Skipping cherry-pick for ${cfg.repo}, use --cherryPick to enable.`);
143+
}
81144
}
82145
}
83146

@@ -87,24 +150,77 @@ const vendorConfigs: VendorConfig[] = [
87150
repo: 'btc-staking-ts',
88151
tag: 'v1.0.3',
89152
targetDir: 'modules/babylonlabs-io-btc-staking-ts',
153+
removeFiles: [
154+
'.eslintrc.json',
155+
'.github/',
156+
'.husky/',
157+
'.npmrc',
158+
'.nvmrc',
159+
'.prettierignore',
160+
'.prettierrc.json',
161+
'docs/',
162+
'tests/',
163+
'README.md',
164+
],
165+
cherryPick: {
166+
start: '8b8261b8b639d09cbe1223615797c18a2788cd89',
167+
end: '06110dd3e892df326261cd79ead158c28370add7',
168+
},
90169
},
91170
];
92171

172+
function getMatches(name: string, version: string | undefined): VendorConfig[] {
173+
const matches = vendorConfigs.filter((cfg) => {
174+
if (name !== cfg.repo) {
175+
return false;
176+
}
177+
if ('tag' in cfg && version !== undefined) {
178+
return cfg.tag === version;
179+
}
180+
return true;
181+
});
182+
if (matches.length === 0) {
183+
throw new Error(`no such vendor config ${name} version ${version}`);
184+
}
185+
if (matches.length > 1) {
186+
throw new Error(`ambiguous vendor config ${name}`);
187+
}
188+
return matches;
189+
}
190+
191+
const optName = {
192+
type: 'string',
193+
demand: true,
194+
describe: 'Name of the vendor config to use, e.g. btc-staking-ts',
195+
} as const;
196+
197+
const optVersion = {
198+
type: 'string',
199+
describe: 'Version of the vendor config to use, e.g. v2.3.4',
200+
} as const;
201+
93202
yargs
94203
.command({
95204
command: 'vendor',
205+
describe: 'Vendor a github repo',
96206
builder(a) {
97-
return a.options({ name: { type: 'string' } });
207+
return a.options({
208+
name: optName,
209+
pkgVersion: optVersion,
210+
removeFiles: {
211+
type: 'boolean',
212+
default: true,
213+
describe: 'Remove dev/test files after extracting the archive',
214+
},
215+
cherryPick: {
216+
type: 'boolean',
217+
default: true,
218+
describe: 'Apply cherry-pick patches after extracting the archive',
219+
},
220+
});
98221
},
99222
async handler(a) {
100-
const matches = vendorConfigs.filter((cfg) => a.name === cfg.repo);
101-
if (matches.length === 0) {
102-
throw new Error(`no such vendor config ${a.name}`);
103-
}
104-
if (matches.length > 1) {
105-
throw new Error(`ambiguous vendor config ${a.name}`);
106-
}
107-
await main(matches);
223+
await cmdVendor(getMatches(a.name, a.pkgVersion), a);
108224
},
109225
})
110226
.help()

0 commit comments

Comments
 (0)