/** * Parse rsync --itemize-changes output into a structured change list. * * rsync itemize format: 11-character code + space + path * * Code structure: YXcstpoguax * Y = update type: > (transfer), * (message/delete), c (local change), . (no update), h (hard link) * X = file type: f (file), d (dir), L (symlink), D (device), S (special) * remaining chars = what changed (size, time, perms, etc.) or '+++++++++' for new * * We care about: * >f... = file transferred (new or modified) * *deleting = file deleted * cd... = directory (ignored for delta purposes) */ /** * @typedef {{ status: 'added'|'modified'|'deleted', path: string }} Change */ /** * Parse rsync --itemize-changes stdout into a list of file changes. * @param {string} output * @returns {Change[]} */ export function parseItemize(output) { const changes = []; for (const raw of output.split('\n')) { const line = raw.trimEnd(); if (!line) continue; // Deleted files: "*deleting path/to/file" if (line.startsWith('*deleting ')) { const path = line.slice('*deleting '.length).trimStart(); // Skip directory deletions (trailing slash) if (!path.endsWith('/')) { changes.push({ status: 'deleted', path }); } continue; } // File transfers: ">f......... path" (new or modified) if (line.length > 12 && line[0] === '>' && line[1] === 'f') { const code = line.slice(0, 11); const path = line.slice(12); const isNew = code.slice(2) === '+++++++++'; changes.push({ status: isNew ? 'added' : 'modified', path }); continue; } // Everything else (dirs, symlinks, attribute-only changes) — ignore } return changes; }