diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b2f61d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,50 @@ +# Changelog + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- **(structured)** Enhanced patch parsing logic with flexible configuration options. +
+ Details + Allows user-defined processing of dates and diff structures. (666eeb5af094f21f049d5ffaa7c08b24b809005d) +
+- **(parse)** Implemented functionality to parse Git diff strings into structured object arrays. +
+ Details + Includes file changes, each hunk, and line types (additions, deletions, context). (2ea4bb4453594b73a05ddb9a804c6a7df6f0da2e) +
+ +### Changed + +- **(parser)** Improved type safety and modularity in parsing logic. +
+ Details + Centralized parsing-related constants, introduced generic types for `ParseOptions` and `ParsedCommit`, and updated `parseGitPatch` to use these enhanced types. (d3a6d9572b5b3986166bb360134499ed49dbe8af) +
+- **(script)** Replaced `rm -rf dist` in build script with a cross-platform Node.js script. +
+ Details + The new script ([`scripts/cleanDist.ts`](scripts/cleanDist.ts:1)) enhances compatibility. (317c061f504bce58d68af66860fbd83161aa824b) +
+- **(parse/patch)** Improved readability and organization of `parseGitPatch` tests. +
+ Details + Moved mock data to a separate file ([`src/mocks/patch.ts`](src/mocks/patch.ts:1)) and updated `tsconfig.json` include paths. (20d72cc2efa8f36750519a15f66470c9a27ed8cf) +
+- **(parser)** Refactored core parsing logic file structure. +
+ Details + Moved core parsing logic files ([`src/logics/parse/patch.ts`](src/logics/parse/patch.ts:1) and [`src/logics/parse/patch.test.ts`](src/logics/parse/patch.test.ts:1)) to `src/logics/parse/`. Created new index files ([`src/index.ts`](src/index.ts:1), [`src/logics/index.ts`](src/logics/index.ts:1)) for re-exporting and added [`src/types.ts`](src/types.ts:1) for type definitions. (40900db843c36adb05f5e4e52f1c197acd186a03) +
+ +### Style + +- **(test)** Adjusted import order in [`src/logics/parse/patch.test.ts`](src/logics/parse/patch.test.ts:1). (4cb8a65b3ac9132794bfc107f1c56fc927355c5b) + +## [0.1.4] - 2024-12-19 + +- Initial state. diff --git a/README.md b/README.md index f267597..d7f0f76 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,15 @@ console.log(commits); ```typescript // Generate patch with `git format-patch --stdout` -parseGitPatch(patch: string): { +parseGitPatch(patch: string, options?: ParseOptions): { sha: string; authorName: string; authorEmail: string; - date: string; + // String by default, or Date object if options.parseDates is true. + date: string | Date; message: string; - diff: string; + // Raw string by default, or FileChange[] if options.structuredDiff is true. + diff: string | FileChange[]; }[] ``` diff --git a/bun.lockb b/bun.lockb index ca36d77..96cbee0 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 88a4cb9..509558f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dist" ], "scripts": { - "build": "rm -rf dist && tsc", + "build": "bun run scripts/cleanDist.ts && tsc", "release": "bun run build && np", "test": "bun test", "typecheck": "tsc --noEmit" diff --git a/scripts/cleanDist.ts b/scripts/cleanDist.ts new file mode 100644 index 0000000..0045e30 --- /dev/null +++ b/scripts/cleanDist.ts @@ -0,0 +1,18 @@ +import { rm } from "node:fs/promises"; + +const dirToDelete = "dist"; + +async function cleanDist() { + try { + const dirExists = await Bun.file(dirToDelete).exists(); + if (dirExists) { + await rm(dirToDelete, { recursive: true, force: true }); + console.log(`Directory '${dirToDelete}' successfully deleted.`); + } + } catch (error) { + console.error(`Error deleting directory '${dirToDelete}':`, error); + process.exit(1); + } +} + +cleanDist(); diff --git a/src/consts.ts b/src/consts.ts new file mode 100644 index 0000000..fd486ea --- /dev/null +++ b/src/consts.ts @@ -0,0 +1,11 @@ +export const HEADERS = { + FROM: "From: ", + DATE: "Date: ", + SUBJECT: "Subject: ", +}; + +export const REGEX = { + FROM: /^From\s+([0-9a-f]{40})\s/, + AUTHOR_EMAIL: /<(.*)>/, + PATCH_HEADER: /^\[PATCH[^\]]*\]\s*/, +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..de0ffe5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from "./logics/parse/patch"; +export * from "./types"; diff --git a/src/logics/index.ts b/src/logics/index.ts new file mode 100644 index 0000000..322b9a9 --- /dev/null +++ b/src/logics/index.ts @@ -0,0 +1 @@ +export * from "./parse/patch"; \ No newline at end of file diff --git a/src/logics/parse/diffToStructured.test.ts b/src/logics/parse/diffToStructured.test.ts new file mode 100644 index 0000000..8cf7bfd --- /dev/null +++ b/src/logics/parse/diffToStructured.test.ts @@ -0,0 +1,405 @@ +import { describe, expect, it } from "bun:test"; +import { + mockComplexExample, + mockDeletedFile, + mockDiffWithContextOnlyHunk, + mockDiffWithOnlyAdditions, + mockDiffWithOnlyDeletions, + mockEmpty, + mockMultipleFilesChange, + mockMultipleHunks, + mockNewFile, + mockNoNewlineAtEndOfFile, + mockRenamedFile, + mockRenamedFileWithChanges, + mockSingleFileChange, +} from "../../mocks/diffs"; +import { FileChange } from "../../types"; +import { parseDiffToStructured } from "./diffToStructured"; + +describe("parseDiffToStructured", () => { + it("should return an empty array for an empty diff string", () => { + const result = parseDiffToStructured(mockEmpty); + expect(result).toEqual([]); + }); + + it("should parse a diff with a single file change", () => { + const result = parseDiffToStructured(mockSingleFileChange); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("file1.txt"); + expect(fileChange.newPath).toBe("file1.txt"); + expect(fileChange.additions).toBe(1); + expect(fileChange.deletions).toBe(1); + expect(fileChange.hunks).toHaveLength(1); + expect(fileChange.hunks[0].header).toBe("@@ -1,3 +1,3 @@"); + expect(fileChange.hunks[0].lines).toEqual([ + { type: "context", content: "Line 1" }, + { type: "deletion", content: "Line 2" }, + { type: "addition", content: "Line 2 changed" }, + { type: "context", content: "Line 3" }, + ]); + }); + + it("should parse a diff with multiple file changes", () => { + const result = parseDiffToStructured(mockMultipleFilesChange); + expect(result).toHaveLength(2); + + const firstFile = result[0]; + expect(firstFile.oldPath).toBe("fileA.txt"); + expect(firstFile.newPath).toBe("fileA.txt"); + expect(firstFile.additions).toBe(1); + expect(firstFile.deletions).toBe(1); + expect(firstFile.hunks).toHaveLength(1); + expect(firstFile.hunks[0].header).toBe("@@ -1,2 +1,2 @@"); + expect(firstFile.hunks[0].lines).toEqual([ + { type: "deletion", content: "Original line in A" }, + { type: "addition", content: "New line in A" }, + { type: "context", content: "Common line" }, + ]); + + const secondFile = result[1]; + expect(secondFile.oldPath).toBe("fileB.txt"); + expect(secondFile.newPath).toBe("fileB.txt"); + expect(secondFile.additions).toBe(2); + expect(secondFile.deletions).toBe(1); + expect(secondFile.hunks).toHaveLength(1); + expect(secondFile.hunks[0].header).toBe("@@ -5,3 +5,4 @@"); + expect(secondFile.hunks[0].lines).toEqual([ + { type: "context", content: "Another line" }, + { type: "deletion", content: "This will be removed" }, + { type: "addition", content: "This is added" }, + { type: "context", content: "And one more" }, + { type: "addition", content: "And a final new line" }, + ]); + }); + + it("should parse a diff for a new file", () => { + const result = parseDiffToStructured(mockNewFile); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("/dev/null"); + expect(fileChange.newPath).toBe("new_file.txt"); + expect(fileChange.additions).toBe(3); + expect(fileChange.deletions).toBe(0); + expect(fileChange.hunks).toHaveLength(1); + expect(fileChange.hunks[0].header).toBe("@@ -0,0 +1,3 @@"); + expect(fileChange.hunks[0].lines).toEqual([ + { type: "addition", content: "This is a new file." }, + { type: "addition", content: "It has multiple lines." }, + { type: "addition", content: "Added content." }, + ]); + }); + + it("should parse a diff for a deleted file", () => { + const result = parseDiffToStructured(mockDeletedFile); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("deleted_file.txt"); + expect(fileChange.newPath).toBe("/dev/null"); + expect(fileChange.additions).toBe(0); + expect(fileChange.deletions).toBe(2); + expect(fileChange.hunks).toHaveLength(1); + expect(fileChange.hunks[0].header).toBe("@@ -1,2 +0,0 @@"); + expect(fileChange.hunks[0].lines).toEqual([ + { type: "deletion", content: "Content of the deleted file." }, + { type: "deletion", content: "Another line." }, + ]); + }); + + it("should parse a diff for a renamed file (no content changes)", () => { + // Note: parseDiffToStructured focuses on content changes. + // Pure renames without content changes might not produce hunks. + const result = parseDiffToStructured(mockRenamedFile); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("old_name.txt"); + expect(fileChange.newPath).toBe("new_name.txt"); + expect(fileChange.additions).toBe(0); + expect(fileChange.deletions).toBe(0); + expect(fileChange.hunks).toEqual([]); // No content change, so no hunks + }); + + it("should parse a diff for a renamed file with content changes", () => { + const result = parseDiffToStructured(mockRenamedFileWithChanges); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("source.txt"); + expect(fileChange.newPath).toBe("destination.txt"); + expect(fileChange.additions).toBe(1); + expect(fileChange.deletions).toBe(1); + expect(fileChange.hunks).toHaveLength(1); + expect(fileChange.hunks[0].header).toBe("@@ -1,4 +1,4 @@"); + expect(fileChange.hunks[0].lines).toEqual([ + { type: "context", content: "First line" }, + { type: "deletion", content: "Second line (old)" }, + { type: "addition", content: "Second line (new)" }, + { type: "context", content: "Third line" }, + { type: "context", content: "Fourth line" }, + ]); + }); + + it("should parse a diff with multiple hunks in a single file", () => { + const result = parseDiffToStructured(mockMultipleHunks); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("multi_hunk_file.txt"); + expect(fileChange.newPath).toBe("multi_hunk_file.txt"); + expect(fileChange.additions).toBe(2); + expect(fileChange.deletions).toBe(2); + expect(fileChange.hunks).toHaveLength(2); + + const firstHunk = fileChange.hunks[0]; + expect(firstHunk.header).toBe("@@ -1,5 +1,5 @@"); + expect(firstHunk.lines).toEqual([ + { type: "context", content: "Context line 1" }, + { type: "deletion", content: "Removed line 1" }, + { type: "addition", content: "Added line 1" }, + { type: "context", content: "Context line 2" }, + { type: "context", content: "Context line 3" }, + { type: "context", content: "Context line 4" }, + ]); + + const secondHunk = fileChange.hunks[1]; + expect(secondHunk.header).toBe("@@ -10,5 +10,5 @@"); + expect(secondHunk.lines).toEqual([ + { type: "context", content: "Context line 10" }, + { type: "context", content: "Context line 11" }, + { type: "deletion", content: "Removed line 2" }, + { type: "addition", content: "Added line 2" }, + { type: "context", content: "Context line 12" }, + { type: "context", content: "Context line 13" }, + ]); + }); + + it("should handle diffs with 'No newline at end of file' markers", () => { + const result = parseDiffToStructured(mockNoNewlineAtEndOfFile); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("file.txt"); + expect(fileChange.newPath).toBe("file.txt"); + expect(fileChange.additions).toBe(1); + expect(fileChange.deletions).toBe(1); + expect(fileChange.hunks).toHaveLength(1); + expect(fileChange.hunks[0].header).toBe("@@ -1 +1 @@"); + // The '\ No newline...' lines are not part of hunk content in this parser + expect(fileChange.hunks[0].lines).toEqual([ + { type: "deletion", content: "old content" }, + { type: "addition", content: "new content" }, + ]); + }); + + it("should parse a diff with only additions (new file)", () => { + const result = parseDiffToStructured(mockDiffWithOnlyAdditions); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("/dev/null"); + expect(fileChange.newPath).toBe("added_only.txt"); + expect(fileChange.additions).toBe(2); + expect(fileChange.deletions).toBe(0); + expect(fileChange.hunks).toHaveLength(1); + expect(fileChange.hunks[0].lines).toEqual([ + { type: "addition", content: "New line 1" }, + { type: "addition", content: "New line 2" }, + ]); + }); + + it("should parse a diff with only deletions (deleted file)", () => { + const result = parseDiffToStructured(mockDiffWithOnlyDeletions); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("deleted_only.txt"); + expect(fileChange.newPath).toBe("/dev/null"); + expect(fileChange.additions).toBe(0); + expect(fileChange.deletions).toBe(2); + expect(fileChange.hunks).toHaveLength(1); + expect(fileChange.hunks[0].lines).toEqual([ + { type: "deletion", content: "Old line 1" }, + { type: "deletion", content: "Old line 2" }, + ]); + }); + + it("should parse a diff with a hunk containing only context lines", () => { + const result = parseDiffToStructured(mockDiffWithContextOnlyHunk); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("context_only.txt"); + expect(fileChange.newPath).toBe("context_only.txt"); + // Additions and deletions should be 0 if only context lines are present in hunks + expect(fileChange.additions).toBe(0); + expect(fileChange.deletions).toBe(0); + expect(fileChange.hunks).toHaveLength(1); + expect(fileChange.hunks[0].lines).toEqual([ + { type: "context", content: "unchanged line 1" }, + { type: "context", content: "unchanged line 2" }, + { type: "context", content: "unchanged line 3" }, + ]); + }); + + it("should correctly parse a complex diff example with multiple files and hunks", () => { + const result: FileChange[] = parseDiffToStructured(mockComplexExample); + expect(result).toHaveLength(2); + + const firstFile = result[0]; + expect(firstFile.oldPath).toBe("src/complex.js"); + expect(firstFile.newPath).toBe("src/complex.js"); + expect(firstFile.additions).toBe(6); + expect(firstFile.deletions).toBe(2); // console.log and return null + expect(firstFile.hunks).toHaveLength(2); + + expect(firstFile.hunks[0].header).toBe("@@ -5,7 +5,7 @@"); + expect(firstFile.hunks[0].lines).toEqual([ + { type: "context", content: " function oldFunction() {" }, + { type: "deletion", content: ' console.log("old");' }, + { type: "addition", content: ' console.log("new");' }, + { type: "context", content: " }" }, + { type: "context", content: "" }, + { type: "context", content: " // unchanged part" }, + ]); + + expect(firstFile.hunks[1].header).toBe("@@ -20,3 +20,8 @@"); + expect(firstFile.hunks[1].lines).toEqual([ + { type: "context", content: " function anotherFunction() {" }, + { type: "deletion", content: " return null;" }, + { type: "addition", content: ' return "something";' }, + { type: "context", content: " }" }, + { type: "addition", content: "" }, + { type: "addition", content: " function newFunction() {" }, + { type: "addition", content: ' return "newly added";' }, + { type: "addition", content: " }" }, + ]); + + const secondFile = result[1]; + expect(secondFile.oldPath).toBe("README.md"); + expect(secondFile.newPath).toBe("README.md"); + expect(secondFile.additions).toBe(2); + expect(secondFile.deletions).toBe(1); + expect(secondFile.hunks).toHaveLength(1); + expect(secondFile.hunks[0].header).toBe("@@ -1,2 +1,3 @@"); + expect(secondFile.hunks[0].lines).toEqual([ + { type: "context", content: "# Project Title" }, + { type: "deletion", content: "Old description" }, + { type: "addition", content: "New and improved description" }, + { type: "addition", content: "Additional line." }, + ]); + }); + + it("should handle empty lines within hunks correctly", () => { + const diffWithEmptyLines = `diff --git a/file.txt b/file.txt +index 123..456 100644 +--- a/file.txt ++++ b/file.txt +@@ -1,5 +1,6 @@ + Line 1 +-Line 2 ++Line Two ++ + Line 3 + Line 4 ++Line 5 added +`; + const result = parseDiffToStructured(diffWithEmptyLines); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.additions).toBe(3); + expect(fileChange.deletions).toBe(1); + expect(fileChange.hunks[0].lines).toEqual([ + { type: "context", content: "Line 1" }, + { type: "deletion", content: "Line 2" }, + { type: "addition", content: "Line Two" }, + { type: "addition", content: "" }, + { type: "context", content: "Line 3" }, + { type: "context", content: "Line 4" }, + { type: "addition", content: "Line 5 added" }, + ]); + }); + + it("should handle diffs with no common lines at the end of a hunk", () => { + const diff = `diff --git a/test.txt b/test.txt +index 123..456 100644 +--- a/test.txt ++++ b/test.txt +@@ -1,2 +1,2 @@ +-old line ++new line`; // No context line at the end + const result = parseDiffToStructured(diff); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.additions).toBe(1); + expect(fileChange.deletions).toBe(1); + expect(fileChange.hunks[0].lines).toEqual([ + { type: "deletion", content: "old line" }, + { type: "addition", content: "new line" }, + ]); + }); + + it("should handle diffs with no common lines at the start of a hunk", () => { + const diff = `diff --git a/test.txt b/test.txt +index 123..456 100644 +--- a/test.txt ++++ b/test.txt +@@ -1,2 +1,2 @@ +-old line ++new line + common line`; + const result = parseDiffToStructured(diff); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.additions).toBe(1); + expect(fileChange.deletions).toBe(1); + expect(fileChange.hunks[0].lines).toEqual([ + { type: "deletion", content: "old line" }, + { type: "addition", content: "new line" }, + { type: "context", content: "common line" }, + ]); + }); + + it("should handle diff with only one line change", () => { + const diff = `diff --git a/single_line_change.txt b/single_line_change.txt +index e0262b5..29a8b95 100644 +--- a/single_line_change.txt ++++ b/single_line_change.txt +@@ -1 +1 @@ +-hello ++world +`; + const result = parseDiffToStructured(diff); + expect(result).toHaveLength(1); + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("single_line_change.txt"); + expect(fileChange.newPath).toBe("single_line_change.txt"); + expect(fileChange.additions).toBe(1); + expect(fileChange.deletions).toBe(1); + expect(fileChange.hunks).toHaveLength(1); + expect(fileChange.hunks[0].header).toBe("@@ -1 +1 @@"); + expect(fileChange.hunks[0].lines).toEqual([ + { type: "deletion", content: "hello" }, + { type: "addition", content: "world" }, + ]); + }); + + it("should handle diff with binary file changes (no hunks expected)", () => { + const diff = `diff --git a/image.png b/image.png +GIT binary patch +literal 0 +HcmV?d00001 + +literal 15 +HcmV;R00001{<*AW6<*AWcNGL@ + +`; + const result = parseDiffToStructured(diff); + // Binary files might not produce standard hunks, or the parser might skip them. + // Depending on the desired behavior for binary files, this test might need adjustment. + // For now, assume it creates a file entry but no hunks if no '@@' lines are found. + if (result.length > 0) { + const fileChange = result[0]; + expect(fileChange.oldPath).toBe("image.png"); + expect(fileChange.newPath).toBe("image.png"); + expect(fileChange.hunks).toEqual([]); + } else { + // Or, if binary files are skipped entirely: + expect(result).toEqual([]); + } + }); +}); diff --git a/src/logics/parse/diffToStructured.ts b/src/logics/parse/diffToStructured.ts new file mode 100644 index 0000000..4c238b3 --- /dev/null +++ b/src/logics/parse/diffToStructured.ts @@ -0,0 +1,142 @@ +import { DiffHunk, FileChange } from "../../types"; + +export function parseDiffToStructured(diffString: string): FileChange[] { + const fileChanges: FileChange[] = []; + const lines = diffString.split("\n"); + + let currentFile: FileChange | null = null; + let currentHunk: DiffHunk | null = null; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Start of a new file diff + if (line.startsWith("diff --git ")) { + // Previous file processing complete + if (currentFile) { + if (currentHunk && currentHunk.lines.length > 0) { + currentFile.hunks.push(currentHunk); + } + fileChanges.push(currentFile); + } + + const paths = line.match(/diff --git a\/(.*) b\/(.*)/); + // Initialize with paths from diff --git, can be overridden by --- and +++ lines + let oldPath = paths?.[1] || ""; + let newPath = paths?.[2] || ""; + + // Check for new or deleted files based on subsequent lines + // This is a heuristic and might need refinement for complex cases + if (lines[i + 1] && lines[i + 1].startsWith("new file mode")) { + oldPath = "/dev/null"; + } + if (lines[i + 1] && lines[i + 1].startsWith("deleted file mode")) { + newPath = "/dev/null"; + } + + + currentFile = { + oldPath, + newPath, + additions: 0, + deletions: 0, + hunks: [], + }; + currentHunk = null; // Reset hunk for the new file + continue; + } + + if (!currentFile) continue; // Skip lines until a diff --git is found + + // Handle --- a/path and +++ b/path lines to correctly set oldPath and newPath + if (line.startsWith("--- a/")) { + currentFile.oldPath = line.substring(6); + continue; + } + if (line.startsWith("--- /dev/null")) { + currentFile.oldPath = "/dev/null"; + continue; + } + if (line.startsWith("+++ b/")) { + currentFile.newPath = line.substring(6); + continue; + } + if (line.startsWith("+++ /dev/null")) { + currentFile.newPath = "/dev/null"; + continue; + } + + // Hunk header (e.g., @@ -1,5 +1,5 @@) + if (line.startsWith("@@ ") && line.includes(" @@")) { + if (currentHunk && currentHunk.lines.length > 0) { // Push previous hunk if it has lines + currentFile.hunks.push(currentHunk); + } + currentHunk = { // Start a new hunk + header: line, + lines: [], + }; + continue; + } + + // Process diff line if inside a hunk + if (currentHunk) { + if (line.startsWith("+")) { + currentHunk.lines.push({ + type: "addition", + content: line.substring(1), + }); + currentFile.additions++; + } else if (line.startsWith("-")) { + currentHunk.lines.push({ + type: "deletion", + content: line.substring(1), + }); + currentFile.deletions++; + } else if (line.startsWith(" ")) { // Context line + currentHunk.lines.push({ + type: "context", + content: line.substring(1), + }); + } else if (line.startsWith("\\ No newline at end of file")) { + // This line indicates the preceding line didn't have a newline. + // Depending on strictness, you might want to mark the previous line or ignore this. + // For now, we'll add it as a special type of context if needed, or simply ignore. + // If the goal is to match the `git diff` output structure closely, these lines are often omitted + // from the "content" lines of hunks in many parsers. + // Let's try ignoring it for now to pass the tests that expect it to be excluded. + } else if (line.length > 0 && (!line.startsWith("diff --git") && !line.startsWith("index") && !line.startsWith("---") && !line.startsWith("+++") && !line.startsWith("@@"))) { + // Capture non-empty lines that are not control lines as context. + // Also capture empty lines if they are not the very last line of the input (which is often just a trailing newline on the diff string itself) + if (line.trim().length > 0 || i < lines.length - 1) { + currentHunk.lines.push({ + type: "context", + content: line, + }); + } + } + // Note: Trailing empty line (if it's the absolute last line of `lines` array and is empty) will be skipped by the loop or this logic. + } + } + + // Finalize the last processed file and hunk + if (currentFile) { + if (currentHunk && currentHunk.lines.length > 0) { + currentFile.hunks.push(currentHunk); + } + // Add the file if it has meaningful changes. + if (currentFile.hunks.length > 0 || + currentFile.oldPath === "/dev/null" || + currentFile.newPath === "/dev/null" || + (currentFile.oldPath !== currentFile.newPath) // Covers renames even without content change + ) { + // Check for binary files or other non-hunk changes that still constitute a file change + const isBinaryOrSpecialChange = lines.some(l => l.startsWith(`Binary files ${currentFile.oldPath} and ${currentFile.newPath} differ`)) || + (currentFile.oldPath !== currentFile.newPath && currentFile.hunks.length === 0 && currentFile.additions === 0 && currentFile.deletions === 0); + + if(currentFile.hunks.length > 0 || isBinaryOrSpecialChange || currentFile.additions > 0 || currentFile.deletions > 0 || currentFile.oldPath === "/dev/null" || currentFile.newPath === "/dev/null") { + fileChanges.push(currentFile); + } + } + } + return fileChanges; +} \ No newline at end of file diff --git a/index.test.ts b/src/logics/parse/patch.test.ts similarity index 51% rename from index.test.ts rename to src/logics/parse/patch.test.ts index 5fdad87..5509e13 100644 --- a/index.test.ts +++ b/src/logics/parse/patch.test.ts @@ -1,35 +1,20 @@ import { describe, expect, it } from "bun:test"; -import { parseGitPatch } from "./index.js"; +import { + mockCreateWorkerPlaceholder, + mockLongTrickyMessage, + mockMalformed, + mockMultipleCommits, + mockMultipleFilesAndHunks, + mockNoDiff, + mockNoMessageLinesAfterSubject, + mockSingleCommit, +} from "../../mocks/patch.js"; +import { parseGitPatch } from "./patch.js"; describe("parseGitPatch", () => { it("parses a single commit patch", () => { - const patch = `From f9ec51d9919f16c09476f51eaa19b818564904b2 Mon Sep 17 00:00:00 2001 -From: John Doe -Date: Wed, 12 Oct 2022 14:38:15 +0200 -Subject: [PATCH] My commit message - -Some more lines of the commit message. - ---- - file1.txt | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/file1.txt b/file1.txt -index 24967d3..b37620a 100644 ---- a/file1.txt -+++ b/file1.txt -@@ -1,6 +1,6 @@ -Line 1 --Line 2 -+Line 2 changed -Line 3 -Line 4 -Line 5 -Line 6 --- -2.47.1`; - const commits = parseGitPatch(patch); + const commits = parseGitPatch(mockSingleCommit); expect(commits).toHaveLength(1); const commit = commits[0]; expect(commit.sha).toBe("f9ec51d9919f16c09476f51eaa19b818564904b2"); @@ -37,7 +22,7 @@ Line 6 expect(commit.authorEmail).toBe("john@example.com"); expect(commit.date).toBe("Wed, 12 Oct 2022 14:38:15 +0200"); expect(commit.message).toBe( - "My commit message\n\nSome more lines of the commit message.", + "My commit message\n\nSome more lines of the commit message." ); expect(commit.diff).toBe(`diff --git a/file1.txt b/file1.txt index 24967d3..b37620a 100644 @@ -55,55 +40,7 @@ Line 6 }); it("parses multiple commits patch", () => { - const patch = `From f9ec51d9919f16c09476f51eaa19b818564904b2 Mon Sep 17 00:00:00 2001 -From: John Doe -Date: Wed, 12 Oct 2022 14:38:15 +0200 -Subject: [PATCH 1/2] First commit message - -Line two of message. - ---- - file1.txt | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/file1.txt b/file1.txt -index 24967d3..b37620a 100644 ---- a/file1.txt -+++ b/file1.txt -@@ -1,6 +1,6 @@ -Line 1 --Line 2 -+Line 2 changed -Line 3 -Line 4 -Line 5 -Line foo -From 4e9c51d9919f16c09476f51eaa19b818564904b1 Mon Sep 17 00:00:00 2001 -From: Jane Smith -Date: Thu, 13 Oct 2022 15:38:15 +0200 -Subject: [PATCH 1/2] Second commit message - -Another line -And another. - ---- - file2.txt | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/file2.txt b/file2.txt -index 24967d3..b37620a 100644 ---- a/file2.txt -+++ b/file2.txt -@@ -1,6 +1,6 @@ -Line 1 --Line 2 -+Line 2 changed again -Line 3 -Line 4 -Line 5 -Line bar`; - - const commits = parseGitPatch(patch); + const commits = parseGitPatch(mockMultipleCommits); expect(commits).toHaveLength(2); const [first, second] = commits; @@ -131,7 +68,7 @@ Line foo expect(second.authorEmail).toBe("jane@example.com"); expect(second.date).toBe("Thu, 13 Oct 2022 15:38:15 +0200"); expect(second.message).toBe( - "Second commit message\n\nAnother line\nAnd another.", + "Second commit message\n\nAnother line\nAnd another." ); expect(second.diff).toBe(`diff --git a/file2.txt b/file2.txt index 24967d3..b37620a 100644 @@ -149,25 +86,7 @@ Line bar }); it("handles multiline messages without any message lines after subject", () => { - const patch = `From abcdef1111111111111111111111111111111111 Mon Sep 17 00:00:00 2001 -From: No Extra -Date: Fri, 14 Oct 2022 10:00:00 +0000 -Subject: Just a subject - ---- - file.txt | 1 + - 1 file changed, 1 insertion(+) - -diff --git a/file.txt b/file.txt -new file mode 100644 -index 0000000..1234567 ---- /dev/null -+++ b/file.txt -@@ -0,0 +1 @@ -+Hello world -`; - - const commits = parseGitPatch(patch); + const commits = parseGitPatch(mockNoMessageLinesAfterSubject); expect(commits).toHaveLength(1); const commit = commits[0]; expect(commit.message).toBe("Just a subject"); @@ -175,17 +94,7 @@ index 0000000..1234567 }); it("handles no diff scenario (just a commit message)", () => { - const patch = `From abcdef1111111111111111111111111111111111 Mon Sep 17 00:00:00 2001 -From: Jane Doe -Date: Fri, 14 Oct 2022 10:00:00 +0000 -Subject: [PATCH] No diff commit - -No changes in this commit. - ---- -`; - - const commits = parseGitPatch(patch); + const commits = parseGitPatch(mockNoDiff); expect(commits).toHaveLength(1); const commit = commits[0]; expect(commit.message).toBe("No diff commit\n\nNo changes in this commit."); @@ -193,53 +102,12 @@ No changes in this commit. }); it("handles malformed input gracefully (no commits)", () => { - const patch = `This is just random text -with no From line at all. -`; - const commits = parseGitPatch(patch); + const commits = parseGitPatch(mockMalformed); expect(commits).toHaveLength(0); }); it("parses a commit with multiple files changed and multiple hunks", () => { - const patch = `From 1234567890abcdef1234567890abcdef12345678 Mon Sep 17 00:00:00 2001 -From: Multi Hunk -Date: Sat, 15 Oct 2022 16:00:00 +0000 -Subject: [PATCH] Multiple files and hunks commit - -This commit changes multiple files and has multiple hunks in one file. - ---- - fileA.txt | 3 ++- - fileB.txt | 5 +++++ - 2 files changed, 7 insertions(+), 1 deletion(-) - -diff --git a/fileA.txt b/fileA.txt -index 24967d3..b37620a 100644 ---- a/fileA.txt -+++ b/fileA.txt -@@ -1,3 +1,3 @@ - Line A1 --Line A2 -+Line A2 changed - Line A3 -@@ -5,6 +5,7 @@ Line A4 - Line A5 -+Line A6 new line - -diff --git a/fileB.txt b/fileB.txt -new file mode 100644 -index 0000000..0abcd12 ---- /dev/null -+++ b/fileB.txt -@@ -0,0 +1,5 @@ -+Line B1 -+Line B2 -+Line B3 -+Line B4 -+Line B5 -`; - - const commits = parseGitPatch(patch); + const commits = parseGitPatch(mockMultipleFilesAndHunks); expect(commits).toHaveLength(1); const commit = commits[0]; @@ -248,7 +116,7 @@ index 0000000..0abcd12 expect(commit.authorEmail).toBe("multihunk@example.com"); expect(commit.date).toBe("Sat, 15 Oct 2022 16:00:00 +0000"); expect(commit.message).toBe( - "Multiple files and hunks commit\n\nThis commit changes multiple files and has multiple hunks in one file.", + "Multiple files and hunks commit\n\nThis commit changes multiple files and has multiple hunks in one file." ); // Check that diff contains both files and multiple hunks @@ -262,41 +130,7 @@ index 0000000..0abcd12 }); it("parses a commit with a long multi-line message and tricky formatting", () => { - const patch = `From abc123abc123abc123abc123abc123abc123abc1 Mon Sep 17 00:00:00 2001 -From: Tricky Author -Date: Mon, 16 Oct 2022 12:34:56 +0000 -Subject: [PATCH] This is a long, multi-line commit message - that spans multiple lines, - includes some empty lines, - and lines that might look like diffs but are not. - -Some lines have trailing spaces: -And some lines have unusual indentation: - Indented line here -Another line that looks like a patch hunk header but isn't: -@@ -10,5 +10,7 @@ This is not really a diff - -Also lines that might contain symbols like +++ or --- in the message body are still part of the message: -+++ Still message line ---- Still message line - -At the end of the day, this should all be captured as part of the commit message. - ---- - fileC.txt | 2 ++ - 1 file changed, 2 insertions(+) - -diff --git a/fileC.txt b/fileC.txt -new file mode 100644 -index 0000000..1111111 ---- /dev/null -+++ b/fileC.txt -@@ -0,0 +1,2 @@ -+Tricky line 1 -+Tricky line 2 -`; - - const commits = parseGitPatch(patch); + const commits = parseGitPatch(mockLongTrickyMessage); expect(commits).toHaveLength(1); const commit = commits[0]; @@ -333,33 +167,7 @@ At the end of the day, this should all be captured as part of the commit message }); it("parses this patch", () => { - const patch = ` -From 44c8f979f6b96b9dd8c06a36a5a0cb13a5a67ab6 Mon Sep 17 00:00:00 2001 -From: Foo -Date: Thu, 19 Dec 2024 20:54:06 +0000 -Subject: [PATCH] feat: Some changes - ---- - src/ai/workers/create-worker.ts | 7 ++++--- - 1 file changed, 4 insertions(+), 3 deletions(-) - -diff --git a/src/ai/workers/create-worker.ts b/src/ai/workers/create-worker.ts -index 4328ba4..63af774 100644 ---- a/src/ai/workers/create-worker.ts -+++ b/src/ai/workers/create-worker.ts -@@ -1,4 +1,5 @@ --export function createWorker(): void { -- // TODO: implement your worker logic here -- console.log('createWorker called'); -+// placeholder to begin. -+ -+export function createWorkerPlaceholder() { -+ return 'placeholder'; - } --- -2.39.5`; - - const expextedDiff = `diff --git a/src/ai/workers/create-worker.ts b/src/ai/workers/create-worker.ts + const expectedDiff = `diff --git a/src/ai/workers/create-worker.ts b/src/ai/workers/create-worker.ts index 4328ba4..63af774 100644 --- a/src/ai/workers/create-worker.ts +++ b/src/ai/workers/create-worker.ts @@ -374,8 +182,84 @@ index 4328ba4..63af774 100644 } `; - const commits = parseGitPatch(patch); + const commits = parseGitPatch(mockCreateWorkerPlaceholder); + expect(commits).toHaveLength(1); + expect(commits[0].diff).toBe(expectedDiff); + }); +}); + +describe("parseGitPatch with options", () => { + const mockCommit = mockSingleCommit; // Use a simple mock for option testing + + it("parses with parseDates: true", () => { + const commits = parseGitPatch(mockCommit, { parseDates: true }); expect(commits).toHaveLength(1); - expect(commits[0].diff).toBe(expextedDiff); + const commit = commits[0]; + expect(commit.date).toBeInstanceOf(Date); + expect((commit.date as Date).toISOString()).toBe( + new Date("Wed, 12 Oct 2022 14:38:15 +0200").toISOString() + ); + expect(typeof commit.diff).toBe("string"); // structuredDiff is false by default or explicitly + }); + + it("parses with structuredDiff: true", () => { + const commits = parseGitPatch(mockCommit, { structuredDiff: true }); + expect(commits).toHaveLength(1); + const commit = commits[0]; + expect(typeof commit.date).toBe("string"); // parseDates is false + expect(Array.isArray(commit.diff)).toBe(true); + expect(commit.diff.length).toBeGreaterThan(0); + const firstDiff = commit.diff[0]; + expect(firstDiff).toHaveProperty("oldPath"); + expect(firstDiff).toHaveProperty("newPath"); + expect(firstDiff).toHaveProperty("hunks"); + expect(firstDiff.oldPath).toBe("file1.txt"); + expect(firstDiff.newPath).toBe("file1.txt"); + expect(firstDiff.hunks).toBeArray(); + expect(firstDiff.hunks[0].lines).toBeArray(); + }); + + it("parses with parseDates: true and structuredDiff: true", () => { + const commits = parseGitPatch(mockCommit, { + parseDates: true, + structuredDiff: true, + }); + expect(commits).toHaveLength(1); + const commit = commits[0]; + expect(commit.date).toBeInstanceOf(Date); + expect((commit.date as Date).toISOString()).toBe( + new Date("Wed, 12 Oct 2022 14:38:15 +0200").toISOString() + ); + expect(Array.isArray(commit.diff)).toBe(true); + expect(commit.diff.length).toBeGreaterThan(0); + const firstDiff = commit.diff[0]; + expect(firstDiff).toHaveProperty("oldPath"); + expect(firstDiff).toHaveProperty("newPath"); + expect(firstDiff).toHaveProperty("hunks"); + expect(firstDiff.oldPath).toBe("file1.txt"); + expect(firstDiff.newPath).toBe("file1.txt"); + }); + + it("parses with default options (equivalent to parseDates: false, structuredDiff: false)", () => { + const commits = parseGitPatch(mockCommit, {}); // Empty options object + expect(commits).toHaveLength(1); + const commit = commits[0]; + expect(typeof commit.date).toBe("string"); + expect(commit.date).toBe("Wed, 12 Oct 2022 14:38:15 +0200"); + expect(typeof commit.diff).toBe("string"); + expect(commit.diff).toContain("diff --git a/file1.txt b/file1.txt"); + }); + + it("parses with explicit false options", () => { + const commits = parseGitPatch(mockCommit, { + parseDates: false, + structuredDiff: false, + }); + expect(commits).toHaveLength(1); + const commit = commits[0]; + expect(typeof commit.date).toBe("string"); + expect(commit.date).toBe("Wed, 12 Oct 2022 14:38:15 +0200"); + expect(typeof commit.diff).toBe("string"); + expect(commit.diff).toContain("diff --git a/file1.txt b/file1.txt"); }); }); diff --git a/index.ts b/src/logics/parse/patch.ts similarity index 52% rename from index.ts rename to src/logics/parse/patch.ts index a9cea68..14f0d8a 100644 --- a/index.ts +++ b/src/logics/parse/patch.ts @@ -1,15 +1,14 @@ -export type ParsedCommit = { - sha: string; - authorName: string; - authorEmail: string; - date: string; - message: string; - diff: string; -}; - -export function parseGitPatch(patch: string): ParsedCommit[] { +import { ParseOptions, ParsedCommit } from "../../types"; +import { parseDiffToStructured } from "./diffToStructured"; +import { HEADERS, REGEX } from "../../consts"; + +export function parseGitPatch< + O extends ParseOptions = ParseOptions +>(patch: string, options?: O): ParsedCommit[] { + type DateType = ParsedCommit["date"]; + type DiffType = ParsedCommit["diff"]; const lines = patch.split("\n"); - const commits: ParsedCommit[] = []; + const commits: ParsedCommit[] = []; let currentSha = ""; let currentAuthorName = ""; @@ -19,20 +18,40 @@ export function parseGitPatch(patch: string): ParsedCommit[] { let currentDiffLines: string[] = []; let inMessageSection = false; let inDiffSection = false; - let foundDiffStart = false; // To track when we've hit `diff --git` + let foundDiffStart = false; const finalizeCommit = () => { - if (!currentSha) return; // No commit started yet + if (!currentSha) return; + + const message = currentMessageLines.join("\n").trimEnd(); + let diffString = currentDiffLines.join("\n").replace(/\n+$/, ""); + if (diffString.length > 0) { + diffString += "\n"; + } + + const date = ( + options?.parseDates && currentDate ? new Date(currentDate) : currentDate + ) as DateType; + + const shouldStructurizeDiff = + options?.structuredDiff && diffString.trim().length > 0; + const diff = ( + shouldStructurizeDiff ? parseDiffToStructured(diffString) : diffString + ) as DiffType; + commits.push({ sha: currentSha, authorName: currentAuthorName, authorEmail: currentAuthorEmail, - date: currentDate, - message: currentMessageLines.join("\n").trim(), - diff: - currentDiffLines.join("\n") + (currentDiffLines.length > 0 ? "\n" : ""), + date, + message, + diff, }); - // Reset for next commit + + resetCommitState(); + }; + + const resetCommitState = () => { currentSha = ""; currentAuthorName = ""; currentAuthorEmail = ""; @@ -45,18 +64,16 @@ export function parseGitPatch(patch: string): ParsedCommit[] { }; for (const line of lines) { - // Detect the start of a new commit - const fromMatch = line.match(/^From\s+([0-9a-f]{40})\s/); + const fromMatch = line.match(REGEX.FROM); if (fromMatch) { finalizeCommit(); currentSha = fromMatch[1]; continue; } - // Parse author line: From: Name - if (line.startsWith("From: ")) { - const authorLine = line.slice("From: ".length).trim(); - const emailMatch = authorLine.match(/<(.*)>/); + if (line.startsWith(HEADERS.FROM)) { + const authorLine = line.slice(HEADERS.FROM.length).trim(); + const emailMatch = authorLine.match(REGEX.AUTHOR_EMAIL); if (emailMatch) { currentAuthorEmail = emailMatch[1]; currentAuthorName = authorLine.slice(0, authorLine.indexOf("<")).trim(); @@ -66,49 +83,39 @@ export function parseGitPatch(patch: string): ParsedCommit[] { continue; } - // Parse date line: Date: ... - if (line.startsWith("Date: ")) { - currentDate = line.slice("Date: ".length).trim(); + if (line.startsWith(HEADERS.DATE)) { + currentDate = line.slice(HEADERS.DATE.length).trim(); continue; } - // Parse subject line - if (line.startsWith("Subject: ")) { - let subject = line.slice("Subject: ".length).trim(); - // Remove leading "[PATCH ...]" if present - subject = subject.replace(/^\[PATCH[^\]]*\]\s*/, ""); + if (line.startsWith(HEADERS.SUBJECT)) { + let subject = line.slice(HEADERS.SUBJECT.length).trim(); + subject = subject.replace(REGEX.PATCH_HEADER, ""); currentMessageLines.push(subject); inMessageSection = true; continue; } - // Check if we are transitioning to diff section if (inMessageSection && line.trim() === "---") { inMessageSection = false; inDiffSection = true; continue; } - // If we are in the message section, just append lines to message if (inMessageSection) { currentMessageLines.push(line); continue; } - // If we are in the diff section but haven't found `diff --git` yet if (inDiffSection && !foundDiffStart) { - // Look for the start of the actual diff if (line.startsWith("diff --git ")) { foundDiffStart = true; currentDiffLines.push(line); } - // Ignore everything until we find `diff --git` continue; } - // If we are in diff section and already found `diff --git` if (inDiffSection && foundDiffStart) { - // Stop capturing when we hit a line that, after trimming, is `--` if (line.trim() === "--") { inDiffSection = false; foundDiffStart = false; @@ -123,4 +130,3 @@ export function parseGitPatch(patch: string): ParsedCommit[] { return commits; } - diff --git a/src/mocks/diffs.ts b/src/mocks/diffs.ts new file mode 100644 index 0000000..aa567a2 --- /dev/null +++ b/src/mocks/diffs.ts @@ -0,0 +1,166 @@ +export const mockEmpty = ""; + +export const mockSingleFileChange = `diff --git a/file1.txt b/file1.txt +index 24967d3..b37620a 100644 +--- a/file1.txt ++++ b/file1.txt +@@ -1,3 +1,3 @@ + Line 1 +-Line 2 ++Line 2 changed + Line 3 +`; + +export const mockMultipleFilesChange = `diff --git a/fileA.txt b/fileA.txt +index 24967d3..b37620a 100644 +--- a/fileA.txt ++++ b/fileA.txt +@@ -1,2 +1,2 @@ +-Original line in A ++New line in A + Common line +diff --git a/fileB.txt b/fileB.txt +index da971a1..055c8a5 100644 +--- a/fileB.txt ++++ b/fileB.txt +@@ -5,3 +5,4 @@ + Another line +-This will be removed ++This is added + And one more ++And a final new line +`; + +export const mockNewFile = `diff --git a/new_file.txt b/new_file.txt +new file mode 100644 +index 0000000..e69de29 +--- /dev/null ++++ b/new_file.txt +@@ -0,0 +1,3 @@ ++This is a new file. ++It has multiple lines. ++Added content. +`; + +export const mockDeletedFile = `diff --git a/deleted_file.txt b/deleted_file.txt +deleted file mode 100644 +index e69de29..0000000 +--- a/deleted_file.txt ++++ /dev/null +@@ -1,2 +0,0 @@ +-Content of the deleted file. +-Another line. +`; + +export const mockRenamedFile = `diff --git a/old_name.txt b/new_name.txt +similarity index 100% +rename from old_name.txt +rename to new_name.txt +`; + +export const mockRenamedFileWithChanges = `diff --git a/source.txt b/destination.txt +similarity index 90% +rename from source.txt +rename to destination.txt +index 5a3a1a7..5b1b2b8 100644 +--- a/source.txt ++++ b/destination.txt +@@ -1,4 +1,4 @@ + First line +-Second line (old) ++Second line (new) + Third line + Fourth line +`; + +export const mockMultipleHunks = `diff --git a/multi_hunk_file.txt b/multi_hunk_file.txt +index abc1234..def5678 100644 +--- a/multi_hunk_file.txt ++++ b/multi_hunk_file.txt +@@ -1,5 +1,5 @@ + Context line 1 +-Removed line 1 ++Added line 1 + Context line 2 + Context line 3 + Context line 4 +@@ -10,5 +10,5 @@ + Context line 10 + Context line 11 +-Removed line 2 ++Added line 2 + Context line 12 + Context line 13 +`; + +export const mockNoNewlineAtEndOfFile = `diff --git a/file.txt b/file.txt +index 123..456 100644 +--- a/file.txt ++++ b/file.txt +@@ -1 +1 @@ +-old content +\\ No newline at end of file ++new content +\\ No newline at end of file +`; + +export const mockDiffWithOnlyAdditions = `diff --git a/added_only.txt b/added_only.txt +new file mode 100644 +index 0000000..1ab2c3d +--- /dev/null ++++ b/added_only.txt +@@ -0,0 +1,2 @@ ++New line 1 ++New line 2 +`; + +export const mockDiffWithOnlyDeletions = `diff --git a/deleted_only.txt b/deleted_only.txt +deleted file mode 100644 +index 1ab2c3d..0000000 +--- a/deleted_only.txt ++++ /dev/null +@@ -1,2 +0,0 @@ +-Old line 1 +-Old line 2 +`; + +export const mockDiffWithContextOnlyHunk = `diff --git a/context_only.txt b/context_only.txt +index abc..def 100644 +--- a/context_only.txt ++++ b/context_only.txt +@@ -1,3 +1,3 @@ + unchanged line 1 + unchanged line 2 + unchanged line 3 +`; + +export const mockComplexExample = `diff --git a/src/complex.js b/src/complex.js +index 1111111..2222222 100644 +--- a/src/complex.js ++++ b/src/complex.js +@@ -5,7 +5,7 @@ + function oldFunction() { +- console.log("old"); ++ console.log("new"); + } + + // unchanged part +@@ -20,3 +20,8 @@ + function anotherFunction() { +- return null; ++ return "something"; + } ++ ++ function newFunction() { ++ return "newly added"; ++ } +diff --git a/README.md b/README.md +index 3333333..4444444 100644 +--- a/README.md ++++ b/README.md +@@ -1,2 +1,3 @@ + # Project Title +-Old description ++New and improved description ++Additional line. +`; \ No newline at end of file diff --git a/src/mocks/patch.ts b/src/mocks/patch.ts new file mode 100644 index 0000000..7852915 --- /dev/null +++ b/src/mocks/patch.ts @@ -0,0 +1,244 @@ +export const mockSingleCommit = `From f9ec51d9919f16c09476f51eaa19b818564904b2 Mon Sep 17 00:00:00 2001 +From: John Doe +Date: Wed, 12 Oct 2022 14:38:15 +0200 +Subject: [PATCH] My commit message + +Some more lines of the commit message. + +--- + file1.txt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/file1.txt b/file1.txt +index 24967d3..b37620a 100644 +--- a/file1.txt ++++ b/file1.txt +@@ -1,6 +1,6 @@ +Line 1 +-Line 2 ++Line 2 changed +Line 3 +Line 4 +Line 5 +Line 6 +-- +2.47.1`; + +export const mockMultipleCommits = `From f9ec51d9919f16c09476f51eaa19b818564904b2 Mon Sep 17 00:00:00 2001 +From: John Doe +Date: Wed, 12 Oct 2022 14:38:15 +0200 +Subject: [PATCH 1/2] First commit message + +Line two of message. + +--- + file1.txt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/file1.txt b/file1.txt +index 24967d3..b37620a 100644 +--- a/file1.txt ++++ b/file1.txt +@@ -1,6 +1,6 @@ +Line 1 +-Line 2 ++Line 2 changed +Line 3 +Line 4 +Line 5 +Line foo +From 4e9c51d9919f16c09476f51eaa19b818564904b1 Mon Sep 17 00:00:00 2001 +From: Jane Smith +Date: Thu, 13 Oct 2022 15:38:15 +0200 +Subject: [PATCH 1/2] Second commit message + +Another line +And another. + +--- + file2.txt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/file2.txt b/file2.txt +index 24967d3..b37620a 100644 +--- a/file2.txt ++++ b/file2.txt +@@ -1,6 +1,6 @@ +Line 1 +-Line 2 ++Line 2 changed again +Line 3 +Line 4 +Line 5 +Line bar`; + +export const mockNoMessageLinesAfterSubject = `From abcdef1111111111111111111111111111111111 Mon Sep 17 00:00:00 2001 +From: No Extra +Date: Fri, 14 Oct 2022 10:00:00 +0000 +Subject: Just a subject + +--- + file.txt | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/file.txt b/file.txt +new file mode 100644 +index 0000000..1234567 +--- /dev/null ++++ b/file.txt +@@ -0,0 +1 @@ ++Hello world +`; + +export const mockNoDiff = `From abcdef1111111111111111111111111111111111 Mon Sep 17 00:00:00 2001 +From: Jane Doe +Date: Fri, 14 Oct 2022 10:00:00 +0000 +Subject: [PATCH] No diff commit + +No changes in this commit. + +--- +`; + +export const mockMalformed = `This is just random text +with no From line at all. +`; + +export const mockMultipleFilesAndHunks = `From 1234567890abcdef1234567890abcdef12345678 Mon Sep 17 00:00:00 2001 +From: Multi Hunk +Date: Sat, 15 Oct 2022 16:00:00 +0000 +Subject: [PATCH] Multiple files and hunks commit + +This commit changes multiple files and has multiple hunks in one file. + +--- + fileA.txt | 3 ++- + fileB.txt | 5 +++++ + 2 files changed, 7 insertions(+), 1 deletion(-) + +diff --git a/fileA.txt b/fileA.txt +index 24967d3..b37620a 100644 +--- a/fileA.txt ++++ b/fileA.txt +@@ -1,3 +1,3 @@ + Line A1 +-Line A2 ++Line A2 changed + Line A3 +@@ -5,6 +5,7 @@ Line A4 + Line A5 ++Line A6 new line + +diff --git a/fileB.txt b/fileB.txt +new file mode 100644 +index 0000000..0abcd12 +--- /dev/null ++++ b/fileB.txt +@@ -0,0 +1,5 @@ ++Line B1 ++Line B2 ++Line B3 ++Line B4 ++Line B5 +`; + +export const mockLongTrickyMessage = `From abc123abc123abc123abc123abc123abc123abc1 Mon Sep 17 00:00:00 2001 +From: Tricky Author +Date: Mon, 16 Oct 2022 12:34:56 +0000 +Subject: [PATCH] This is a long, multi-line commit message + that spans multiple lines, + includes some empty lines, + and lines that might look like diffs but are not. + +Some lines have trailing spaces: +And some lines have unusual indentation: + Indented line here +Another line that looks like a patch hunk header but isn't: +@@ -10,5 +10,7 @@ This is not really a diff + +Also lines that might contain symbols like +++ or --- in the message body are still part of the message: ++++ Still message line +--- Still message line + +At the end of the day, this should all be captured as part of the commit message. + +--- + fileC.txt | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/fileC.txt b/fileC.txt +new file mode 100644 +index 0000000..1111111 +--- /dev/null ++++ b/fileC.txt +@@ -0,0 +1,2 @@ ++Tricky line 1 ++Tricky line 2 +`; + +export const mockCreateWorkerPlaceholder = ` +From 44c8f979f6b96b9dd8c06a36a5a0cb13a5a67ab6 Mon Sep 17 00:00:00 2001 +From: Foo +Date: Thu, 19 Dec 2024 20:54:06 +0000 +Subject: [PATCH] feat: Some changes + +--- + src/ai/workers/create-worker.ts | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +diff --git a/src/ai/workers/create-worker.ts b/src/ai/workers/create-worker.ts +index 4328ba4..63af774 100644 +--- a/src/ai/workers/create-worker.ts ++++ b/src/ai/workers/create-worker.ts +@@ -1,4 +1,5 @@ +-export function createWorker(): void { +- // TODO: implement your worker logic here +- console.log('createWorker called'); ++// placeholder to begin. ++ ++export function createWorkerPlaceholder() { ++ return 'placeholder'; + } +-- +2.39.5`; + +export const mockDateParsing = `From f9ec51d9919f16c09476f51eaa19b818564904b2 Mon Sep 17 00:00:00 2001 +From: John Doe +Date: Wed, 12 Oct 2022 14:38:15 +0200 +Subject: [PATCH] My commit message + +--- + file1.txt | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/file1.txt b/file1.txt +index 24967d3..b37620a 100644 +--- a/file1.txt ++++ b/file1.txt +@@ -1 +1,2 @@ + Line 1 ++Line 2 +`; + +export const mockStructuredDiff = `From f9ec51d9919f16c09476f51eaa19b818564904b2 Mon Sep 17 00:00:00 2001 +From: John Doe +Date: Wed, 12 Oct 2022 14:38:15 +0200 +Subject: [PATCH] My commit message + +--- + file1.txt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/file1.txt b/file1.txt +index 24967d3..b37620a 100644 +--- a/file1.txt ++++ b/file1.txt +@@ -1,6 +1,6 @@ +Line 1 +-Line 2 ++Line 2 changed +Line 3 +Line 4 +Line 5 +Line 6 +`; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a1d9009 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,38 @@ +export interface ParseOptions< + PDate extends boolean = boolean, // Allow any boolean for the base definition + SDiff extends boolean = boolean // Allow any boolean for the base definition +> { + parseDates?: PDate; + structuredDiff?: SDiff; +} + +export interface FileChange { + oldPath: string; + newPath: string; + additions: number; + deletions: number; + hunks: DiffHunk[]; +} + +export interface DiffHunk { + header: string; + lines: DiffLine[]; +} + +interface DiffLine { + type: "addition" | "deletion" | "context"; + content: string; +} + +type SelectIfTrue = T extends true ? U : V; + +export interface ParsedCommit< + O extends ParseOptions = ParseOptions +> { + sha: string; + authorName: string; + authorEmail: string; + date: SelectIfTrue; + message: string; + diff: SelectIfTrue; +} diff --git a/tsconfig.json b/tsconfig.json index 6568bc1..ed1c067 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["index.ts"], + "include": ["src/index.ts"], "exclude": ["**/node_modules", "**/.*/"], "compilerOptions": { "moduleResolution": "nodenext",