diff --git a/src/__tests__/diff2html-tests.ts b/src/__tests__/diff2html-tests.ts index 6528b404..eeeae5c0 100644 --- a/src/__tests__/diff2html-tests.ts +++ b/src/__tests__/diff2html-tests.ts @@ -157,26 +157,32 @@ describe('Diff2Html', () => { "type": "delete", }, { - "content": "+ TokenRevoked, MissingToken,", + "content": "\\ No newline at end of file", "newNumber": 53, + "oldNumber": 55, + "type": "context", + }, + { + "content": "+ TokenRevoked, MissingToken,", + "newNumber": 54, "oldNumber": undefined, "type": "insert", }, { "content": "+ IndexLock, RepositoryError, NotValidRepo, PullRequestNotMergeable, BranchError,", - "newNumber": 54, + "newNumber": 55, "oldNumber": undefined, "type": "insert", }, { "content": "+ PluginError, CodeParserError, EngineError = Value", - "newNumber": 55, + "newNumber": 56, "oldNumber": undefined, "type": "insert", }, { "content": "+}", - "newNumber": 56, + "newNumber": 57, "oldNumber": undefined, "type": "insert", }, diff --git a/src/__tests__/side-by-side-printer-tests.ts b/src/__tests__/side-by-side-printer-tests.ts index ab886aba..3ea0cb7f 100644 --- a/src/__tests__/side-by-side-printer-tests.ts +++ b/src/__tests__/side-by-side-printer-tests.ts @@ -407,6 +407,357 @@ describe('SideBySideRenderer', () => { `); }); + it('should handle files without newlines at the end', () => { + const exampleJson: DiffFile[] = [ + { + blocks: [ + // Scenario 1: Old file missing newline, new file has newline + { + lines: [ + { + content: '-oldLine1', + type: LineType.DELETE, + oldNumber: 1, + newNumber: undefined, + }, + { + content: '\\ No newline at end of file', + type: LineType.CONTEXT, + oldNumber: 1, + newNumber: 1, + }, + { + content: '+newLine1', + type: LineType.INSERT, + oldNumber: undefined, + newNumber: 1, + }, + ], + oldStartLine: 1, + newStartLine: 1, + header: '@@ -1 +1 @@', + }, + // Scenario 2: Old file has newline, new file missing newline + { + lines: [ + { + content: '-oldLine2', + type: LineType.DELETE, + oldNumber: 2, + newNumber: undefined, + }, + { + content: '+newLine2', + type: LineType.INSERT, + oldNumber: undefined, + newNumber: 2, + }, + { + content: '\\ No newline at end of file', + type: LineType.CONTEXT, + oldNumber: 2, + newNumber: 2, + }, + ], + oldStartLine: 2, + newStartLine: 2, + header: '@@ -2 +2 @@', + }, + // Scenario 3: Both files missing newline + { + lines: [ + { + content: '-oldLine3', + type: LineType.DELETE, + oldNumber: 3, + newNumber: undefined, + }, + { + content: '\\ No newline at end of file', + type: LineType.CONTEXT, + oldNumber: 3, + newNumber: 3, + }, + { + content: '+newLine3', + type: LineType.INSERT, + oldNumber: undefined, + newNumber: 3, + }, + { + content: '\\ No newline at end of file', + type: LineType.CONTEXT, + oldNumber: 3, + newNumber: 3, + }, + ], + oldStartLine: 3, + newStartLine: 3, + header: '@@ -3 +3 @@', + }, + ], + deletedLines: 3, + addedLines: 3, + oldName: 'sample', + language: 'txt', + newName: 'sample', + isCombined: false, + isGitDiff: true, + }, + ]; + + const hoganUtils = new HoganJsUtils({}); + const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {}); + const html = sideBySideRenderer.render(exampleJson); + expect(html).toMatchInlineSnapshot(` + "
+
+
+ + sample + CHANGED + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
@@ -1 +1 @@
+
+ 1 + +
+ - + oldLine1 +
+
+ 1 + +
+ \\ + No newline at end of file +
+
+ + +
+   +
+
+
+
@@ -2 +2 @@
+
+ 2 + +
+ - + oldLine2 +
+
+ + +
+   +
+
+
+
@@ -3 +3 @@
+
+ 3 + +
+ - + oldLine3 +
+
+ 3 + +
+ \\ + No newline at end of file +
+
+ + +
+   +
+
+
+ + +
+   +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
 
+
+ + +
+   +
+
+
+ + +
+   +
+
+
+ 1 + +
+ + + newLine1 +
+
+
 
+
+ 2 + +
+ + + newLine2 +
+
+ 2 + +
+ \\ + No newline at end of file +
+
+
 
+
+ + +
+   +
+
+
+ + +
+   +
+
+
+ 3 + +
+ + + newLine3 +
+
+ 3 + +
+ \\ + No newline at end of file +
+
+
+
+
+
+
" + `); + }); + it('should work for too big file diff', () => { const exampleJson = [ { diff --git a/src/diff-parser.ts b/src/diff-parser.ts index 83d9c630..2c654202 100644 --- a/src/diff-parser.ts +++ b/src/diff-parser.ts @@ -87,16 +87,15 @@ export function parse(diffInput: string, config: DiffParserConfig = {}): DiffFil const binaryFiles = /^Binary files (.*) and (.*) differ/; const binaryDiff = /^GIT binary patch/; + const noNewlineAtEndOfFile = /^\\ No newline at end of file/; + /* Combined Diff */ const combinedIndex = /^index ([\da-z]+),([\da-z]+)\.\.([\da-z]+)/; const combinedMode = /^mode (\d{6}),(\d{6})\.\.(\d{6})/; const combinedNewFile = /^new file mode (\d{6})/; const combinedDeletedFile = /^deleted file mode (\d{6}),(\d{6})/; - const diffLines = diffInput - .replace(/\\ No newline at end of file/g, '') - .replace(/\r\n?/g, '\n') - .split('\n'); + const diffLines = diffInput.replace(/\r\n?/g, '\n').split('\n'); /* Add previous block(if exists) before start a new file */ function saveBlock(): void { @@ -471,6 +470,8 @@ export function parse(diffInput: string, config: DiffParserConfig = {}): DiffFil } else if ((values = combinedDeletedFile.exec(line))) { currentFile.deletedFileMode = values[1]; currentFile.isDeleted = true; + } else if (line.match(noNewlineAtEndOfFile)) { + createLine(line); } }); diff --git a/src/render-utils.ts b/src/render-utils.ts index f557796e..71e1916a 100644 --- a/src/render-utils.ts +++ b/src/render-utils.ts @@ -83,6 +83,8 @@ export function toCSSClass(lineType: LineType): CSSLineClass { return CSSLineClass.INSERTS; case LineType.DELETE: return CSSLineClass.DELETES; + case LineType.NO_NEW_LINE: + return CSSLineClass.CONTEXT; } } diff --git a/src/side-by-side-renderer.ts b/src/side-by-side-renderer.ts index e63893ae..43b9c901 100644 --- a/src/side-by-side-renderer.ts +++ b/src/side-by-side-renderer.ts @@ -10,6 +10,7 @@ import { DiffLineDeleted, DiffLineInserted, DiffLineContent, + DiffLineNoNewline, } from './types'; export interface SideBySideRendererConfig extends renderUtils.RenderConfig { @@ -114,20 +115,39 @@ export default class SideBySideRenderer { } else if (contextLines.length) { contextLines.forEach(line => { const { prefix, content } = renderUtils.deconstructLine(line.content, file.isCombined); - const { left, right } = this.generateLineHtml( - { + let leftContext = undefined; + let rightContext = undefined; + if (line.type !== LineType.NO_NEW_LINE) { + leftContext = { type: renderUtils.CSSLineClass.CONTEXT, prefix: prefix, content: content, number: line.oldNumber, - }, - { + }; + rightContext = { type: renderUtils.CSSLineClass.CONTEXT, prefix: prefix, content: content, number: line.newNumber, - }, - ); + }; + } else if (line.type === LineType.NO_NEW_LINE) { + if (line.isLeft) { + leftContext = { + type: renderUtils.CSSLineClass.CONTEXT, + prefix: prefix, + content: content, + number: line.oldNumber, + }; + } else { + rightContext = { + type: renderUtils.CSSLineClass.CONTEXT, + prefix: prefix, + content: content, + number: line.newNumber, + }; + } + } + const { left, right } = this.generateLineHtml(leftContext, rightContext); fileHtml.left += left; fileHtml.right += right; }); @@ -155,6 +175,7 @@ export default class SideBySideRenderer { let oldLines: (DiffLineDeleted & DiffLineContent)[] = []; let newLines: (DiffLineInserted & DiffLineContent)[] = []; + let lastLineType: LineType = LineType.DELETE; for (let i = 0; i < block.lines.length; i++) { const diffLine = block.lines[i]; @@ -169,7 +190,16 @@ export default class SideBySideRenderer { } if (diffLine.type === LineType.CONTEXT) { - blockLinesGroups.push([[diffLine], [], []]); + if (diffLine.content.trim() === '\\ No newline at end of file') { + const noNewLine: DiffLineNoNewline & DiffLineContent = { + ...diffLine, + isLeft: lastLineType === LineType.DELETE, + type: LineType.NO_NEW_LINE, + }; + blockLinesGroups.push([[noNewLine], [], []]); + } else { + blockLinesGroups.push([[diffLine], [], []]); + } } else if (diffLine.type === LineType.INSERT && oldLines.length === 0) { blockLinesGroups.push([[], [], [diffLine]]); } else if (diffLine.type === LineType.INSERT && oldLines.length > 0) { @@ -177,6 +207,11 @@ export default class SideBySideRenderer { } else if (diffLine.type === LineType.DELETE) { oldLines.push(diffLine); } + + // Track the last non-context line type to determine where "No newline" belongs + if (diffLine.type !== LineType.CONTEXT) { + lastLineType = diffLine.type; + } } if (oldLines.length || newLines.length) { @@ -296,7 +331,7 @@ export default class SideBySideRenderer { } type DiffLineGroups = [ - (DiffLineContext & DiffLineContent)[], + ((DiffLineContext & DiffLineContent) | (DiffLineNoNewline & DiffLineContent))[], (DiffLineDeleted & DiffLineContent)[], (DiffLineInserted & DiffLineContent)[], ][]; diff --git a/src/types.ts b/src/types.ts index d226286b..2573e8cb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ export enum LineType { INSERT = 'insert', DELETE = 'delete', CONTEXT = 'context', + NO_NEW_LINE = 'noNewLine', } export interface DiffLineDeleted { @@ -27,6 +28,13 @@ export interface DiffLineContext { newNumber: number; } +export interface DiffLineNoNewline { + type: LineType.NO_NEW_LINE; + oldNumber: number; + newNumber: number; + isLeft: boolean; +} + export type DiffLineContent = { content: string; };