Skip to content

Commit 24f5076

Browse files
committed
core: patch: support multi file patch
1 parent 4fc7b81 commit 24f5076

2 files changed

Lines changed: 83 additions & 29 deletions

File tree

packages/hydrooj/bin/hydrooj.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const packageBasedir = require('path').resolve(__dirname, '..');
55
const { homedir } = require('os');
66
const { default: hook } = require('@undefined-moe/require-resolve-hook');
77
const { bypass } = hook(/^(hydrooj|@hydrooj|cordis|lodash|js-yaml)($|\/)/, (id) => {
8-
if (id.startsWith('hydrooj/src')) {
8+
if (id.startsWith('hydrooj/src') && !('__DISABLE_HYDRO_DEPRECATION_WARNING__' in global)) {
99
if (process.env.DEV) {
1010
const error = new Error(`module require via ${id} is deprecated.`);
1111
const filter = [
Lines changed: 82 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,93 @@
1+
/* eslint-disable no-await-in-loop */
12
import child from 'child_process';
23
import path from 'path';
34
import { CAC } from 'cac';
45
import fs from 'fs-extra';
56
import superagent from 'superagent';
6-
import { Logger } from '@hydrooj/utils';
7+
import { findFileSync, Logger } from '@hydrooj/utils';
78

89
const logger = new Logger('patch');
910

10-
export function register(cli: CAC) {
11-
cli.command('patch <module> <patch>').action(async (filename, patch) => {
12-
const mod = require.resolve(filename);
13-
if (!mod) {
14-
logger.error('Module %s not found', filename);
15-
return;
16-
}
17-
logger.info('Patching %s', mod);
18-
let content = '';
19-
if (patch.startsWith('http')) {
20-
const res = await superagent.get(patch).responseType('arraybuffer');
21-
logger.info('Downloaded patch');
22-
content = res.body;
23-
} else {
24-
content = await fs.readFile(patch, 'utf-8');
25-
}
26-
for (let i = 0; i <= 100; i++) {
27-
const fp = path.join(path.dirname(mod), `${path.basename(mod)}.${i}.patch`);
28-
if (fs.existsSync(fp)) continue;
29-
patch = fp;
30-
break;
11+
function locateFile(file: string): string | null {
12+
const candidates = [file];
13+
if (file.startsWith('packages/')) {
14+
candidates.push(file.replace(/^packages\//, ''), file.replace(/^packages\//, '@hydrooj/'));
15+
}
16+
for (const candidate of candidates) {
17+
try {
18+
return findFileSync(candidate, false) || require.resolve(candidate);
19+
} catch (e) {
20+
const resolved = path.resolve(candidate);
21+
if (fs.existsSync(resolved)) return resolved;
3122
}
32-
await fs.writeFile(patch, content);
33-
child.execSync(`patch ${mod} -o ${mod}.tmp < ${patch}`);
34-
await fs.move(`${mod}.tmp`, mod, { overwrite: true });
35-
logger.info('Patched %s', mod);
36-
});
23+
}
24+
return null;
25+
}
3726

38-
// TODO: support revert patch
27+
export function register(cli: CAC) {
28+
cli.command('patch <patchfile>')
29+
.option('--dry-run', 'Show what files would be patched without actually patching them')
30+
.option('-R, --revert', 'Revert the patch instead of applying it')
31+
.action(async (patch: string, options: { dryRun?: boolean, revert?: boolean }) => {
32+
let content = '';
33+
global.__DISABLE_HYDRO_DEPRECATION_WARNING__ = true;
34+
if (/^[a-f0-9]{40}$/.test(patch)) patch = `https://github.com/hydro-dev/Hydro/commit/${patch}.patch`;
35+
if (patch.startsWith('http')) {
36+
const res = await superagent.get(patch).responseType('arraybuffer');
37+
logger.info('Downloaded patch');
38+
content = res.body.toString();
39+
} else content = await fs.readFile(patch, 'utf-8');
40+
const lines = content.split('\n');
41+
const filePatches: { filename: string, startLine: number, endLine: number }[] = [];
42+
for (let i = 0; i < lines.length; i++) {
43+
const line = lines[i];
44+
if (line.startsWith('diff --git')) {
45+
const match = line.match(/diff --git a\/(.+?) b\/(.+?)(?:\s|$)/);
46+
if (!match) continue;
47+
const filename = match[2];
48+
const startLine = i;
49+
let endLine = lines.length;
50+
for (let j = i + 1; j < lines.length; j++) {
51+
if (lines[j].startsWith('diff --git')) {
52+
endLine = j;
53+
break;
54+
}
55+
}
56+
filePatches.push({ filename, startLine, endLine });
57+
}
58+
}
59+
if (!filePatches.length) {
60+
logger.error('No valid patches found in %s', patch);
61+
return;
62+
}
63+
logger.info('Found %d file(s) to %s', filePatches.length, options.revert ? 'revert' : 'patch');
64+
if (options.dryRun) logger.info('DRY RUN MODE - No files will be modified');
65+
for (const filePatch of filePatches) {
66+
const { filename, startLine, endLine } = filePatch;
67+
logger.info(options.revert ? 'Reverting %s' : 'Patching %s', filename);
68+
const filepath = locateFile(filename);
69+
if (!filepath) {
70+
logger.error('File %s not found', filename);
71+
continue;
72+
}
73+
if (options.dryRun) for (let i = startLine; i < endLine; i++) logger.info(lines[i]);
74+
else {
75+
const tempPatchFile = `${filepath}.tmp.patch`;
76+
const filePatchLines = lines.slice(startLine, endLine);
77+
await fs.writeFile(tempPatchFile, `${filePatchLines.join('\n')}\n`);
78+
try {
79+
child.execSync(`patch "${filepath}" -o "${filepath}.tmp"${options.revert ? ' -R' : ''} < "${tempPatchFile}"`);
80+
await fs.move(`${filepath}.tmp`, filepath, { overwrite: true });
81+
logger.success('%s %s', options.revert ? 'Reverted' : 'Patched', filename);
82+
} catch (e) {
83+
logger.error('Failed to patch %s: %s', filename, e.message);
84+
logger.error(e.stdout.toString());
85+
} finally {
86+
if (fs.existsSync(tempPatchFile)) fs.unlinkSync(tempPatchFile);
87+
if (fs.existsSync(`${filepath}.tmp`)) fs.unlinkSync(`${filepath}.tmp`);
88+
}
89+
}
90+
}
91+
logger.info(`${options.dryRun ? 'Dry-run' : options.revert ? 'Revert' : 'Patch'} completed`);
92+
});
3993
}

0 commit comments

Comments
 (0)