From 40900db843c36adb05f5e4e52f1c197acd186a03 Mon Sep 17 00:00:00 2001 From: zenyr Date: Sat, 17 May 2025 17:08:19 +0900 Subject: [PATCH 1/9] refactor(parser): Move source files to src directory Moved core parsing logic files (index.ts and index.test.ts) to the src/logics/parse/ directory. Created new index files in src/ and src/logics/ directories for re-exporting. Added src/types.ts file for type definitions. --- src/index.ts | 2 + src/logics/index.ts | 1 + .../logics/parse/patch.test.ts | 12 +-- index.ts => src/logics/parse/patch.ts | 92 ++++++++++++++----- src/types.ts | 31 +++++++ 5 files changed, 109 insertions(+), 29 deletions(-) create mode 100644 src/index.ts create mode 100644 src/logics/index.ts rename index.test.ts => src/logics/parse/patch.test.ts (97%) rename index.ts => src/logics/parse/patch.ts (54%) create mode 100644 src/types.ts 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..5fada93 --- /dev/null +++ b/src/logics/index.ts @@ -0,0 +1 @@ +export * from "./parse/gitPatch"; \ No newline at end of file diff --git a/index.test.ts b/src/logics/parse/patch.test.ts similarity index 97% rename from index.test.ts rename to src/logics/parse/patch.test.ts index 5fdad87..f0c30a1 100644 --- a/index.test.ts +++ b/src/logics/parse/patch.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test"; -import { parseGitPatch } from "./index.js"; +import { parseGitPatch } from "./patch.js"; describe("parseGitPatch", () => { it("parses a single commit patch", () => { @@ -37,7 +37,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 @@ -131,7 +131,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 @@ -248,7 +248,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 @@ -359,7 +359,7 @@ index 4328ba4..63af774 100644 -- 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 @@ -376,6 +376,6 @@ index 4328ba4..63af774 100644 const commits = parseGitPatch(patch); expect(commits).toHaveLength(1); - expect(commits[0].diff).toBe(expextedDiff); + expect(commits[0].diff).toBe(expectedDiff); }); }); diff --git a/index.ts b/src/logics/parse/patch.ts similarity index 54% rename from index.ts rename to src/logics/parse/patch.ts index a9cea68..9b478d3 100644 --- a/index.ts +++ b/src/logics/parse/patch.ts @@ -1,13 +1,22 @@ -export type ParsedCommit = { - sha: string; - authorName: string; - authorEmail: string; - date: string; - message: string; - diff: string; +import { ParseOptions, ParsedCommit, FileChange } from "../../types"; +import { parseDiffToStructured } from "./diffToStructured"; + +const HEADERS = { + FROM: "From: ", + DATE: "Date: ", + SUBJECT: "Subject: ", +}; + +const REGEX = { + FROM: /^From\s+([0-9a-f]{40})\s/, + AUTHOR_EMAIL: /<(.*)>/, + PATCH_HEADER: /^\[PATCH[^\]]*\]\s*/, }; -export function parseGitPatch(patch: string): ParsedCommit[] { +export function parseGitPatch( + patch: string, + options: ParseOptions = {} +): ParsedCommit[] { const lines = patch.split("\n"); const commits: ParsedCommit[] = []; @@ -23,16 +32,51 @@ export function parseGitPatch(patch: string): ParsedCommit[] { const finalizeCommit = () => { if (!currentSha) return; // No commit started yet + + const message = currentMessageLines.join("\n").trimEnd(); // trimEnd to preserve leading/internal newlines + let date: string | Date = currentDate; + // Join lines, then trim trailing newlines that might have been added if the original patch ended with multiple blank lines. + // Then, ensure a single trailing newline if there's content. + let diffString = currentDiffLines.join("\n").replace(/\n+$/, ""); + if (diffString.length > 0) { + diffString += "\n"; + } + + let diff: string | FileChange[] = diffString; + + // Process based on options + if (options.parseDates && currentDate) { + try { + date = new Date(currentDate); + } catch (e) { + // Keep original string if date parsing fails + console.warn(`Failed to parse date: ${currentDate}`); + } + } + + // Process structured diff + if ( + options.structuredDiff && + typeof diff === "string" && + diff.trim().length > 0 + ) { + diff = parseDiffToStructured(diff); + } + 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 = ""; @@ -46,7 +90,7 @@ 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]; @@ -54,9 +98,9 @@ export function parseGitPatch(patch: string): ParsedCommit[] { } // 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(); @@ -67,16 +111,16 @@ export function parseGitPatch(patch: string): ParsedCommit[] { } // 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(); + if (line.startsWith(HEADERS.SUBJECT)) { + let subject = line.slice(HEADERS.SUBJECT.length).trim(); // Remove leading "[PATCH ...]" if present - subject = subject.replace(/^\[PATCH[^\]]*\]\s*/, ""); + subject = subject.replace(REGEX.PATCH_HEADER, ""); currentMessageLines.push(subject); inMessageSection = true; continue; @@ -91,6 +135,9 @@ export function parseGitPatch(patch: string): ParsedCommit[] { // If we are in the message section, just append lines to message if (inMessageSection) { + // For subject lines, they are already pushed. + // For subsequent message lines, they might have leading spaces from the patch format. + // We should preserve these as they are part of the message. currentMessageLines.push(line); continue; } @@ -122,5 +169,4 @@ export function parseGitPatch(patch: string): ParsedCommit[] { finalizeCommit(); return commits; -} - +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9674229 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,31 @@ +export interface ParseOptions { + parseDates?: boolean; + structuredDiff?: boolean; +} + +export interface FileChange { + oldPath: string; + newPath: string; + additions: number; + deletions: number; + hunks: DiffHunk[]; +} + +export interface DiffHunk { + header: string; + lines: DiffLine[]; +} + +export interface DiffLine { + type: "addition" | "deletion" | "context"; + content: string; +} + +export interface ParsedCommit { + sha: string; + authorName: string; + authorEmail: string; + date: string | Date; + message: string; + diff: string | FileChange[]; +} \ No newline at end of file From 20d72cc2efa8f36750519a15f66470c9a27ed8cf Mon Sep 17 00:00:00 2001 From: zenyr Date: Sat, 17 May 2025 17:14:03 +0900 Subject: [PATCH 2/9] refactor(parse/patch): Extracts patch mocks to separate file Move mock data to a separate `src/mocks/patch.ts` file to improve readability and organization of the `parseGitPatch` test. This change also includes updating the include paths in `tsconfig.json` for bundling. --- bun.lockb | Bin 111638 -> 111630 bytes src/logics/parse/patch.test.ts | 228 +++--------------------------- src/mocks/patch.ts | 244 +++++++++++++++++++++++++++++++++ tsconfig.json | 2 +- 4 files changed, 263 insertions(+), 211 deletions(-) create mode 100644 src/mocks/patch.ts diff --git a/bun.lockb b/bun.lockb index ca36d77bb333501a9c13f18b1ec7b17d0da1ef43..96cbee0a24911781065be446f6c42b2290c91bdf 100755 GIT binary patch delta 990 zcmYk)OGs2<6u|L0qJv{i4^7n4L@j4bXPj~JRX)bZ2W47Tn;@O31~XG|d|;bG+O&!w z=-akQDl4?K=|N>}#30BPQM)#QEduSeDWd;ruHc0q-}%mW?)~ol?zy&Tzr1L_(kAD} z+GpBj(v=!?i%qX&Z+uLtG3jmzm{zwe<*kqBY(FT_(5Z!1?k90#?0At3Cggo5&PF9` zy6nX_D_2D#!7LX`Z~ARg1qfPY<;3G=phVL0?paHFw3@gQ6IO1omYW+Ta<%+1DGv@< zWkF(n5#6xedTImlFDrNO|E+xWFn)6Y&fx%)CcjQg4<$lIy6lj^Mh<^2*$W za}qDeyGyVGrEtM(#3zBeMk<@2C#CL+>^0nh1>Nl1c delta 969 zcmYk*NlcSL7{+nl1_+>43M#UR6{M}wmQoQ>gjQrz0Ymg+qO@Y8Y$j4QaxpO`9u0|! zbMk7y1#y9j#_fcdc+fxq-e|bCHt(p_)w3}INTC6A-60zGm?4A0*g{%$8sxMWGspO9*NtWUl|vn zhTm-TK76yXS~4;78>PSPDp4sy8qrtdnw5pEY%}xgR<^^+YM75(StFUvT&a*t)!&J{ zXt$Py82_-c4l4^YpN8I1=fABnue7YxllSSAz8RCKG0(i>lCYWgN=atOdMBgC;ggxR zGb|p)G4!Gj{n&-w*aN)}Jy*}!hB|CVJsO~o9X3wC1I!j5Iomzxa-a*%5d z`sIASi`#Q!11g|Ttxx1PKm8J{-^t1*G($f@CGwDu0u*BfB5XN?ZcN}d?xdK$N;y#6 z#8}@%3pU{t>rc=RA#OfZN};!vs0CXPKpEDf9P99!rSEYMca0|?4QYpod(2=!YSNA} zJYvQI(w(!KVG-7#1g}y|L6wx-XV^H47kFvftHdAu#qc+TWq;_k(zLR)BER4xu|7N8!@xB(v_igrY%vrRbu63A0x?M*G7|l@{iYh jJbp4dFyM>F22aO^%&mxc%$tZbX706?olQ8S^116Th7yrE diff --git a/src/logics/parse/patch.test.ts b/src/logics/parse/patch.test.ts index f0c30a1..7454438 100644 --- a/src/logics/parse/patch.test.ts +++ b/src/logics/parse/patch.test.ts @@ -1,35 +1,20 @@ import { describe, expect, it } from "bun:test"; import { parseGitPatch } from "./patch.js"; +import { + mockSingleCommit, + mockMultipleCommits, + mockNoMessageLinesAfterSubject, + mockNoDiff, + mockMalformed, + mockMultipleFilesAndHunks, + mockLongTrickyMessage, + mockCreateWorkerPlaceholder, +} from "../../mocks/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"); @@ -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; @@ -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]; @@ -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,32 +167,6 @@ 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 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 @@ -374,7 +182,7 @@ index 4328ba4..63af774 100644 } `; - const commits = parseGitPatch(patch); + const commits = parseGitPatch(mockCreateWorkerPlaceholder); expect(commits).toHaveLength(1); expect(commits[0].diff).toBe(expectedDiff); }); 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/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", From 2ea4bb4453594b73a05ddb9a804c6a7df6f0da2e Mon Sep 17 00:00:00 2001 From: zenyr Date: Sat, 17 May 2025 17:24:33 +0900 Subject: [PATCH 3/9] feat(parse): Parse diff string to structured data Implements a function to parse Git diff strings into a structured array of objects, including file changes, each hunk, and line types (additions, deletions, context). Includes a comprehensive test suite that covers various diff scenarios, such as single file changes, multiple file changes, file additions/deletions/renames, multiple hunks, and indicators for lines without trailing newlines. --- src/logics/parse/diffToStructured.test.ts | 405 ++++++++++++++++++++++ src/logics/parse/diffToStructured.ts | 142 ++++++++ src/mocks/diffs.ts | 166 +++++++++ 3 files changed, 713 insertions(+) create mode 100644 src/logics/parse/diffToStructured.test.ts create mode 100644 src/logics/parse/diffToStructured.ts create mode 100644 src/mocks/diffs.ts 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/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 From d3a6d9572b5b3986166bb360134499ed49dbe8af Mon Sep 17 00:00:00 2001 From: Jinhyeok Lee Date: Sat, 17 May 2025 17:49:13 +0900 Subject: [PATCH 4/9] refactor(parser): Enhance type safety and modularity in patch parsing Refactors the git patch parsing logic to improve type safety and code organization. Key improvements include: - Centralizing parsing-related constants. - Introducing generic types for ParseOptions and ParsedCommit to allow dynamic type inference based on options, leading to more precise type checking. - Updating the parseGitPatch function to leverage these enhanced types for better accuracy. --- src/consts.ts | 11 ++++++ src/logics/index.ts | 2 +- src/logics/parse/patch.ts | 83 +++++++++++---------------------------- src/types.ts | 21 ++++++---- 4 files changed, 49 insertions(+), 68 deletions(-) create mode 100644 src/consts.ts 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/logics/index.ts b/src/logics/index.ts index 5fada93..322b9a9 100644 --- a/src/logics/index.ts +++ b/src/logics/index.ts @@ -1 +1 @@ -export * from "./parse/gitPatch"; \ No newline at end of file +export * from "./parse/patch"; \ No newline at end of file diff --git a/src/logics/parse/patch.ts b/src/logics/parse/patch.ts index 9b478d3..167066c 100644 --- a/src/logics/parse/patch.ts +++ b/src/logics/parse/patch.ts @@ -1,24 +1,17 @@ -import { ParseOptions, ParsedCommit, FileChange } from "../../types"; +import { ParseOptions, ParsedCommit } from "../../types"; import { parseDiffToStructured } from "./diffToStructured"; +import { HEADERS, REGEX } from "../../consts"; -const HEADERS = { - FROM: "From: ", - DATE: "Date: ", - SUBJECT: "Subject: ", -}; - -const REGEX = { - FROM: /^From\s+([0-9a-f]{40})\s/, - AUTHOR_EMAIL: /<(.*)>/, - PATCH_HEADER: /^\[PATCH[^\]]*\]\s*/, -}; - -export function parseGitPatch( +export function parseGitPatch< + O extends ParseOptions = ParseOptions +>( patch: string, - options: ParseOptions = {} -): ParsedCommit[] { + options: O = { parseDates: false, structuredDiff: false } as 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 = ""; @@ -28,40 +21,26 @@ export function parseGitPatch( 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(); // trimEnd to preserve leading/internal newlines - let date: string | Date = currentDate; - // Join lines, then trim trailing newlines that might have been added if the original patch ended with multiple blank lines. - // Then, ensure a single trailing newline if there's content. + const message = currentMessageLines.join("\n").trimEnd(); let diffString = currentDiffLines.join("\n").replace(/\n+$/, ""); if (diffString.length > 0) { diffString += "\n"; } - - let diff: string | FileChange[] = diffString; - - // Process based on options - if (options.parseDates && currentDate) { - try { - date = new Date(currentDate); - } catch (e) { - // Keep original string if date parsing fails - console.warn(`Failed to parse date: ${currentDate}`); - } - } - // Process structured diff - if ( - options.structuredDiff && - typeof diff === "string" && - diff.trim().length > 0 - ) { - diff = parseDiffToStructured(diff); - } + 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, @@ -72,7 +51,6 @@ export function parseGitPatch( diff, }); - // Reset for next commit resetCommitState(); }; @@ -89,7 +67,6 @@ export function parseGitPatch( }; for (const line of lines) { - // Detect the start of a new commit const fromMatch = line.match(REGEX.FROM); if (fromMatch) { finalizeCommit(); @@ -97,7 +74,6 @@ export function parseGitPatch( continue; } - // Parse author line: From: Name if (line.startsWith(HEADERS.FROM)) { const authorLine = line.slice(HEADERS.FROM.length).trim(); const emailMatch = authorLine.match(REGEX.AUTHOR_EMAIL); @@ -110,52 +86,39 @@ export function parseGitPatch( continue; } - // Parse date line: Date: ... if (line.startsWith(HEADERS.DATE)) { currentDate = line.slice(HEADERS.DATE.length).trim(); continue; } - // Parse subject line if (line.startsWith(HEADERS.SUBJECT)) { let subject = line.slice(HEADERS.SUBJECT.length).trim(); - // Remove leading "[PATCH ...]" if present 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) { - // For subject lines, they are already pushed. - // For subsequent message lines, they might have leading spaces from the patch format. - // We should preserve these as they are part of the message. 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; @@ -169,4 +132,4 @@ export function parseGitPatch( finalizeCommit(); return commits; -} \ No newline at end of file +} diff --git a/src/types.ts b/src/types.ts index 9674229..a58a95d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,9 @@ -export interface ParseOptions { - parseDates?: boolean; - structuredDiff?: boolean; +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 { @@ -21,11 +24,15 @@ export interface DiffLine { content: string; } -export interface ParsedCommit { +type SelectIfTrue = T extends true ? U : V; + +export interface ParsedCommit< + O extends ParseOptions = ParseOptions +> { sha: string; authorName: string; authorEmail: string; - date: string | Date; + date: SelectIfTrue; message: string; - diff: string | FileChange[]; -} \ No newline at end of file + diff: SelectIfTrue; +} From 666eeb5af094f21f049d5ffaa7c08b24b809005d Mon Sep 17 00:00:00 2001 From: Jinhyeok Lee Date: Sat, 17 May 2025 18:13:55 +0900 Subject: [PATCH 5/9] feat(structured): improve patch parsing flexibility and type safety Enhanced the patch parsing logic to provide more flexible configuration options. This includes adjustments to how dates and diff structures are processed based on user-defined settings. Corresponding test suites have been expanded to cover these new option variations, ensuring robustness. Type definitions related to parsed commit data have also been updated to accurately represent the structured diff output when that option is enabled. --- src/logics/parse/patch.test.ts | 76 ++++++++++++++++++++++++++++++++++ src/logics/parse/patch.ts | 11 ++--- src/types.ts | 4 +- 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/src/logics/parse/patch.test.ts b/src/logics/parse/patch.test.ts index 7454438..764e85b 100644 --- a/src/logics/parse/patch.test.ts +++ b/src/logics/parse/patch.test.ts @@ -187,3 +187,79 @@ index 4328ba4..63af774 100644 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); + 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/src/logics/parse/patch.ts b/src/logics/parse/patch.ts index 167066c..14f0d8a 100644 --- a/src/logics/parse/patch.ts +++ b/src/logics/parse/patch.ts @@ -3,11 +3,8 @@ import { parseDiffToStructured } from "./diffToStructured"; import { HEADERS, REGEX } from "../../consts"; export function parseGitPatch< - O extends ParseOptions = ParseOptions ->( - patch: string, - options: O = { parseDates: false, structuredDiff: false } as O -): ParsedCommit[] { + O extends ParseOptions = ParseOptions +>(patch: string, options?: O): ParsedCommit[] { type DateType = ParsedCommit["date"]; type DiffType = ParsedCommit["diff"]; const lines = patch.split("\n"); @@ -33,11 +30,11 @@ export function parseGitPatch< } const date = ( - options.parseDates && currentDate ? new Date(currentDate) : currentDate + options?.parseDates && currentDate ? new Date(currentDate) : currentDate ) as DateType; const shouldStructurizeDiff = - options.structuredDiff && diffString.trim().length > 0; + options?.structuredDiff && diffString.trim().length > 0; const diff = ( shouldStructurizeDiff ? parseDiffToStructured(diffString) : diffString ) as DiffType; diff --git a/src/types.ts b/src/types.ts index a58a95d..a1d9009 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,7 +19,7 @@ export interface DiffHunk { lines: DiffLine[]; } -export interface DiffLine { +interface DiffLine { type: "addition" | "deletion" | "context"; content: string; } @@ -34,5 +34,5 @@ export interface ParsedCommit< authorEmail: string; date: SelectIfTrue; message: string; - diff: SelectIfTrue; + diff: SelectIfTrue; } From 317c061f504bce58d68af66860fbd83161aa824b Mon Sep 17 00:00:00 2001 From: Jinhyeok Lee Date: Sat, 17 May 2025 18:44:49 +0900 Subject: [PATCH 6/9] refactor(script): Improve build script compatibility by replacing rm -rf with a Node.js script This commit replaces the `rm -rf dist` command in the build script with a cross-platform Node.js script (`scripts/cleanDist.ts`). This enhances compatibility, particularly for Windows environments where `rm -rf` is not natively available. The new script uses `node:fs/promises` to delete the 'dist' directory. --- package.json | 2 +- scripts/cleanDist.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 scripts/cleanDist.ts 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(); From 4cb8a65b3ac9132794bfc107f1c56fc927355c5b Mon Sep 17 00:00:00 2001 From: Jinhyeok Lee Date: Sat, 17 May 2025 19:11:39 +0900 Subject: [PATCH 7/9] style(test): Adjust import order in patch.test.ts This commit updates the import order in the `patch.test.ts` file. This change is likely due to automated linting or code style adjustments and does not affect the logic of the tests. --- src/logics/parse/patch.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/logics/parse/patch.test.ts b/src/logics/parse/patch.test.ts index 764e85b..5509e13 100644 --- a/src/logics/parse/patch.test.ts +++ b/src/logics/parse/patch.test.ts @@ -1,16 +1,16 @@ import { describe, expect, it } from "bun:test"; -import { parseGitPatch } from "./patch.js"; import { - mockSingleCommit, - mockMultipleCommits, - mockNoMessageLinesAfterSubject, - mockNoDiff, + mockCreateWorkerPlaceholder, + mockLongTrickyMessage, mockMalformed, + mockMultipleCommits, mockMultipleFilesAndHunks, - mockLongTrickyMessage, - mockCreateWorkerPlaceholder, + mockNoDiff, + mockNoMessageLinesAfterSubject, + mockSingleCommit, } from "../../mocks/patch.js"; +import { parseGitPatch } from "./patch.js"; describe("parseGitPatch", () => { it("parses a single commit patch", () => { From 62394545d8b74996c6e6d66fd788f93c74b6837f Mon Sep 17 00:00:00 2001 From: Jinhyeok Lee Date: Sat, 17 May 2025 19:21:40 +0900 Subject: [PATCH 8/9] docs: Update CHANGELOG.md - Updated CHANGELOG.md with recent changes. --- CHANGELOG.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 CHANGELOG.md 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. From 508189a8d0598687d52d8e4aa9f685f6b6bd3803 Mon Sep 17 00:00:00 2001 From: Jinhyeok Lee Date: Sat, 17 May 2025 19:44:22 +0900 Subject: [PATCH 9/9] docs: Update parseGitPatch signature in README Updates the `parseGitPatch` function signature in the README.md file. - Adds an `options` parameter to selectively enable date parsing and structured diff return. - Changes the `date` return type to `string | Date`. - Changes the `diff` return type to `string | FileChange[]`. --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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[]; }[] ```