This is an automated email from the ASF dual-hosted git repository.

voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 45f305a765 [ZEPPELIN-6584] Fix reversed line diff direction in New UI 
revision comparator
45f305a765 is described below

commit 45f305a765915fe22e616a74c37fd7d6dbdb4d31
Author: JangAyeon <[email protected]>
AuthorDate: Thu Oct 1 00:58:01 2026 +0900

    [ZEPPELIN-6584] Fix reversed line diff direction in New UI revision 
comparator
    
    ### What is this PR for?
    The New UI revision comparator labels a comparison as `first --> second` 
(e.g. `older --> newer`), but `compareRevisions()` built the line-level diff 
from the second revision back to the first. As a result, added lines were 
rendered as red deletions and removed lines as green insertions for paragraphs 
present in both revisions.
    
    This PR computes the line diff in the same `first --> second` direction 
shown in the UI, matching the legacy AngularJS comparator 
(`diffLines(firstText, secondText)`). The whole-paragraph `added`/`deleted` 
classification, which was already correct, is unchanged.
    
    
    ### What type of PR is it?
    Bug Fix
    
    ### Todos
    * [x] - Fix line diff direction
    * [x] - Add focused comparator unit tests
    
    ### What is the Jira issue?
    [ZEPPELIN-6584](https://issues.apache.org/jira/browse/ZEPPELIN-6584)
    
    ### How should this be tested?
    * Unit tests:
    ```
      cd zeppelin-web-angular
      npm run test:shell -- revisions-comparator.component.spec.ts
      npm run lint
    ```
    
    ### Screenshots (if appropriate)
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    
    Closes #5507 from JangAyeon/ZEPPELIN-6584.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 .../revisions-comparator.component.spec.ts         | 98 ++++++++++++++++++++++
 .../revisions-comparator.component.ts              | 39 +++++----
 2 files changed, 117 insertions(+), 20 deletions(-)

diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.spec.ts
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.spec.ts
new file mode 100644
index 0000000000..7cd14e8233
--- /dev/null
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.spec.ts
@@ -0,0 +1,98 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ChangeDetectorRef } from '@angular/core';
+import { DatePipe } from '@angular/common';
+import { describe, expect, it, vi } from 'vitest';
+
+import { NoteRevisionForCompareReceived } from '@zeppelin/sdk';
+import { MessageService } from '@zeppelin/services';
+
+import { NotebookRevisionsComparatorComponent } from 
'./revisions-comparator.component';
+
+// The barrel pulls in monaco-editor; the comparator only needs the 
constructor token.
+vi.mock('@zeppelin/services', () => ({ MessageService: class {} }));
+
+interface ParagraphFixture {
+  id: string;
+  text: string;
+}
+
+const revision = (revisionId: string, paragraphs: ParagraphFixture[]): 
NoteRevisionForCompareReceived =>
+  ({
+    noteId: 'note',
+    revisionId,
+    position: '',
+    note: { paragraphs }
+  }) as unknown as NoteRevisionForCompareReceived;
+
+const compare = (first: NoteRevisionForCompareReceived, second: 
NoteRevisionForCompareReceived) => {
+  const component = new NotebookRevisionsComparatorComponent(
+    {} as MessageService,
+    {} as ChangeDetectorRef,
+    new DatePipe('en-US')
+  );
+  component.firstNoteRevisionForCompare = first;
+  component.secondNoteRevisionForCompare = second;
+  component.compareRevisions();
+  return component.mergeNoteRevisionsDiff;
+};
+
+const segmentTexts = (diff: ReturnType<typeof compare>[number], type: 'insert' 
| 'delete') =>
+  (diff.segments || []).filter(s => s.type === type).map(s => s.text);
+
+const older = revision('older', [{ id: 'p', text: 'line common\nold line' }]);
+const newer = revision('newer', [{ id: 'p', text: 'line common\nnew line' }]);
+
+describe('NotebookRevisionsComparatorComponent.compareRevisions', () => {
+  it('marks removed lines as deleted and added lines as inserted for older --> 
newer', () => {
+    const [diff] = compare(older, newer);
+
+    expect(diff.type).toBe('compared');
+    expect(diff.identical).toBe(false);
+    expect(segmentTexts(diff, 'delete')).toEqual(['old line']);
+    expect(segmentTexts(diff, 'insert')).toEqual(['new line']);
+  });
+
+  it('reverses the line diff when the revisions are selected in the opposite 
order', () => {
+    const [diff] = compare(newer, older);
+
+    expect(segmentTexts(diff, 'delete')).toEqual(['new line']);
+    expect(segmentTexts(diff, 'insert')).toEqual(['old line']);
+  });
+
+  it('marks a paragraph with unchanged text as identical', () => {
+    const [diff] = compare(older, revision('same', [{ id: 'p', text: 'line 
common\nold line' }]));
+
+    expect(diff.type).toBe('compared');
+    expect(diff.identical).toBe(true);
+    expect(diff.segments?.every(s => s.type === 'equal')).toBe(true);
+  });
+
+  it('classifies a paragraph only in the second revision as added', () => {
+    const diffs = compare(revision('first', []), revision('second', [{ id: 
'new', text: 'added\nbody' }]));
+
+    expect(diffs).toHaveLength(1);
+    expect(diffs[0].type).toBe('added');
+    expect(diffs[0].paragraph.id).toBe('new');
+    expect(diffs[0].firstString).toBe('added');
+  });
+
+  it('classifies a paragraph only in the first revision as deleted', () => {
+    const diffs = compare(revision('first', [{ id: 'gone', text: 
'removed\nbody' }]), revision('second', []));
+
+    expect(diffs).toHaveLength(1);
+    expect(diffs[0].type).toBe('deleted');
+    expect(diffs[0].paragraph.id).toBe('gone');
+    expect(diffs[0].firstString).toBe('removed');
+  });
+});
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts
index b29f42dad5..2bd19ef90f 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts
@@ -12,7 +12,7 @@
 
 import { DatePipe } from '@angular/common';
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, 
OnDestroy, OnInit } from '@angular/core';
-import * as DiffMatchPatch from 'diff-match-patch';
+import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
 import { Subscription } from 'rxjs';
 
 import { NoteRevisionForCompareReceived, OP, ParagraphItem, RevisionListItem } 
from '@zeppelin/sdk';
@@ -127,38 +127,37 @@ export class NotebookRevisionsComparatorComponent 
implements OnInit, OnDestroy {
     if (!this.firstNoteRevisionForCompare || 
!this.secondNoteRevisionForCompare) {
       return;
     }
-    const baseParagraphs = this.secondNoteRevisionForCompare.note?.paragraphs 
|| [];
-    const compareParagraphs = 
this.firstNoteRevisionForCompare.note?.paragraphs || [];
+    // The UI reads `first --> second`, so every diff runs from first (from) 
to second (to).
+    const fromParagraphs = this.firstNoteRevisionForCompare.note?.paragraphs 
|| [];
+    const toParagraphs = this.secondNoteRevisionForCompare.note?.paragraphs || 
[];
     const paragraphDiffs: MergedParagraphDiff[] = [];
 
-    for (const p1 of baseParagraphs) {
-      const p2 = compareParagraphs.find((p: ParagraphItem) => p.id === p1.id) 
|| null;
-      if (p2 === null) {
+    for (const toParagraph of toParagraphs) {
+      const fromParagraph = fromParagraphs.find((p: ParagraphItem) => p.id === 
toParagraph.id) || null;
+      if (fromParagraph === null) {
         paragraphDiffs.push({
-          paragraph: p1,
-          firstString: (p1.text || '').split('\n')[0],
+          paragraph: toParagraph,
+          firstString: (toParagraph.text || '').split('\n')[0],
           type: 'added'
         });
       } else {
-        const text1 = p1.text || '';
-        const text2 = p2.text || '';
-        const diffResult = this.buildLineDiff(text1, text2);
+        const diffResult = this.buildLineDiff(fromParagraph.text || '', 
toParagraph.text || '');
         paragraphDiffs.push({
-          paragraph: p1,
+          paragraph: toParagraph,
           segments: diffResult.segments,
           identical: diffResult.identical,
-          firstString: (p1.text || '').split('\n')[0],
+          firstString: (toParagraph.text || '').split('\n')[0],
           type: 'compared'
         });
       }
     }
 
-    for (const p2 of compareParagraphs) {
-      const p1 = baseParagraphs.find((p: ParagraphItem) => p.id === p2.id) || 
null;
-      if (p1 === null) {
+    for (const fromParagraph of fromParagraphs) {
+      const toParagraph = toParagraphs.find((p: ParagraphItem) => p.id === 
fromParagraph.id) || null;
+      if (toParagraph === null) {
         paragraphDiffs.push({
-          paragraph: p2,
-          firstString: (p2.text || '').split('\n')[0],
+          paragraph: fromParagraph,
+          firstString: (fromParagraph.text || '').split('\n')[0],
           type: 'deleted'
         });
       }
@@ -183,8 +182,8 @@ export class NotebookRevisionsComparatorComponent 
implements OnInit, OnDestroy {
     return this.datePipe.transform(time * 1000, 'MMMM d yyyy, h:mm:ss a') || 
'';
   }
 
-  private buildLineDiff(text1: string, text2: string): { segments: 
DiffSegment[]; identical: boolean } {
-    const { chars1, chars2, lineArray } = this.dmp.diff_linesToChars_(text1, 
text2);
+  private buildLineDiff(fromText: string, toText: string): { segments: 
DiffSegment[]; identical: boolean } {
+    const { chars1, chars2, lineArray } = 
this.dmp.diff_linesToChars_(fromText, toText);
     const diffs = this.dmp.diff_main(chars1, chars2, false);
     this.dmp.diff_charsToLines_(diffs, lineArray);
 

Reply via email to