This commit is contained in:
Shanan Sussman 2025-03-19 16:58:32 -07:00 committed by GitHub
commit 28be4657da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 421 additions and 18 deletions

View file

@ -157,29 +157,35 @@ describe('Diff2Html', () => {
"type": "delete",
},
{
"content": "+ TokenRevoked, MissingToken,",
"content": "\\ No newline at end of file",
"newNumber": 53,
"oldNumber": undefined,
"type": "insert",
"oldNumber": 55,
"type": "context",
},
{
"content": "+ IndexLock, RepositoryError, NotValidRepo, PullRequestNotMergeable, BranchError,",
"content": "+ TokenRevoked, MissingToken,",
"newNumber": 54,
"oldNumber": undefined,
"type": "insert",
},
{
"content": "+ PluginError, CodeParserError, EngineError = Value",
"content": "+ IndexLock, RepositoryError, NotValidRepo, PullRequestNotMergeable, BranchError,",
"newNumber": 55,
"oldNumber": undefined,
"type": "insert",
},
{
"content": "+}",
"content": "+ PluginError, CodeParserError, EngineError = Value",
"newNumber": 56,
"oldNumber": undefined,
"type": "insert",
},
{
"content": "+}",
"newNumber": 57,
"oldNumber": undefined,
"type": "insert",
},
],
"newStartLine": 50,
"oldStartLine": 50,

View file

@ -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(`
"<div class="d2h-wrapper d2h-light-color-scheme">
<div id="d2h-675094" class="d2h-file-wrapper" data-lang="txt">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">sample</span>
<span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-files-diff">
<div class="d2h-file-side-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">@@ -1 +1 @@</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-del">
1
</td>
<td class="d2h-del">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">-</span>
<span class="d2h-code-line-ctn">oldLine1</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-cntx">
1
</td>
<td class="d2h-cntx">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">\\</span>
<span class="d2h-code-line-ctn"> No newline at end of file</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">@@ -2 +2 @@</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-del d2h-change">
2
</td>
<td class="d2h-del d2h-change">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">-</span>
<span class="d2h-code-line-ctn"><del>oldLine2</del></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">@@ -3 +3 @@</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-del">
3
</td>
<td class="d2h-del">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">-</span>
<span class="d2h-code-line-ctn">oldLine3</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-cntx">
3
</td>
<td class="d2h-cntx">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">\\</span>
<span class="d2h-code-line-ctn"> No newline at end of file</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="d2h-file-side-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">&nbsp;</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
1
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn">newLine1</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">&nbsp;</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins d2h-change">
2
</td>
<td class="d2h-ins d2h-change">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"><ins>newLine2</ins></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-cntx">
2
</td>
<td class="d2h-cntx">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">\\</span>
<span class="d2h-code-line-ctn"> No newline at end of file</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">&nbsp;</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
3
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn">newLine3</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-cntx">
3
</td>
<td class="d2h-cntx">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">\\</span>
<span class="d2h-code-line-ctn"> No newline at end of file</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>"
`);
});
it('should work for too big file diff', () => {
const exampleJson = [
{

View file

@ -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);
}
});

View file

@ -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;
}
}

View file

@ -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)[],
][];

View file

@ -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;
};