core: patch: support multi file patch

This commit is contained in:
undefined committed 2026-01-06 16:58:16 +08:00
1 parent 4fc7b815b7
commit 24f5076ddc
2 files changed
+84 -30

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ const packageBasedir = require('path').resolve(__dirname, '..');
const { homedir } = require('os'); const { homedir } = require('os');
const { default: hook } = require('@undefined-moe/require-resolve-hook'); const { default: hook } = require('@undefined-moe/require-resolve-hook');
const { bypass } = hook(/^(hydrooj|@hydrooj|cordis|lodash|js-yaml)($|\/)/, (id) => { const { bypass } = hook(/^(hydrooj|@hydrooj|cordis|lodash|js-yaml)($|\/)/, (id) => {
if (id.startsWith('hydrooj/src')) { if (id.startsWith('hydrooj/src') && !('__DISABLE_HYDRO_DEPRECATION_WARNING__' in global)) {
if (process.env.DEV) { if (process.env.DEV) {
const error = new Error(`module require via ${id} is deprecated.`); const error = new Error(`module require via ${id} is deprecated.`);
const filter = [ const filter = [
+83 -29
View File
@@ -1,39 +1,93 @@
/* eslint-disable no-await-in-loop */
import child from 'child_process'; import child from 'child_process';
import path from 'path'; import path from 'path';
import { CAC } from 'cac'; import { CAC } from 'cac';
import fs from 'fs-extra'; import fs from 'fs-extra';
import superagent from 'superagent'; import superagent from 'superagent';
import { Logger } from '@hydrooj/utils'; import { findFileSync, Logger } from '@hydrooj/utils';
const logger = new Logger('patch'); const logger = new Logger('patch');
export function register(cli: CAC) { function locateFile(file: string): string | null {
cli.command('patch <module> <patch>').action(async (filename, patch) => { const candidates = [file];
const mod = require.resolve(filename); if (file.startsWith('packages/')) {
if (!mod) { candidates.push(file.replace(/^packages\//, ''), file.replace(/^packages\//, '@hydrooj/'));
logger.error('Module %s not found', filename); }
return; for (const candidate of candidates) {
try {
return findFileSync(candidate, false) || require.resolve(candidate);
} catch (e) {
const resolved = path.resolve(candidate);
if (fs.existsSync(resolved)) return resolved;
} }
logger.info('Patching %s', mod); }
let content = ''; return null;
if (patch.startsWith('http')) { }
const res = await superagent.get(patch).responseType('arraybuffer');
logger.info('Downloaded patch'); export function register(cli: CAC) {
content = res.body; cli.command('patch <patchfile>')
} else { .option('--dry-run', 'Show what files would be patched without actually patching them')
content = await fs.readFile(patch, 'utf-8'); .option('-R, --revert', 'Revert the patch instead of applying it')
} .action(async (patch: string, options: { dryRun?: boolean, revert?: boolean }) => {
for (let i = 0; i <= 100; i++) { let content = '';
const fp = path.join(path.dirname(mod), `${path.basename(mod)}.${i}.patch`); global.__DISABLE_HYDRO_DEPRECATION_WARNING__ = true;
if (fs.existsSync(fp)) continue; if (/^[a-f0-9]{40}$/.test(patch)) patch = `https://github.com/hydro-dev/Hydro/commit/${patch}.patch`;
patch = fp; if (patch.startsWith('http')) {
break; const res = await superagent.get(patch).responseType('arraybuffer');
} logger.info('Downloaded patch');
await fs.writeFile(patch, content); content = res.body.toString();
child.execSync(`patch ${mod} -o ${mod}.tmp < ${patch}`); } else content = await fs.readFile(patch, 'utf-8');
await fs.move(`${mod}.tmp`, mod, { overwrite: true }); const lines = content.split('\n');
logger.info('Patched %s', mod); const filePatches: { filename: string, startLine: number, endLine: number }[] = [];
}); for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// TODO: support revert patch if (line.startsWith('diff --git')) {
const match = line.match(/diff --git a\/(.+?) b\/(.+?)(?:\s|$)/);
if (!match) continue;
const filename = match[2];
const startLine = i;
let endLine = lines.length;
for (let j = i + 1; j < lines.length; j++) {
if (lines[j].startsWith('diff --git')) {
endLine = j;
break;
}
}
filePatches.push({ filename, startLine, endLine });
}
}
if (!filePatches.length) {
logger.error('No valid patches found in %s', patch);
return;
}
logger.info('Found %d file(s) to %s', filePatches.length, options.revert ? 'revert' : 'patch');
if (options.dryRun) logger.info('DRY RUN MODE - No files will be modified');
for (const filePatch of filePatches) {
const { filename, startLine, endLine } = filePatch;
logger.info(options.revert ? 'Reverting %s' : 'Patching %s', filename);
const filepath = locateFile(filename);
if (!filepath) {
logger.error('File %s not found', filename);
continue;
}
if (options.dryRun) for (let i = startLine; i < endLine; i++) logger.info(lines[i]);
else {
const tempPatchFile = `${filepath}.tmp.patch`;
const filePatchLines = lines.slice(startLine, endLine);
await fs.writeFile(tempPatchFile, `${filePatchLines.join('\n')}\n`);
try {
child.execSync(`patch "${filepath}" -o "${filepath}.tmp"${options.revert ? ' -R' : ''} < "${tempPatchFile}"`);
await fs.move(`${filepath}.tmp`, filepath, { overwrite: true });
logger.success('%s %s', options.revert ? 'Reverted' : 'Patched', filename);
} catch (e) {
logger.error('Failed to patch %s: %s', filename, e.message);
logger.error(e.stdout.toString());
} finally {
if (fs.existsSync(tempPatchFile)) fs.unlinkSync(tempPatchFile);
if (fs.existsSync(`${filepath}.tmp`)) fs.unlinkSync(`${filepath}.tmp`);
}
}
}
logger.info(`${options.dryRun ? 'Dry-run' : options.revert ? 'Revert' : 'Patch'} completed`);
});
} }