Merge "Refactor AccountUpdate to be an interface"
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 8d72a97..1f32073 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -327,7 +327,7 @@
       ${this.renderCCs()} ${this.renderProjectBranch()} ${this.renderParent()}
       ${this.renderMergedAs()} ${this.renderShowRevertCreatedAs()}
       ${this.renderTopic()} ${this.renderCherryPickOf()}
-      ${this.renderStrategy()} ${this.renderHashTags()}
+      ${this.renderRevertOf()} ${this.renderStrategy()} ${this.renderHashTags()}
       ${this.renderSubmitRequirements()} ${this.renderWeblinks()}
       <gr-endpoint-decorator name="change-metadata-item">
         <gr-endpoint-param
@@ -687,6 +687,23 @@
     </section>`;
   }
 
+  private renderRevertOf() {
+    if (!this.change?.revert_of) return nothing;
+    return html` <section class=${this.computeDisplayState(Metadata.REVERT_OF)}>
+      <span class="title">Revert of</span>
+      <span class="value">
+        <a
+          href=${createChangeUrl({
+            changeNum: this.change.revert_of,
+            repo: this.change.project,
+            usp: 'metadata',
+          })}
+          >${this.change.revert_of}</a
+        >
+      </span>
+    </section>`;
+  }
+
   private renderStrategy() {
     if (!changeIsOpen(this.change)) return nothing;
     return html`<section
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
index c93cc97..97fa267 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
@@ -87,6 +87,10 @@
           background-color: var(--status-ready);
           color: var(--status-ready);
         }
+        :host(.revert) .chip {
+          background-color: var(--status-revert);
+          color: var(--status-revert);
+        }
         :host(.revert-created) .chip {
           background-color: var(--status-revert-created);
           color: var(--status-revert-created);
diff --git a/polygerrit-ui/app/embed/diff-old/gr-diff-builder/gr-diff-row.ts b/polygerrit-ui/app/embed/diff-old/gr-diff-builder/gr-diff-row.ts
index f2742c7..1486154 100644
--- a/polygerrit-ui/app/embed/diff-old/gr-diff-builder/gr-diff-row.ts
+++ b/polygerrit-ui/app/embed/diff-old/gr-diff-builder/gr-diff-row.ts
@@ -315,8 +315,11 @@
     // For unified diff, this method will be called with number set to 0 for
     // the empty line number column for added/removed lines. This should not
     // be announced to the screenreader.
-    if (lineNumber === LOST || lineNumber <= 0) return undefined;
-
+    if (
+      lineNumber === LOST ||
+      (typeof lineNumber === 'number' && lineNumber <= 0)
+    )
+      return undefined;
     switch (line.type) {
       case GrDiffLineType.REMOVE:
         return `${lineNumber} removed`;
diff --git a/polygerrit-ui/app/embed/diff-old/gr-diff-selection/gr-diff-selection.ts b/polygerrit-ui/app/embed/diff-old/gr-diff-selection/gr-diff-selection.ts
index a9ec6a2..407b403 100644
--- a/polygerrit-ui/app/embed/diff-old/gr-diff-selection/gr-diff-selection.ts
+++ b/polygerrit-ui/app/embed/diff-old/gr-diff-selection/gr-diff-selection.ts
@@ -18,6 +18,7 @@
   getSideByLineEl,
   isThreadEl,
 } from '../../diff/gr-diff/gr-diff-utils';
+import {getContentFromDiff} from '../../../utils/diff-util';
 
 /**
  * Possible CSS classes indicating the state of selection. Dynamically added/
@@ -34,15 +35,6 @@
   return side === Side.LEFT ? SelectionClass.LEFT : SelectionClass.RIGHT;
 }
 
-interface LinesCache {
-  left: string[] | null;
-  right: string[] | null;
-}
-
-function getNewCache(): LinesCache {
-  return {left: null, right: null};
-}
-
 export class GrDiffSelection {
   // visible for testing
   diff?: DiffInfo;
@@ -50,9 +42,6 @@
   // visible for testing
   diffTable?: HTMLElement;
 
-  // visible for testing
-  linesCache: LinesCache = getNewCache();
-
   init(diff: DiffInfo, diffTable: HTMLElement) {
     this.cleanup();
     this.diff = diff;
@@ -60,7 +49,6 @@
     this.diffTable.classList.add(SelectionClass.RIGHT);
     this.diffTable.addEventListener('copy', this.handleCopy);
     this.diffTable.addEventListener('mousedown', this.handleDown);
-    this.linesCache = getNewCache();
   }
 
   cleanup() {
@@ -161,6 +149,7 @@
    * @return The selected text.
    */
   getSelectedText(side: Side) {
+    if (!this.diff) return '';
     const sel = this.getSelection();
     if (!sel || sel.rangeCount !== 1) {
       return ''; // No multi-select support yet.
@@ -188,7 +177,8 @@
       if (endLineDataValue) endLineNum = Number(endLineDataValue);
     }
 
-    return this.getRangeFromDiff(
+    return getContentFromDiff(
+      this.diff,
       startLineNum,
       range.startOffset,
       endLineNum,
@@ -196,52 +186,4 @@
       side
     );
   }
-
-  /**
-   * Query the diff object for the selected lines.
-   */
-  getRangeFromDiff(
-    startLineNum: number,
-    startOffset: number,
-    endLineNum: number | undefined,
-    endOffset: number,
-    side: Side
-  ) {
-    const skipChunk = this.diff?.content.find(chunk => chunk.skip);
-    if (skipChunk) {
-      startLineNum -= skipChunk.skip!;
-      if (endLineNum) endLineNum -= skipChunk.skip!;
-    }
-    const lines = this.getDiffLines(side).slice(startLineNum - 1, endLineNum);
-    if (lines.length) {
-      lines[lines.length - 1] = lines[lines.length - 1].substring(0, endOffset);
-      lines[0] = lines[0].substring(startOffset);
-    }
-    return lines.join('\n');
-  }
-
-  /**
-   * Query the diff object for the lines from a particular side.
-   *
-   * @param side The side that is currently selected.
-   * @return An array of strings indexed by line number.
-   */
-  getDiffLines(side: Side): string[] {
-    if (this.linesCache[side]) {
-      return this.linesCache[side]!;
-    }
-    if (!this.diff) return [];
-    let lines: string[] = [];
-    for (const chunk of this.diff.content) {
-      if (chunk.ab) {
-        lines = lines.concat(chunk.ab);
-      } else if (side === Side.LEFT && chunk.a) {
-        lines = lines.concat(chunk.a);
-      } else if (side === Side.RIGHT && chunk.b) {
-        lines = lines.concat(chunk.b);
-      }
-    }
-    this.linesCache[side] = lines;
-    return lines;
-  }
 }
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
index bfd6a0d..0ac7516 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
@@ -312,7 +312,11 @@
     // For unified diff, this method will be called with number set to 0 for
     // the empty line number column for added/removed lines. This should not
     // be announced to the screenreader.
-    if (lineNumber === LOST || lineNumber <= 0) return undefined;
+    if (
+      lineNumber === LOST ||
+      (typeof lineNumber === 'number' && lineNumber <= 0)
+    )
+      return undefined;
 
     switch (line.type) {
       case GrDiffLineType.REMOVE:
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
index a790736..f403ef9 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
@@ -18,6 +18,7 @@
   getSideByLineEl,
   isThreadEl,
 } from '../gr-diff/gr-diff-utils';
+import {getContentFromDiff} from '../../../utils/diff-util';
 
 /**
  * Possible CSS classes indicating the state of selection. Dynamically added/
@@ -34,15 +35,6 @@
   return side === Side.LEFT ? SelectionClass.LEFT : SelectionClass.RIGHT;
 }
 
-interface LinesCache {
-  left: string[] | null;
-  right: string[] | null;
-}
-
-function getNewCache(): LinesCache {
-  return {left: null, right: null};
-}
-
 export class GrDiffSelection {
   // visible for testing
   diff?: DiffInfo;
@@ -50,9 +42,6 @@
   // visible for testing
   diffTable?: HTMLElement;
 
-  // visible for testing
-  linesCache: LinesCache = getNewCache();
-
   init(diff: DiffInfo, diffTable: HTMLElement) {
     this.cleanup();
     this.diff = diff;
@@ -60,7 +49,6 @@
     this.diffTable.classList.add(SelectionClass.RIGHT);
     this.diffTable.addEventListener('copy', this.handleCopy);
     this.diffTable.addEventListener('mousedown', this.handleDown);
-    this.linesCache = getNewCache();
   }
 
   cleanup() {
@@ -161,6 +149,7 @@
    * @return The selected text.
    */
   getSelectedText(side: Side) {
+    if (!this.diff) return '';
     const sel = this.getSelection();
     if (!sel || sel.rangeCount !== 1) {
       return ''; // No multi-select support yet.
@@ -188,7 +177,8 @@
       if (endLineDataValue) endLineNum = Number(endLineDataValue);
     }
 
-    return this.getRangeFromDiff(
+    return getContentFromDiff(
+      this.diff,
       startLineNum,
       range.startOffset,
       endLineNum,
@@ -196,52 +186,4 @@
       side
     );
   }
-
-  /**
-   * Query the diff object for the selected lines.
-   */
-  getRangeFromDiff(
-    startLineNum: number,
-    startOffset: number,
-    endLineNum: number | undefined,
-    endOffset: number,
-    side: Side
-  ) {
-    const skipChunk = this.diff?.content.find(chunk => chunk.skip);
-    if (skipChunk) {
-      startLineNum -= skipChunk.skip!;
-      if (endLineNum) endLineNum -= skipChunk.skip!;
-    }
-    const lines = this.getDiffLines(side).slice(startLineNum - 1, endLineNum);
-    if (lines.length) {
-      lines[lines.length - 1] = lines[lines.length - 1].substring(0, endOffset);
-      lines[0] = lines[0].substring(startOffset);
-    }
-    return lines.join('\n');
-  }
-
-  /**
-   * Query the diff object for the lines from a particular side.
-   *
-   * @param side The side that is currently selected.
-   * @return An array of strings indexed by line number.
-   */
-  getDiffLines(side: Side): string[] {
-    if (this.linesCache[side]) {
-      return this.linesCache[side]!;
-    }
-    if (!this.diff) return [];
-    let lines: string[] = [];
-    for (const chunk of this.diff.content) {
-      if (chunk.ab) {
-        lines = lines.concat(chunk.ab);
-      } else if (side === Side.LEFT && chunk.a) {
-        lines = lines.concat(chunk.a);
-      } else if (side === Side.RIGHT && chunk.b) {
-        lines = lines.concat(chunk.b);
-      }
-    }
-    this.linesCache[side] = lines;
-    return lines;
-  }
 }
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 0503e4c..7742b1f 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -323,6 +323,7 @@
     --status-wip: #795548;
     --status-private: var(--purple-500);
     --status-conflict: var(--red-600);
+    --status-revert: var(--gray-900);
     --status-revert-created: #e64a19;
     --status-active: var(--blue-700);
     --status-ready: var(--pink-800);
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index dc3d4e9..574e6c2 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -181,6 +181,7 @@
     --status-wip: #bcaaa4;
     --status-private: var(--purple-200);
     --status-conflict: var(--red-300);
+    --status-revert: var(--gray-200);
     --status-revert-created: #ff8a65;
     --status-active: var(--blue-400);
     --status-ready: var(--pink-500);
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 7a6610b..766b407 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -692,7 +692,11 @@
   MERGED = 'Merged',
   PRIVATE = 'Private',
   READY_TO_SUBMIT = 'Ready to submit',
+  /** This change is a revert of another change. */
+  REVERT = 'Revert',
+  /** A revert of this change was created. */
   REVERT_CREATED = 'Revert Created',
+  /** A revert of this change was submitted. */
   REVERT_SUBMITTED = 'Revert Submitted',
   WIP = 'WIP',
 }
diff --git a/polygerrit-ui/app/utils/change-metadata-util.ts b/polygerrit-ui/app/utils/change-metadata-util.ts
index 9d3106d..8b208e4 100644
--- a/polygerrit-ui/app/utils/change-metadata-util.ts
+++ b/polygerrit-ui/app/utils/change-metadata-util.ts
@@ -22,6 +22,7 @@
   AUTHOR = 'Author',
   COMMITTER = 'Committer',
   CHERRY_PICK_OF = 'Cherry pick of',
+  REVERT_OF = 'Revert of',
 }
 
 export const DisplayRules = {
@@ -39,6 +40,7 @@
     Metadata.AUTHOR,
     Metadata.COMMITTER,
     Metadata.CHERRY_PICK_OF,
+    Metadata.REVERT_OF,
   ],
   ALWAYS_HIDE: [
     Metadata.PARENT,
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index 7de5e7e..584a4b1 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -157,6 +157,9 @@
   options?: ChangeStatusesOptions
 ): ChangeStates[] {
   const states = [];
+  if (change.revert_of) {
+    states.push(ChangeStates.REVERT);
+  }
   if (change.status === ChangeStatus.MERGED) {
     if (options?.revertingChangeStatus === ChangeStatus.MERGED) {
       return [ChangeStates.MERGED, ChangeStates.REVERT_SUBMITTED];
diff --git a/polygerrit-ui/app/utils/change-util_test.ts b/polygerrit-ui/app/utils/change-util_test.ts
index 1782d80..1d209f9 100644
--- a/polygerrit-ui/app/utils/change-util_test.ts
+++ b/polygerrit-ui/app/utils/change-util_test.ts
@@ -163,6 +163,19 @@
     assert.deepEqual(changeStatuses(change), [ChangeStates.ABANDONED]);
   });
 
+  test('Revert status', () => {
+    const change = {
+      ...createChange(),
+      revert_of: 123 as NumericChangeId,
+    };
+    assert.deepEqual(changeStatuses(change), [ChangeStates.REVERT]);
+    change.is_private = true;
+    assert.deepEqual(changeStatuses(change), [
+      ChangeStates.REVERT,
+      ChangeStates.PRIVATE,
+    ]);
+  });
+
   test('Open status with private and wip', () => {
     const change = {
       ...createChange(),
diff --git a/polygerrit-ui/app/utils/diff-util.ts b/polygerrit-ui/app/utils/diff-util.ts
index 1e5abf9..28c22e5 100644
--- a/polygerrit-ui/app/utils/diff-util.ts
+++ b/polygerrit-ui/app/utils/diff-util.ts
@@ -18,6 +18,38 @@
   }, 0);
 }
 
+function getDiffLines(diff: DiffInfo, side: Side): string[] {
+  let lines: string[] = [];
+  for (const chunk of diff.content) {
+    if (chunk.skip) {
+      lines = lines.concat(Array(chunk.skip).fill(''));
+    } else if (chunk.ab) {
+      lines = lines.concat(chunk.ab);
+    } else if (side === Side.LEFT && chunk.a) {
+      lines = lines.concat(chunk.a);
+    } else if (side === Side.RIGHT && chunk.b) {
+      lines = lines.concat(chunk.b);
+    }
+  }
+  return lines;
+}
+
+export function getContentFromDiff(
+  diff: DiffInfo,
+  startLineNum: number,
+  startOffset: number,
+  endLineNum: number | undefined,
+  endOffset: number,
+  side: Side
+) {
+  const lines = getDiffLines(diff, side).slice(startLineNum - 1, endLineNum);
+  if (lines.length) {
+    lines[lines.length - 1] = lines[lines.length - 1].substring(0, endOffset);
+    lines[0] = lines[0].substring(startOffset);
+  }
+  return lines.join('\n');
+}
+
 export function isFileUnchanged(diff: DiffInfo) {
   return !diff.content.some(
     content => (content.a && !content.common) || (content.b && !content.common)
diff --git a/polygerrit-ui/app/utils/diff-util_test.ts b/polygerrit-ui/app/utils/diff-util_test.ts
index dbab76d..2829209 100644
--- a/polygerrit-ui/app/utils/diff-util_test.ts
+++ b/polygerrit-ui/app/utils/diff-util_test.ts
@@ -4,10 +4,10 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {assert} from '@open-wc/testing';
-import {DiffInfo} from '../api/diff';
+import {DiffInfo, Side} from '../api/diff';
 import '../test/common-test-setup';
 import {createDiff} from '../test/test-data-generators';
-import {isFileUnchanged} from './diff-util';
+import {getContentFromDiff, isFileUnchanged} from './diff-util';
 
 suite('diff-util tests', () => {
   test('isFileUnchanged', () => {
@@ -41,4 +41,78 @@
     };
     assert.equal(isFileUnchanged(diff), true);
   });
+
+  suite('getContentFromDiff', () => {
+    test('one changed line', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [{a: ['abcd']}, {b: ['wxyz']}],
+      };
+      assert.equal(getContentFromDiff(diff, 1, 1, 1, 3, Side.LEFT), 'bc');
+      assert.equal(getContentFromDiff(diff, 1, 1, 1, 3, Side.RIGHT), 'xy');
+    });
+
+    test('one common line', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [{ab: ['abcd']}],
+      };
+      assert.equal(getContentFromDiff(diff, 1, 1, 1, 3, Side.LEFT), 'bc');
+      assert.equal(getContentFromDiff(diff, 1, 1, 1, 3, Side.RIGHT), 'bc');
+    });
+
+    test('multiple lines', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [
+          {a: ['l1-asdf', 'l2-asdf']},
+          {b: ['r1-wxyz']},
+          {ab: ['l3-r2-qwer', 'l4-r3-uiop']},
+          {b: ['r4-hjkl']},
+          {ab: ['l5-r5-bnm,']},
+        ],
+      };
+      assert.equal(
+        getContentFromDiff(diff, 1, 0, 5, 10, Side.LEFT),
+        'l1-asdf\nl2-asdf\nl3-r2-qwer\nl4-r3-uiop\nl5-r5-bnm,'
+      );
+      assert.equal(
+        getContentFromDiff(diff, 1, 0, 5, 10, Side.RIGHT),
+        'r1-wxyz\nl3-r2-qwer\nl4-r3-uiop\nr4-hjkl\nl5-r5-bnm,'
+      );
+    });
+
+    test('one skip chunk', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [{skip: 5}, {ab: ['abcd']}],
+      };
+      assert.equal(getContentFromDiff(diff, 1, 1, 1, 3, Side.LEFT), '');
+      assert.equal(getContentFromDiff(diff, 1, 1, 1, 3, Side.RIGHT), '');
+      assert.equal(getContentFromDiff(diff, 6, 1, 6, 3, Side.LEFT), 'bc');
+      assert.equal(getContentFromDiff(diff, 6, 1, 6, 3, Side.RIGHT), 'bc');
+    });
+
+    test('multiple skip chunks', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [
+          {skip: 5},
+          {ab: ['abcd']},
+          {skip: 5},
+          {ab: ['qwer']},
+          {skip: 5},
+          {ab: ['zxcv']},
+        ],
+      };
+      assert.equal(getContentFromDiff(diff, 1, 1, 1, 3, Side.LEFT), '');
+      assert.equal(getContentFromDiff(diff, 1, 1, 1, 3, Side.RIGHT), '');
+      assert.equal(getContentFromDiff(diff, 6, 1, 6, 3, Side.LEFT), 'bc');
+      assert.equal(getContentFromDiff(diff, 6, 1, 6, 3, Side.RIGHT), 'bc');
+      assert.equal(getContentFromDiff(diff, 12, 1, 12, 3, Side.LEFT), 'we');
+      assert.equal(getContentFromDiff(diff, 12, 1, 12, 3, Side.RIGHT), 'we');
+      assert.equal(getContentFromDiff(diff, 18, 1, 18, 3, Side.LEFT), 'xc');
+      assert.equal(getContentFromDiff(diff, 18, 1, 18, 3, Side.RIGHT), 'xc');
+    });
+  });
 });