import { CommitDiff } from '../interfaces/CommitDiff'; import { FileDiff, Line } from '../interfaces/FileDiff'; /** * The output of `git show` is a string that looks like this: * * commit 9fceb02d0ae598e95dc970b74767e84987207e60 (HEAD -> main, origin/main) * Author: John Doe <john.doe@example.com> * Date: Mon Nov 15 08:37:41 2023 -0800 * * Fix a bug and improve documentation * * diff --git a/file1.txt b/file1.txt * index 7f77f51..9c54642 100644 * --- a/file1.txt * +++ b/file1.txt * @@ -1,2 +1,2 @@ * -Hello, world! * +Hello, Git! * * diff --git a/file2.txt b/file2.txt * index 8ad1c76..f414f31 100644 * --- a/file2.txt * +++ b/file2.txt * @@ -1,3 +1,4 @@ * Hello, world! * +This is a new line. * * diff --git a/documentation.txt b/documentation.txt * index 553c207..762f61c 100644 * --- a/documentation.txt * +++ b/documentation.txt * @@ -4,6 +4,7 @@ Title: Documentation * - Section 1 * - Section 2 * - Section 3 * +- Section 4 */ /** * Generates an object that represents the output of `git show`. * @param showString The output of `git show` * @returns An object that represents the output of `git show` */ function parseGitCommitShowOutput(showString: string): CommitDiff { exitIfGitShowOutputEmpty(showString); // Given the format of the output of `git show`, we can split the string by '\n\n' // to get the header, title and message, and the diff chunks. const componentsOfShowString = showString.split('\n\n'); const header = componentsOfShowString[0]; const titleAndMessage = componentsOfShowString[1]; const rawDiffStrings = componentsOfShowString.slice(2); const { commitHash, author, date, } = getHeaderFieldsOfGitShowOutput(header); const { title, message, } = getTitleAndMessageOfGitShowOutput(titleAndMessage); const validFileDiffStrings = rawDiffStrings.filter(isDiffValidForInspection); const fileDiffs = validFileDiffStrings.map(parseFileDiff); return { commitHash, author, date, title, message, diffs: fileDiffs }; } /** * Generates an object that represents the header of the output of `git show`. * The output of `git show` must be in the following format: * * commit 9fceb02d0ae598e95dc970b74767e84987207e60 (HEAD -> main, origin/main) * Author: John Doe <john.doe@example.com> * Date: Mon Nov 15 08:37:41 2023 -0800 */ function getHeaderFieldsOfGitShowOutput(showStringHeader: string) { let lines = showStringHeader.split('\n'); lines = lines.map(line => line.trim()).filter(line => line !== ''); const commitHash = lines[0].split(' ')[1]; const START_OF_AUTHOR_LINE = "Author:"; let author = "could not find author" const authorLine = lines[1]; const position = authorLine.indexOf(START_OF_AUTHOR_LINE); if (position !== -1) { author = authorLine.substring(position + START_OF_AUTHOR_LINE.length).trim(); } const START_OF_DATE_LINE = "Date:"; let date = "could not find date" const dateLine = lines[2]; const datePosition = dateLine.indexOf(START_OF_DATE_LINE); if (datePosition !== -1) { date = dateLine.substring(datePosition + START_OF_DATE_LINE.length).trim(); } return { commitHash, author, date }; } /** * Generates an object that represents the title and message in the output of `git show`. * @param titleAndMessage The string containing the title and message in the output of `git show`. */ function getTitleAndMessageOfGitShowOutput(titleAndMessage: string) { let lines = titleAndMessage.split('\n'); lines = lines.map(line => line.trim()); const title = lines[0]; const message = lines.slice(1).join('\n'); return { title, message }; } /** * Generates an object for a git diff chunk. * @param diffString A string that represents a git diff chunk. * @returns An object that represents a git diff chunk. */ function parseFileDiff(fileDiff: string): FileDiff { if (fileDiff === "" || !fileDiff.startsWith("diff")) { throw new Error("Invalid diff chunk. The diff chunk should be a non-empty string, and start with the word 'diff'."); } let lines = fileDiff.split('\n'); const componentsOfFirstLine = lines[0].split(' '); const filePath = componentsOfFirstLine[2] const fileName = filePath.split('/').at(-1) if (!fileName) { throw new Error("Invalid diff chunk. No file name found."); } const hunks = getDiffHunks(fileDiff); const linesFromAllHunks: Line[] = []; for (const hunk of hunks) { const hunkLines = parseDiffHunk(hunk); linesFromAllHunks.push(...hunkLines); } return { fileName, lines: linesFromAllHunks }; } /** * Takes a string with one or more git diff hunks, * and returns each hunk as a string in a string array. * Within each file diff, there is one or more diff hunks. * Each diff hunk starts with a line that starts with '@@'. */ function getDiffHunks(fileDiff: string): string[] { const hunkArrays: string[][] = []; let currHunk: string[] | null = null; const lines = fileDiff.split('\n'); for (const line of lines) { // If we encounter @@, start a new hunk and push current hunk to hunks array // If we encounter anything else, add it to the current hunk if (line.startsWith("@@")) { currHunk = [line] hunkArrays.push(currHunk); } else if (currHunk === null) { continue } else { currHunk.push(line); } } const hunkStrings = hunkArrays.map(hunk => hunk.join('\n')); return hunkStrings } /** * Within each file diff, there is one or more diff hunks. * Each diff hunk starts with a line that starts with '@@'. */ function parseDiffHunk(diffHunk: string): Line[] { const lines = diffHunk.split('\n'); const hunkHeader = lines[0] const hunkHeaderParts = hunkHeader.trim().split(' '); const startLineNum = parseInt(hunkHeaderParts[2].split(',')[0].slice(1), 10); // Get the diff lines and calculate the ending output line let diffLineStrings = lines.slice(1); diffLineStrings = adjustIndentation(diffLineStrings); diffLineStrings = diffLineStrings.filter(line => !(line.trim() == "")); const hunkLines = diffLineStrings.map((line, index) => { return { lineNum: startLineNum + index, content: line } }); return hunkLines; } function isDiffValidForInspection(diffString: string): boolean { // Only diffs with a '+++' line are valid for inspection. // Otherwise, they are diffs for empty files, which we don't want to inspect. return diffString.includes("+++") } function adjustIndentation(lines: string[]): string[] { // Find the minimum indentation across all lines const minIndentation = Math.min( ...lines .filter(line => line.trim()) // Ignore empty lines .map(line => line.search(/\S/)) // Find the position of the first non-whitespace character ); // Subtract this minimum indentation from each line return lines.map(line => { // Only adjust lines that are not empty if (line.trim()) { return line.slice(minIndentation); } else { return line; // Return empty lines unmodified } }); } // Throw error and exit if git show output is empty function exitIfGitShowOutputEmpty(gitShowString: string) { if (gitShowString === "") { throw new Error("Git show output empty. Exiting."); } } export { parseGitCommitShowOutput }