@yuki-no/plugin-batch-pr
Version:
Batch PR plugin for yuki-no - Collects opened Yuki-no translation issues and creates a single pull request
148 lines (147 loc) • 5.99 kB
JavaScript
import { formatError, log } from '@yuki-no/plugin-sdk/utils/log';
import fs from 'node:fs';
import path from 'node:path';
export const applyFileChanges = (upstreamGit, fileChanges) => {
log('I', `applyFileChanges :: Starting to apply ${fileChanges.length} file changes`);
if (fileChanges.length === 0) {
log('I', 'applyFileChanges :: No file changes to apply, returning');
return;
}
const repoDirName = upstreamGit.dirName;
for (const fileChange of fileChanges) {
const absoluteFileName = path.join(repoDirName, fileChange.upstreamFileName);
log('I', `applyFileChanges :: Processing ${fileChange.type} for ${fileChange.upstreamFileName}`);
try {
switch (fileChange.type) {
case 'update':
handleUpdateFile(absoluteFileName, fileChange);
break;
case 'delete':
deleteFileSync(absoluteFileName);
break;
case 'rename':
case 'copy':
handleMoveFile(absoluteFileName, repoDirName, fileChange);
break;
case 'type':
// noop (ignore)
log('I', `applyFileChanges :: Ignoring type change for ${fileChange.upstreamFileName}`);
break;
}
}
catch (error) {
log('E', `applyFileChanges :: Failed to process ${fileChange.type} for ${fileChange.upstreamFileName}: ${formatError(error)}`);
throw error;
}
}
log('S', `applyFileChanges :: Successfully applied all ${fileChanges.length} file changes`);
};
const deleteFileSync = (absoluteFileName) => {
if (!fs.existsSync(absoluteFileName)) {
log('I', `deleteFileSync :: File ${absoluteFileName} does not exist, skipping deletion`);
return;
}
try {
fs.unlinkSync(absoluteFileName);
log('S', `deleteFileSync :: Successfully deleted ${absoluteFileName}`);
}
catch (error) {
log('E', `deleteFileSync :: Failed to delete ${absoluteFileName}: ${formatError(error)}`);
throw error;
}
};
const handleUpdateFile = (absoluteFileName, { changes }) => {
log('I', `handleUpdateFile :: Updating file ${absoluteFileName}`);
if (Buffer.isBuffer(changes)) {
log('I', `handleUpdateFile :: Writing binary content (${changes.length} bytes)`);
writeFileSync(absoluteFileName, changes);
return;
}
log('I', `handleUpdateFile :: Processing ${changes.length} line changes`);
// To avoid index shifting
const deleteChanges = changes.filter(({ type }) => type === 'delete-line');
const insertChanges = changes.filter(({ type }) => type === 'insert-line');
log('I', `handleUpdateFile :: ${deleteChanges.length} deletions, ${insertChanges.length} insertions`);
const sortedDeletes = deleteChanges.sort((a, b) => b.lineNumber - a.lineNumber);
const sortedInserts = insertChanges.sort((a, b) => a.lineNumber - b.lineNumber);
let lines = readFileSync(absoluteFileName);
lines = applyLineChanges(lines, sortedDeletes);
lines = applyLineChanges(lines, sortedInserts);
const nextContent = lines.join('\n');
writeFileSync(absoluteFileName, nextContent);
log('S', `handleUpdateFile :: Successfully updated ${absoluteFileName}`);
};
const readFileSync = (fileName) => {
if (!fs.existsSync(fileName)) {
return [];
}
const content = fs.readFileSync(fileName, 'utf-8');
if (content === '') {
return [];
}
return content.split('\n');
};
const applyLineChanges = (lines, changes) => {
if (changes.length === 0) {
return lines;
}
const nextLines = [...lines];
return changes.reduce(applyLineChange, nextLines);
};
const applyLineChange = (lines, lineChange) => {
const index = lineChange.lineNumber - 1; // 1-based to 0-based
switch (lineChange.type) {
case 'insert-line':
lines.splice(Math.max(index, 0), 0, lineChange.content);
break;
case 'delete-line':
if (index >= 0 && index < lines.length) {
lines.splice(index, 1);
}
break;
}
return lines;
};
const writeFileSync = (fileName, contents, encoding = 'utf-8') => {
const dir = path.dirname(fileName);
createDirIfNotExists(dir);
if (Buffer.isBuffer(contents)) {
fs.writeFileSync(fileName, contents);
}
else {
fs.writeFileSync(fileName, contents, encoding);
}
};
const handleMoveFile = (absoluteFileName, repoDirName, { type, nextUpstreamFileName, changes, }) => {
if (!fs.existsSync(absoluteFileName)) {
log('I', `handleMoveFile :: Source file ${absoluteFileName} does not exist, skipping ${type}`);
return;
}
const absoluteNextFileName = path.join(repoDirName, nextUpstreamFileName);
log('I', `handleMoveFile :: ${type === 'rename' ? 'Renaming' : 'Copying'} ${absoluteFileName} to ${absoluteNextFileName}`);
const nextDirName = path.dirname(absoluteNextFileName);
createDirIfNotExists(nextDirName);
try {
if (type === 'rename') {
fs.renameSync(absoluteFileName, absoluteNextFileName);
}
else {
fs.copyFileSync(absoluteFileName, absoluteNextFileName);
}
log('S', `handleMoveFile :: Successfully ${type === 'rename' ? 'renamed' : 'copied'} to ${absoluteNextFileName}`);
if (changes.length > 0) {
log('I', `handleMoveFile :: Applying ${changes.length} additional changes to moved file`);
handleUpdateFile(absoluteNextFileName, { changes });
}
}
catch (error) {
log('E', `handleMoveFile :: Failed to ${type} ${absoluteFileName}: ${formatError(error)}`);
throw error;
}
};
const createDirIfNotExists = (dir) => {
if (fs.existsSync(dir)) {
return;
}
fs.mkdirSync(dir, { recursive: true });
};