|
| 1 | +/* eslint-disable no-await-in-loop */ |
1 | 2 | import child from 'child_process'; |
2 | 3 | import path from 'path'; |
3 | 4 | import { CAC } from 'cac'; |
4 | 5 | import fs from 'fs-extra'; |
5 | 6 | import superagent from 'superagent'; |
6 | | -import { Logger } from '@hydrooj/utils'; |
| 7 | +import { findFileSync, Logger } from '@hydrooj/utils'; |
7 | 8 |
|
8 | 9 | const logger = new Logger('patch'); |
9 | 10 |
|
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; |
31 | 22 | } |
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 | +} |
37 | 26 |
|
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 | + }); |
39 | 93 | } |
0 commit comments