import fsPromises from 'fs/promises';
import { write as temporaryWrite } from 'tempy';
import CodedError, { ASSERT_ERROR } from './codedError.js';
import Janitor from './janitor.js';
import SeparatedRecordConsumer from './SeparatedRecordConsumer.js';
import { ENDL_PATTERN } from './stringUtils.js';
import { normalizeOpts } from './toolInvocationHelpers.js';
export { ASSERT_ERROR };
const hunkMapping = /^@@\s*-?(\d+)(?:,(\d+))?\s+\+?(\d+)(?:,(\d+))?/;
/**
* @summary Class encapsulating usage of `diff`
* @memberof module:git-casefile/impl
*/
class DiffInteraction {
constructor({ runDiffCommand }) {
this.diffCommandRunner = runDiffCommand;
}
async runDiffCommand({opts = {}, ...kwargs} = {}) {
// Preprocess opts
opts = normalizeOpts(opts);
return this.diffCommandRunner({opts, ...kwargs});
}
/**
* @typedef {object} OnDiskContent
* @summary Indicate content stored on the disk
* @property {string} path - Path to the file containing the content
*/
/**
* @typedef {object} ImmediateContent
* @summary Provide content as a string
* @property {string} immediate - The text to process
*/
/**
* @typedef {(OnDiskContent | ImmediateContent)} TextContent
*/
/**
* @typedef {object} Change
* @summary Range of 1-based line numbers in both sides of a diff representing a change
*
* @property {number} baseStart
* 1-based line number in base version to delete or before which to insert
* @property {number} baseEnd
* 1-based line number of the first line *not* to delete; for pure
* insertion, this equals *baseStart*
* @property {number} currentStart
* 1-based line number in current version to add or mark position of
* deletion
* @property {number} currentEnd
* 1-based line number in current version marking the first line *not* to
* insert; for pure deletion, this equals *currentStart*
*/
/**
* @summary Get line number ranges of hunks that change between two text versions
* @param {TextContent} baseContent
* @param {TextContent} currentContent
* @returns {Array.<Change>}
*
* @description
* Invoke `diff` to compute line-based hunks that change from a base version
* to a current version. The content for a version may be provided either
* as a path to a file already on disk or as an immediate string; when an
* immediate string is provided, it is saved to a temporary file so that
* two files paths can be provided for invoking `diff`.
*/
async getHunks(baseContent, currentContent) {
const janitor = new Janitor();
try {
const [ basePath, currentPath ] = await Promise.all(
[baseContent, currentContent].map(c => this._getPath(c, janitor))
);
const hunks = [];
return await this.runDiffCommand({
opts: { U: 0 },
args: [ basePath, currentPath ],
stdout: new SeparatedRecordConsumer(ENDL_PATTERN).on('record', (line) => {
const parts = hunkMapping.exec(line);
if (!parts) return;
const newHunk = {
baseStart: parseInt(parts[1]),
baseEnd: parseInt(parts[1]) + parseInt(parts[2] || "1"),
currentStart: parseInt(parts[3]),
currentEnd: parseInt(parts[3]) + parseInt(parts[4] || "1"),
};
if (parts[2] == "0") {
newHunk.baseStart = newHunk.baseEnd = newHunk.baseStart + 1;
}
if (parts[4] == "0") {
newHunk.currentStart = newHunk.currentEnd = newHunk.currentStart + 1;
}
hunks.push(newHunk);
}),
exit: exitCode => {
if (exitCode === 0 || exitCode === 1) {
return hunks;
} else {
const error = new DiffInteractionError({
code: 'DiffFailure',
message: "'diff' between base and current failed",
base: contentDesc(baseContent),
current: contentDesc(currentContent),
exitCode,
});
throw error;
}
},
});
} finally {
await janitor.cleanUpAsync();
}
}
async _getPath(content, janitor) {
if (content.path) {
return content.path;
}
if (content.immediate) {
const filePath = await temporaryWrite(content.immediate);
janitor.addTask(() => fsPromises.rm(filePath, {force: true}));
return filePath;
}
throw new DiffInteractionError({
code: 'UnknownContentType',
contentKeys: Object.keys(content),
});
}
}
function contentDesc(content) {
if (content.path) {
return { path: content.path };
}
if (content.immediate) {
return { immediate: '...' };
}
/* istanbul ignore next */
return Object.keys(content);
}
const ERROR_MESSAGES_BY_CODE = {
DiffFailure: "The diff command failed",
UnknownContentType: "The content source is of an unknown type (expected 'path' or 'immediate')",
};
export class DiffInteractionError extends CodedError(ERROR_MESSAGES_BY_CODE) {}
export default DiffInteraction;