Merge "Fix label votes not showing up when not logged in"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 40721f5..5ed8ccf 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -5523,6 +5523,49 @@
   trustFolderStat = false
 ----
 
+[[jgit-gc]]
+=== Section gc
+
+Options in section gc are used when command link:cmd-gc.html[gerrit gc] is used
+or scheduled via options link:cmd-gc.html#gc.startTime[gc.startTime] and
+link:cmd-gc.html#gc.interval[gc.interval].
+
+[[gc.auto]]gc.auto::
++
+When there are approximately more than this many loose objects in the repository,
+auto gc will pack them. Some commands use this command to perform a light-weight
+garbage collection from time to time. The default value is 6700.
++
+Setting this to 0 disables not only automatic packing based on the number of
+loose objects, but any other heuristic auto gc will otherwise use to determine
+if there’s work to do, such as link:#gc.autoPackLimit[gc.autoPackLimit].
+
+[[gc.autodetach]]gc.autodetach::
++
+Makes auto gc run in a background thread. Default is `true`.
+
+[[gc.autopacklimit]]gc.autopacklimit::
++
+When there are more than this many packs that are not marked with `*.keep` file
+in the repository, auto gc consolidates them into one larger pack. The
+default value is 50. Setting this to 0 disables it. Setting `gc.auto` to 0 will
+also disable this.
+
+[[gc.packRefs]]gc.packRefs::
++
+This variable determines whether gc runs git pack-refs. The default is `true`.
+
+[[gc.reflogExpire]]gc.reflogExpire::
++
+Removes reflog entries older than this time; defaults to 90 days. The value "now"
+expires all entries immediately, and "never" suppresses expiration altogether.
+
+[[gc.reflogExpireUnreachable]]gc.reflogExpireUnreachable::
++
+Removes reflog entries older than this time and not reachable from the
+current tip; defaults to 30 days. The value "now" expires all entries immediately,
+and "never" suppresses expiration altogether.
+
 [[jgit-protocol]]
 === Section protocol
 
@@ -5540,6 +5583,16 @@
 2:: wire protocol version 2. Speeds up fetches from repositories with many refs by allowing the client
     to specify which refs to list before the server lists them.
 
+[[jgit-receive]]
+=== Section receive
+
+[[receive.autogc]]receive.autogc::
++
+By default, `git-receive-pack` will run auto gc after receiving data from git-push and updating refs.
+You can stop it by setting this variable to `false`. This is recommended in gerrit to avoid the
+additional load this creates. Instead schedule gc using link:cmd-gc.html#gc.startTime[gc.startTime]
+and link:cmd-gc.html#gc.interval[gc.interval] or e.g. in a cron job that runs gc in a separate process.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 086e836..96cc67f 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -61,8 +61,7 @@
 in future gerrit releases. To build Gerrit with Java 8 language level, run:
 
 ```
-  $ bazel build --java_toolchain //tools:error_prone_warnings_toolchain_java8
-        :release
+  $ bazel build --java_toolchain //tools:error_prone_warnings_toolchain :release
 ```
 
 [[java-11]]
diff --git a/Documentation/dev-core-plugins.txt b/Documentation/dev-core-plugins.txt
index aa519806..6b777d3 100644
--- a/Documentation/dev-core-plugins.txt
+++ b/Documentation/dev-core-plugins.txt
@@ -170,7 +170,7 @@
 The plugin functionality has gone outside the Gerrit-related scope,
 has a clear scope or conflict with other core plugins or existing and
 planned Gerrit core features.
-
++
 NOTE: The plugin would need to remain core until the planned replacement gets
 implemented. Otherwise the feature is likely missing between the removal and
 planned implementation times.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
index bb7b518..6d81e9b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -150,6 +150,9 @@
   @property({type: Array})
   coverageRanges: CoverageRange[] = [];
 
+  @property({type: Boolean})
+  useNewContextControls = false;
+
   @property({
     type: Array,
     computed: '_computeLeftCoverageRanges(coverageRanges)',
@@ -405,14 +408,16 @@
         diff,
         localPrefs,
         this.diffElement,
-        this._layers
+        this._layers,
+        this.useNewContextControls
       );
     } else if (this.viewMode === DiffViewMode.UNIFIED) {
       builder = new GrDiffBuilderUnified(
         diff,
         localPrefs,
         this.diffElement,
-        this._layers
+        this._layers,
+        this.useNewContextControls
       );
     }
     if (!builder) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
index 7cbbdb9..b10b251 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
@@ -96,47 +96,113 @@
       return [new GrDiffGroup(GrDiffGroupType.BOTH, lines)];
     }
 
-    test('no +10 buttons for 10 or less lines', () => {
-      const contextGroups = createContextGroups({count: 10});
-      const td = builder._createContextControl({}, contextGroups);
-      const buttons = td.querySelectorAll('gr-button.showContext');
+    function createContextSectionForGroups(options) {
+      const section = document.createElement('div');
+      builder._createContextControls(
+          section, createContextGroups(options), DiffViewMode.UNIFIED);
+      return section;
+    }
 
-      assert.equal(buttons.length, 1);
-      assert.equal(buttons[0].textContent, 'Show 10 common lines');
+    suite('old style', () => {
+      setup(() => {
+        builder = new GrDiffBuilder(
+            {content: []}, prefs, null, [], false /* useNewContextControls */);
+      });
+
+      test('no +10 buttons for 10 or less lines', () => {
+        const section = createContextSectionForGroups({count: 10});
+        const buttons = section.querySelectorAll('gr-button.showContext');
+
+        assert.equal(buttons.length, 1);
+        assert.equal(buttons[0].textContent, 'Show 10 common lines');
+      });
+
+      test('context control at the top', () => {
+        builder._numLinesLeft = 50;
+        const section = createContextSectionForGroups({offset: 0, count: 20});
+        const buttons = section.querySelectorAll('gr-button.showContext');
+
+        assert.equal(buttons.length, 2);
+        assert.equal(buttons[0].textContent, 'Show 20 common lines');
+        assert.equal(buttons[1].textContent, '+10 below');
+      });
+
+      test('context control in the middle', () => {
+        builder._numLinesLeft = 50;
+        const section = createContextSectionForGroups({offset: 10, count: 20});
+        const buttons = section.querySelectorAll('gr-button.showContext');
+
+        assert.equal(buttons.length, 3);
+        assert.equal(buttons[0].textContent, '+10 above');
+        assert.equal(buttons[1].textContent, 'Show 20 common lines');
+        assert.equal(buttons[2].textContent, '+10 below');
+      });
+
+      test('context control at the bottom', () => {
+        builder._numLinesLeft = 50;
+        const section = createContextSectionForGroups({offset: 30, count: 20});
+        const buttons = section.querySelectorAll('gr-button.showContext');
+
+        assert.equal(buttons.length, 2);
+        assert.equal(buttons[0].textContent, '+10 above');
+        assert.equal(buttons[1].textContent, 'Show 20 common lines');
+      });
     });
 
-    test('context control at the top', () => {
-      const contextGroups = createContextGroups({offset: 0, count: 20});
-      builder._numLinesLeft = 50;
-      const td = builder._createContextControl({}, contextGroups);
-      const buttons = td.querySelectorAll('gr-button.showContext');
+    suite('new style', () => {
+      setup(() => {
+        builder = new GrDiffBuilder(
+            {content: []}, prefs, null, [], true /* useNewContextControls */);
+      });
 
-      assert.equal(buttons.length, 2);
-      assert.equal(buttons[0].textContent, 'Show 20 common lines');
-      assert.equal(buttons[1].textContent, '+10 below');
-    });
+      test('no +10 buttons for 10 or less lines', () => {
+        const section = createContextSectionForGroups({count: 10});
+        const buttons = section.querySelectorAll('gr-button.showContext');
 
-    test('context control in the middle', () => {
-      const contextGroups = createContextGroups({offset: 10, count: 20});
-      builder._numLinesLeft = 50;
-      const td = builder._createContextControl({}, contextGroups);
-      const buttons = td.querySelectorAll('gr-button.showContext');
+        assert.equal(buttons.length, 1);
+        assert.equal(buttons[0].textContent, '+10 common lines');
+      });
 
-      assert.equal(buttons.length, 3);
-      assert.equal(buttons[0].textContent, '+10 above');
-      assert.equal(buttons[1].textContent, 'Show 20 common lines');
-      assert.equal(buttons[2].textContent, '+10 below');
-    });
+      test('context control at the top', () => {
+        builder._numLinesLeft = 50;
+        const section = createContextSectionForGroups({offset: 0, count: 20});
+        const buttons = section.querySelectorAll('gr-button.showContext');
 
-    test('context control at the top', () => {
-      const contextGroups = createContextGroups({offset: 30, count: 20});
-      builder._numLinesLeft = 50;
-      const td = builder._createContextControl({}, contextGroups);
-      const buttons = td.querySelectorAll('gr-button.showContext');
+        assert.equal(buttons.length, 2);
+        assert.equal(buttons[0].textContent, '+20 common lines');
+        assert.equal(buttons[1].textContent, '+10');
 
-      assert.equal(buttons.length, 2);
-      assert.equal(buttons[0].textContent, '+10 above');
-      assert.equal(buttons[1].textContent, 'Show 20 common lines');
+        assert.include([...buttons[0].classList.values()], 'belowButton');
+        assert.include([...buttons[1].classList.values()], 'belowButton');
+      });
+
+      test('context control in the middle', () => {
+        builder._numLinesLeft = 50;
+        const section = createContextSectionForGroups({offset: 10, count: 20});
+        const buttons = section.querySelectorAll('gr-button.showContext');
+
+        assert.equal(buttons.length, 3);
+        assert.equal(buttons[0].textContent, '+20 common lines');
+        assert.equal(buttons[1].textContent, '+10');
+        assert.equal(buttons[2].textContent, '+10');
+
+        assert.include([...buttons[0].classList.values()], 'centeredButton');
+        assert.include([...buttons[1].classList.values()], 'aboveButton');
+        assert.include([...buttons[2].classList.values()], 'belowButton');
+      });
+
+      test('context control at the bottom', () => {
+        builder._numLinesLeft = 50;
+        const section = createContextSectionForGroups({offset: 30, count: 20});
+        const buttons = section.querySelectorAll('gr-button.showContext');
+
+        assert.equal(buttons.length, 2);
+        assert.equal(buttons[0].textContent, '+20 common lines');
+        assert.equal(buttons[1].textContent, '+10');
+
+        assert.include([...buttons[0].classList.values()], 'aboveButton');
+        assert.include([...buttons[1].classList.values()], 'aboveButton');
+      });
     });
   });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index 0f7eb43..657dfa2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -19,7 +19,7 @@
 import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
 import {DiffInfo, DiffPreferencesInfo} from '../../../types/common';
 import {GrDiffLine, LineNumber} from '../gr-diff/gr-diff-line';
-import {Side} from '../../../constants/constants';
+import {DiffViewMode, Side} from '../../../constants/constants';
 
 export class GrDiffBuilderSideBySide extends GrDiffBuilder {
   constructor(
@@ -27,9 +27,10 @@
     prefs: DiffPreferencesInfo,
     outputEl: HTMLElement,
     // TODO(TS): Replace any by a layer interface.
-    readonly layers: any[] = []
+    readonly layers: any[] = [],
+    useNewContextControls = false
   ) {
-    super(diff, prefs, outputEl, layers);
+    super(diff, prefs, outputEl, layers, useNewContextControls);
   }
 
   _getMoveControlsConfig() {
@@ -57,8 +58,10 @@
       sectionEl.classList.add('ignoredWhitespaceOnly');
     }
     if (group.type === GrDiffGroupType.CONTEXT_CONTROL) {
-      sectionEl.appendChild(
-        this._createContextRow(sectionEl, group.contextGroups)
+      this._createContextControls(
+        sectionEl,
+        group.contextGroups,
+        DiffViewMode.SIDE_BY_SIDE
       );
       return sectionEl;
     }
@@ -122,21 +125,6 @@
     row.appendChild(this._createTextEl(lineNumberEl, line, side));
   }
 
-  _createContextRow(section: HTMLElement, contextGroups: GrDiffGroup[]) {
-    const row = this._createElement('tr');
-    row.classList.add('diff-row', 'side-by-side');
-    row.setAttribute('left-type', GrDiffGroupType.CONTEXT_CONTROL);
-    row.setAttribute('right-type', GrDiffGroupType.CONTEXT_CONTROL);
-    row.tabIndex = -1;
-
-    row.appendChild(this._createBlameCell(0));
-    row.appendChild(this._createElement('td', 'contextLineNum'));
-    row.appendChild(this._createContextControl(section, contextGroups));
-    row.appendChild(this._createElement('td', 'contextLineNum'));
-    row.appendChild(this._createContextControl(section, contextGroups));
-    return row;
-  }
-
   _getNextContentOnSide(content: HTMLElement, side: Side): HTMLElement | null {
     let tr: HTMLElement = content.parentElement!.parentElement!;
     while ((tr = tr.nextSibling as HTMLElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
index 7c070e5..2028b0c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
@@ -18,7 +18,7 @@
 import {GrDiffBuilder} from './gr-diff-builder';
 import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
 import {DiffInfo, DiffPreferencesInfo} from '../../../types/common';
-import {Side} from '../../../constants/constants';
+import {DiffViewMode, Side} from '../../../constants/constants';
 
 export class GrDiffBuilderUnified extends GrDiffBuilder {
   constructor(
@@ -26,9 +26,10 @@
     prefs: DiffPreferencesInfo,
     outputEl: HTMLElement,
     // TODO(TS): Replace any by a layer interface.
-    readonly layers: any[] = []
+    readonly layers: any[] = [],
+    useNewContextControls = false
   ) {
-    super(diff, prefs, outputEl, layers);
+    super(diff, prefs, outputEl, layers, useNewContextControls);
   }
 
   _getMoveControlsConfig() {
@@ -56,8 +57,10 @@
       sectionEl.classList.add('ignoredWhitespaceOnly');
     }
     if (group.type === GrDiffGroupType.CONTEXT_CONTROL) {
-      sectionEl.appendChild(
-        this._createContextRow(sectionEl, group.contextGroups)
+      this._createContextControls(
+        sectionEl,
+        group.contextGroups,
+        DiffViewMode.UNIFIED
       );
       return sectionEl;
     }
@@ -121,17 +124,6 @@
     return row;
   }
 
-  _createContextRow(section: HTMLElement, contextGroups: GrDiffGroup[]) {
-    const row = this._createElement('tr', GrDiffGroupType.CONTEXT_CONTROL);
-    row.classList.add('diff-row', 'unified');
-    row.tabIndex = -1;
-    row.appendChild(this._createBlameCell(0));
-    row.appendChild(this._createElement('td', 'contextLineNum'));
-    row.appendChild(this._createElement('td', 'contextLineNum'));
-    row.appendChild(this._createContextControl(section, contextGroups));
-    return row;
-  }
-
   _getNextContentOnSide(content: HTMLElement, side: Side): HTMLElement | null {
     let tr: HTMLElement = content.parentElement!.parentElement!;
     while ((tr = tr.nextSibling as HTMLElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index 91fd137..29af31c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -24,7 +24,7 @@
   rangeBySide,
 } from '../gr-diff/gr-diff-group';
 import {BlameInfo, DiffInfo, DiffPreferencesInfo} from '../../../types/common';
-import {Side} from '../../../constants/constants';
+import {DiffViewMode, Side} from '../../../constants/constants';
 import {DiffLayer} from '../../../types/types';
 
 /**
@@ -92,7 +92,8 @@
     diff: DiffInfo,
     prefs: DiffPreferencesInfo,
     outputEl: HTMLElement,
-    readonly layers: DiffLayer[] = []
+    readonly layers: DiffLayer[] = [],
+    protected readonly useNewContextControls: boolean = false
   ) {
     this._diff = diff;
     this._numLinesLeft = this._diff.content
@@ -304,25 +305,116 @@
     );
   }
 
-  _createContextControl(
+  _createContextControls(
     section: HTMLElement,
-    contextGroups: GrDiffGroup[]
-  ): HTMLElement {
+    contextGroups: GrDiffGroup[],
+    viewMode: DiffViewMode
+  ) {
     const leftStart = contextGroups[0].lineRange.left.start!;
     const leftEnd = contextGroups[contextGroups.length - 1].lineRange.left.end!;
     const numLines = leftEnd - leftStart + 1;
 
     if (numLines === 0) console.error('context group without lines');
 
-    const td = this._createElement('td');
-    const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
     const firstGroupIsSkipped = !!contextGroups[0].skip;
     const lastGroupIsSkipped = !!contextGroups[contextGroups.length - 1].skip;
-    const showAboveButton =
-      showPartialLinks && leftStart > 1 && !firstGroupIsSkipped;
-    const showBelowButton =
-      showPartialLinks && leftEnd < this._numLinesLeft && !lastGroupIsSkipped;
-    if (showAboveButton) {
+
+    const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
+    const showAbove = leftStart > 1 && !firstGroupIsSkipped;
+    const showBelow = leftEnd < this._numLinesLeft && !lastGroupIsSkipped;
+
+    if (this.useNewContextControls) {
+      section.classList.add('newStyle');
+      if (showAbove) {
+        const paddingRow = this._createContextControlPaddingRow(viewMode);
+        paddingRow.classList.add('above');
+        section.appendChild(paddingRow);
+      }
+      section.appendChild(
+        this._createNewContextControlRow(
+          section,
+          contextGroups,
+          showAbove,
+          showBelow,
+          numLines
+        )
+      );
+      if (showBelow) {
+        const paddingRow = this._createContextControlPaddingRow(viewMode);
+        paddingRow.classList.add('below');
+        section.appendChild(paddingRow);
+      }
+    } else {
+      section.appendChild(
+        this._createOldContextControlRow(
+          section,
+          contextGroups,
+          viewMode,
+          showAbove && showPartialLinks,
+          showBelow && showPartialLinks,
+          numLines
+        )
+      );
+    }
+  }
+
+  /**
+   * Creates old-style context controls: a single row of "+X above" and
+   * "+X below" buttons.
+   */
+  _createOldContextControlRow(
+    section: HTMLElement,
+    contextGroups: GrDiffGroup[],
+    viewMode: DiffViewMode,
+    showAbove: boolean,
+    showBelow: boolean,
+    numLines: number
+  ) {
+    const row = this._createElement('tr', GrDiffGroupType.CONTEXT_CONTROL);
+
+    row.classList.add('diff-row');
+    row.classList.add(
+      viewMode === DiffViewMode.SIDE_BY_SIDE ? 'side-by-side' : 'unified'
+    );
+
+    row.tabIndex = -1;
+    row.appendChild(this._createBlameCell(0));
+    row.appendChild(this._createElement('td', 'contextLineNum'));
+    if (viewMode === DiffViewMode.SIDE_BY_SIDE) {
+      row.appendChild(
+        this._createOldContextControlButtons(
+          section,
+          contextGroups,
+          showAbove,
+          showBelow,
+          numLines
+        )
+      );
+    }
+    row.appendChild(this._createElement('td', 'contextLineNum'));
+    row.appendChild(
+      this._createOldContextControlButtons(
+        section,
+        contextGroups,
+        showAbove,
+        showBelow,
+        numLines
+      )
+    );
+
+    return row;
+  }
+
+  _createOldContextControlButtons(
+    section: HTMLElement,
+    contextGroups: GrDiffGroup[],
+    showAbove: boolean,
+    showBelow: boolean,
+    numLines: number
+  ): HTMLElement {
+    const td = this._createElement('td');
+
+    if (showAbove) {
       td.appendChild(
         this._createContextButton(
           ContextButtonType.ABOVE,
@@ -332,6 +424,7 @@
         )
       );
     }
+
     td.appendChild(
       this._createContextButton(
         ContextButtonType.ALL,
@@ -340,7 +433,8 @@
         numLines
       )
     );
-    if (showBelowButton) {
+
+    if (showBelow) {
       td.appendChild(
         this._createContextButton(
           ContextButtonType.BELOW,
@@ -350,9 +444,104 @@
         )
       );
     }
+
     return td;
   }
 
+  /**
+   * Creates new-style context controls: buttons extend from the gap created by
+   * this method up or down into the area of code that they affect.
+   */
+  _createNewContextControlRow(
+    section: HTMLElement,
+    contextGroups: GrDiffGroup[],
+    showAbove: boolean,
+    showBelow: boolean,
+    numLines: number
+  ): HTMLElement {
+    const row = this._createElement('tr', 'contextDivider');
+    if (!(showAbove && showBelow)) {
+      row.classList.add('collapsed');
+    }
+
+    const element = this._createElement('td', 'dividerCell');
+    row.appendChild(element);
+
+    const showAllContainer = this._createElement('div', 'aboveBelowButtons');
+    element.appendChild(showAllContainer);
+
+    const showAllButton = this._createContextButton(
+      ContextButtonType.ALL,
+      section,
+      contextGroups,
+      numLines
+    );
+    showAllButton.classList.add(
+      showAbove && showBelow
+        ? 'centeredButton'
+        : showAbove
+        ? 'aboveButton'
+        : 'belowButton'
+    );
+    showAllContainer.appendChild(showAllButton);
+
+    const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
+    if (showPartialLinks) {
+      const container = this._createElement('div', 'aboveBelowButtons');
+      if (showAbove) {
+        container.appendChild(
+          this._createContextButton(
+            ContextButtonType.ABOVE,
+            section,
+            contextGroups,
+            numLines
+          )
+        );
+      }
+      if (showBelow) {
+        container.appendChild(
+          this._createContextButton(
+            ContextButtonType.BELOW,
+            section,
+            contextGroups,
+            numLines
+          )
+        );
+      }
+      element.appendChild(container);
+    }
+
+    return row;
+  }
+
+  /**
+   * Creates a table row to serve as padding between code and context controls.
+   * Blame column, line gutters, and content area will continue visually, but
+   * context controls can render over this background to map more clearly to
+   * the area of code they expand.
+   */
+  _createContextControlPaddingRow(viewMode: DiffViewMode) {
+    const row = this._createElement('tr', 'contextBackground');
+
+    if (viewMode === DiffViewMode.SIDE_BY_SIDE) {
+      row.classList.add('side-by-side');
+      row.setAttribute('left-type', GrDiffGroupType.CONTEXT_CONTROL);
+      row.setAttribute('right-type', GrDiffGroupType.CONTEXT_CONTROL);
+    } else {
+      row.classList.add('unified');
+    }
+
+    row.appendChild(this._createBlameCell(0));
+    row.appendChild(this._createElement('td', 'contextLineNum'));
+    if (viewMode === DiffViewMode.SIDE_BY_SIDE) {
+      row.appendChild(this._createElement('td'));
+    }
+    row.appendChild(this._createElement('td', 'contextLineNum'));
+    row.appendChild(this._createElement('td'));
+
+    return row;
+  }
+
   _createContextButton(
     type: ContextButtonType,
     section: HTMLElement,
@@ -361,6 +550,9 @@
   ) {
     const context = PARTIAL_CONTEXT_AMOUNT;
     const button = this._createElement('gr-button', 'showContext');
+    if (this.useNewContextControls) {
+      button.classList.add('contextControlButton');
+    }
     button.setAttribute('link', 'true');
     button.setAttribute('no-uppercase', 'true');
 
@@ -368,26 +560,39 @@
     let groups: GrDiffGroup[] = []; // The groups that replace this one if tapped.
     let requiresLoad = false;
     if (type === GrDiffBuilder.ContextButtonType.ALL) {
-      requiresLoad = contextGroups.find(c => !!c.skip) !== undefined;
-      const icon = this._createElement('iron-icon', 'showContext');
-      icon.setAttribute('icon', 'gr-icons:unfold-more');
-      button.appendChild(icon);
-
-      text = `Show ${numLines} common line`;
+      if (this.useNewContextControls) {
+        text = `+${numLines} common line`;
+      } else {
+        text = `Show ${numLines} common line`;
+        const icon = this._createElement('iron-icon', 'showContext');
+        icon.setAttribute('icon', 'gr-icons:unfold-more');
+        button.appendChild(icon);
+      }
       if (numLines > 1) {
         text += 's';
       }
+      requiresLoad = contextGroups.find(c => !!c.skip) !== undefined;
       if (requiresLoad) {
         // Expanding content would require load of more data
         text += ' (too large)';
       }
       groups.push(...contextGroups);
     } else if (type === GrDiffBuilder.ContextButtonType.ABOVE) {
-      text = `+${context} above`;
       groups = hideInContextControl(contextGroups, context, numLines);
+      if (this.useNewContextControls) {
+        text = `+${context}`;
+        button.classList.add('aboveButton');
+      } else {
+        text = `+${context} above`;
+      }
     } else if (type === GrDiffBuilder.ContextButtonType.BELOW) {
-      text = `+${context} below`;
       groups = hideInContextControl(contextGroups, 0, numLines - context);
+      if (this.useNewContextControls) {
+        text = `+${context}`;
+        button.classList.add('belowButton');
+      } else {
+        text = `+${context} below`;
+      }
     }
     const textSpan = this._createElement('span', 'showContext');
     textSpan.textContent = text;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
index e7fb30e..e42eb84 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -20,6 +20,7 @@
   AbortStop,
   CursorMoveResult,
   GrCursorManager,
+  Stop,
   isTargetable,
 } from '../../shared/gr-cursor-manager/gr-cursor-manager';
 import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
@@ -525,7 +526,7 @@
 
   _updateStops() {
     this.$.cursorManager.stops = this.diffs.reduce(
-      (stops: HTMLElement[], diff) => stops.concat(diff.getCursorStops()),
+      (stops: Stop[], diff) => stops.concat(diff.getCursorStops()),
       []
     );
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
index 8e95f3d..5619acc 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
@@ -19,8 +19,9 @@
 import '../gr-diff/gr-diff.js';
 import './gr-diff-cursor.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {listenOnce} from '../../../test/test-utils.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 
 const basicFixture = fixtureFromTemplate(html`
   <gr-diff></gr-diff>
@@ -490,5 +491,78 @@
       someEmptyDiv.appendChild(cursorElement);
     });
   });
+
+  suite('multi diff', () => {
+    const multiDiffFixture = fixtureFromTemplate(html`
+      <gr-diff></gr-diff>
+      <gr-diff></gr-diff>
+      <gr-diff></gr-diff>
+      <gr-diff-cursor></gr-diff-cursor>
+      <gr-rest-api-interface></gr-rest-api-interface>
+    `);
+
+    let diffElements;
+
+    setup(async () => {
+      const fixtureElems = multiDiffFixture.instantiate();
+      diffElements = fixtureElems.slice(0, 3);
+      cursorElement = fixtureElems[3];
+      const restAPI = fixtureElems[4];
+
+      // Register the diff with the cursor.
+      cursorElement.push('diffs', ...diffElements);
+
+      await restAPI.getDiffPreferences().then(prefs => {
+        for (const el of diffElements) {
+          el.prefs = prefs;
+        }
+      });
+    });
+
+    function getTargetDiffIndex() {
+      // Mocha has a bug where when `assert.equals` fails, it will try to
+      // JSON.stringify the operands, which fails when they are cyclic structures
+      // like GrDiffElement. The failure is difficult to attribute to a specific
+      // assertion because of the async nature assertion errors are handled and
+      // can cause the test simply timing out, causing a lot of debugging headache.
+      // Working with indices circumvents the problem.
+      return diffElements.indexOf(cursorElement.getTargetDiffElement());
+    }
+
+    test('do not skip loading diffs', async () => {
+      const diffRenderedPromises =
+          diffElements.map(diffEl => listenOnce(diffEl, 'render'));
+
+      diffElements[0].diff = getMockDiffResponse();
+      diffElements[2].diff = getMockDiffResponse();
+      await Promise.all([diffRenderedPromises[0], diffRenderedPromises[2]]);
+
+      const lastLine = diffElements[0].diff.meta_b.lines;
+
+      // Goto second last line of the first diff
+      cursorElement.moveToLineNumber(lastLine - 1, 'right');
+      assert.equal(
+          cursorElement.getTargetLineElement().textContent, lastLine - 1);
+
+      // Can move down until we reach the loading file
+      cursorElement.moveDown();
+      assert.equal(getTargetDiffIndex(), 0);
+      assert.equal(cursorElement.getTargetLineElement().textContent, lastLine);
+
+      // Cannot move down while still loading the diff we would switch to
+      cursorElement.moveDown();
+      assert.equal(getTargetDiffIndex(), 0);
+      assert.equal(cursorElement.getTargetLineElement().textContent, lastLine);
+
+      // Diff 1 finishing to load
+      diffElements[1].diff = getMockDiffResponse();
+      await diffRenderedPromises[1];
+
+      // Now we can go down
+      cursorElement.moveDown();
+      assert.equal(getTargetDiffIndex(), 1);
+      assert.equal(cursorElement.getTargetLineElement().textContent, 'File');
+    });
+  });
 });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index 50112aa..c6f5d21 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -71,6 +71,7 @@
 import {LineNumber} from '../gr-diff/gr-diff-line';
 import {GrCommentThread} from '../../shared/gr-comment-thread/gr-comment-thread';
 import {PatchSetFile} from '../../../types/types';
+import {KnownExperimentId} from '../../../services/flags/flags';
 
 const MSG_EMPTY_BLAME = 'No blame information for this diff.';
 
@@ -273,6 +274,8 @@
 
   private readonly reporting = appContext.reportingService;
 
+  private readonly flags = appContext.flagsService;
+
   /** @override */
   created() {
     super.created();
@@ -1197,6 +1200,10 @@
   _showNewlineWarningRight(diff?: DiffInfo) {
     return this._hasTrailingNewlines(diff, false) === false;
   }
+
+  _useNewContextControls() {
+    return this.flags.isEnabled(KnownExperimentId.NEW_CONTEXT_CONTROLS);
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.ts
index d1564b0..9921dd6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.ts
@@ -41,6 +41,7 @@
     diff="[[diff]]"
     show-newline-warning-left="[[_showNewlineWarningLeft(diff)]]"
     show-newline-warning-right="[[_showNewlineWarningRight(diff)]]"
+    use-new-context-controls="[[_useNewContextControls()]]"
   >
   </gr-diff>
   <gr-syntax-layer
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
index 08ea1a6..ab7ab8a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
@@ -307,8 +307,10 @@
       state.lineNums.right + 1
     );
 
-    if (this.context !== WHOLE_FILE) {
-      const hiddenStart = state.chunkIndex === 0 ? 0 : this.context;
+    const hasSkippedGroup = !!groups.find(g => g.skip);
+    if (this.context !== WHOLE_FILE || hasSkippedGroup) {
+      const contextNumLines = this.context > 0 ? this.context : 0;
+      const hiddenStart = state.chunkIndex === 0 ? 0 : contextNumLines;
       const hiddenEnd =
         lineCount -
         (firstUncollapsibleChunkIndex === chunks.length ? 0 : this.context);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.js
index eed5900..ce7a3c4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.js
@@ -770,6 +770,51 @@
             state.lineNums.right + rows.length);
       });
 
+      test('WHOLE_FILE with skip chunks still get collapsed', () => {
+        element.context = WHOLE_FILE;
+        const lineNums = {left: 10, right: 100};
+        const state = {
+          lineNums,
+          chunkIndex: 1,
+        };
+        const skip = 10000;
+        const chunks = [
+          {a: ['foo']},
+          {skip},
+          {ab: rows},
+          {a: ['bar']},
+        ];
+        const result = element._processNext(state, chunks);
+        // Results in one, uncollapsed group with all rows.
+        assert.equal(result.groups.length, 1);
+        assert.equal(result.groups[0].type, GrDiffGroupType.CONTEXT_CONTROL);
+
+        // Skip and ab group are hidden in the same context control
+        assert.equal(result.groups[0].contextGroups.length, 2);
+        const [skippedGroup, abGroup] = result.groups[0].contextGroups;
+
+        // Line numbers are set correctly.
+        assert.deepEqual(
+            skippedGroup.lineRange,
+            {
+              left: {start: lineNums.left + 1, end: lineNums.left + skip},
+              right: {start: lineNums.right + 1, end: lineNums.right + skip},
+            });
+
+        assert.deepEqual(
+            abGroup.lineRange,
+            {
+              left: {
+                start: lineNums.left + skip + 1,
+                end: lineNums.left + skip + rows.length,
+              },
+              right: {
+                start: lineNums.right + skip + 1,
+                end: lineNums.right + skip + rows.length,
+              },
+            });
+      });
+
       test('with context', () => {
         element.context = 10;
         const state = {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 7c317bb..14b8666 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -54,6 +54,7 @@
 import {KeyLocations} from '../gr-diff-processor/gr-diff-processor';
 import {FlattenedNodesObserver} from '@polymer/polymer/lib/utils/flattened-nodes-observer';
 import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
+import {AbortStop} from '../../shared/gr-cursor-manager/gr-cursor-manager';
 
 const NO_NEWLINE_BASE = 'No newline at end of base file.';
 const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
@@ -193,7 +194,7 @@
 
   /** True when diff is changed, until the content is done rendering. */
   @property({type: Boolean})
-  _loading = false;
+  _loading = true;
 
   @property({type: Boolean})
   loggedIn = false;
@@ -240,6 +241,9 @@
   @property({type: Boolean})
   showNewlineWarningRight = false;
 
+  @property({type: Boolean})
+  useNewContextControls = false;
+
   @property({
     type: String,
     computed:
@@ -456,14 +460,17 @@
     this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME);
   }
 
-  getCursorStops(): HTMLElement[] {
+  getCursorStops(): Array<HTMLElement | AbortStop> {
     if (this.hidden && this.noAutoRender) return [];
-    if (!this.root) return [];
+
+    if (this._loading) {
+      return [new AbortStop()];
+    }
 
     return Array.from(
-      this.root.querySelectorAll<HTMLElement>(
+      this.root?.querySelectorAll<HTMLElement>(
         ':not(.contextControl) > .diff-row'
-      )
+      ) || []
     ).filter(tr => tr.querySelector('button'));
   }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
index e9de9e7..48a4596 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
@@ -46,9 +46,38 @@
     }
     table {
       border-collapse: collapse;
-      border-right: 1px solid var(--border-color);
       table-layout: fixed;
     }
+
+    /*
+      Context controls break up the table visually, so we set the right border
+      on individual sections to leave a gap for the divider.
+      */
+    .section {
+      border-right: 1px solid var(--border-color);
+    }
+    .section.contextControl.newStyle {
+      /*
+       * Divider inside this section must not have border; we set borders on
+       * the padding rows below.
+       */
+      border-right-width: 0;
+    }
+    /*
+     * Padding rows behind new style context controls. The diff is styled to be
+     * cut into two halves by the negative space of the divider on which the
+     * context control buttons are anchored.
+     */
+    .contextBackground {
+      border-right: 1px solid var(--border-color);
+    }
+    .contextBackground.above {
+      border-bottom: 1px solid var(--border-color);
+    }
+    .contextBackground.below {
+      border-top: 1px solid var(--border-color);
+    }
+
     .lineNumButton {
       display: block;
       width: 100%;
@@ -206,12 +235,22 @@
       /* Newline, to ensure empty lines are one line-height tall. */
       content: '\\A';
     }
+
+    /* Context controls */
     .contextControl {
       background-color: var(--diff-context-control-background-color);
       border: 1px solid var(--diff-context-control-border-color);
       color: var(--diff-context-control-color);
+      --divider-height: var(--spacing-s);
+      --divider-border: 1px;
     }
-    .contextControl gr-button {
+    .contextControl.newStyle {
+      background-color: transparent;
+      border: none;
+      /* Change to --diff-context-control-color once only new style exists. */
+      --diff-context-control-color: var(--default-button-text-color);
+    }
+    .contextControl:not(.newStyle) gr-button {
       display: inline-block;
       text-decoration: none;
       vertical-align: top;
@@ -229,6 +268,97 @@
     .contextControl td:not(.lineNumButton) {
       text-align: center;
     }
+
+    /*
+     * Padding rows behind new style context controls. Styled as a continuation
+     * of the line gutters and code area.
+     */
+    .contextBackground > .contextLineNum {
+      background-color: var(--diff-blank-background-color);
+    }
+    .contextBackground > td:not(.contextLineNum) {
+      background-color: var(--view-background-color);
+    }
+    .contextBackground {
+      /* 
+       * One line of background behind the context expanders which they can 
+       * render on top of, plus some padding.
+       */
+      height: calc(var(--line-height-normal) + var(--spacing-s));
+    }
+
+    .contextDivider {
+      height: var(--divider-height);
+      /* Create a positioning context. */
+      transform: translateX(0px);
+    }
+    .contextDivider.collapsed {
+      /* Hide divider gap, but still show child elements (expansion buttons). */
+      height: 0;
+    }
+    .dividerCell {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      justify-content: center;
+      position: absolute;
+      top: 0;
+      left: 0;
+    }
+    .contextControlButton {
+      background-color: var(--default-button-background-color);
+      font: var(--context-control-button-font, inherit);
+      /* All position is relative to container, so ignore sibling buttons. */
+      position: absolute;
+    }
+    .contextControlButton:first-child {
+      /* First button needs to claim width to display without text wrapping. */
+      position: relative;
+    }
+    .centeredButton {
+      /* Center over divider. */
+      top: 50%;
+      transform: translateY(-50%);
+      --gr-button: {
+        color: var(--diff-context-control-color);
+        border: solid var(--border-color);
+        border-width: 1px;
+        border-radius: var(--border-radius);
+        padding: var(--spacing-s) var(--spacing-l);
+      }
+    }
+    .aboveBelowButtons {
+      display: flex;
+      flex-direction: column;
+      margin-left: var(--spacing-m);
+      position: relative;
+    }
+    .aboveBelowButtons:first-child {
+      margin-left: 0;
+    }
+    .aboveButton {
+      /* Display over preceding content / background placeholder. */
+      transform: translateY(-100%);
+      --gr-button: {
+        color: var(--diff-context-control-color);
+        border: solid var(--border-color);
+        border-width: 1px 1px 0 1px;
+        border-radius: var(--border-radius) var(--border-radius) 0 0;
+        padding: var(--spacing-xxs) var(--spacing-l);
+      }
+    }
+    .belowButton {
+      /* Display over following content / background placeholder. */
+      top: calc(100% + var(--divider-border));
+      --gr-button: {
+        color: var(--diff-context-control-color);
+        border: solid var(--border-color);
+        border-width: 0 1px 1px 1px;
+        border-radius: 0 0 var(--border-radius) var(--border-radius);
+        padding: var(--spacing-xxs) var(--spacing-l);
+      }
+    }
+
     .displayLine .diff-row.target-row td {
       box-shadow: inset 0 -1px var(--border-color);
     }
@@ -437,6 +567,7 @@
           base-image="[[baseImage]]"
           layers="[[layers]]"
           revision-image="[[revisionImage]]"
+          use-new-context-controls="[[useNewContextControls]]"
         >
           <table
             id="diffTable"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index 36b3b8f..5e95d83 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -585,7 +585,7 @@
     });
 
     suite('getCursorStops', () => {
-      const setupDiff = function() {
+      function setupDiff() {
         element.diff = getMockDiffResponse();
         element.prefs = {
           context: 10,
@@ -605,8 +605,9 @@
         };
 
         element._renderDiffTable();
+        element._loading = false;
         flush();
-      };
+      }
 
       test('getCursorStops returns [] when hidden and noAutoRender', () => {
         element.noAutoRender = true;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
index ed44807..52465b3 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
@@ -45,9 +45,6 @@
         --native-select-style: {
           max-width: 5.25em;
         }
-        --dropdown-content-stype: {
-          max-width: 300px;
-        }
       }
     }
   </style>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
index 30e0143..26a6b3f 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
@@ -35,9 +35,7 @@
       background-color: var(--dropdown-background-color);
       box-shadow: var(--elevation-level-2);
       max-height: 70vh;
-      margin-top: var(--spacing-xxl);
       min-width: 266px;
-      @apply --dropdown-content-style;
     }
     paper-listbox {
       --paper-listbox: {
@@ -136,6 +134,9 @@
   <iron-dropdown
     id="dropdown"
     vertical-align="top"
+    horizontal-align="left"
+    dynamic-align
+    no-overlap
     allow-outside-scroll="true"
     on-click="_handleDropdownClick"
   >
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 9b62718..3641815 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -134,6 +134,7 @@
     --font-weight-h1: 400;
     --font-weight-h2: 400;
     --font-weight-h3: 400;
+    --context-control-button-font: var(--font-weight-normal) var(--font-size-normal) var(--font-family);
 
     /* spacing */
     --spacing-xxs: 1px;
diff --git a/tools/release_noter/release_noter.py b/tools/release_noter/release_noter.py
index 4b10620..05fa023 100644
--- a/tools/release_noter/release_noter.py
+++ b/tools/release_noter/release_noter.py
@@ -187,12 +187,14 @@
 class Commit:
     sha1 = None
     subject = None
+    component = None
     issues = set()
 
     def reset(self, signature, task):
         if signature is not None:
             self.sha1 = signature.group(1)
             self.subject = None
+            self.component = None
             self.issues = set()
             return Task.finish_headers
         return task
@@ -238,24 +240,25 @@
                 if noted_commit.subject == commit.subject:
                     return Commit()
     set_component(commit, commits, cwd)
-    link_subject(commit, gerrit, options)
+    link_subject(commit, gerrit, options, cwd)
     escape_these(commit)
     return Commit()
 
 
 def set_component(commit, commits, cwd):
-    component_found = False
+    component_found = None
     for component in Components:
         for sentinel in component.value.sentinels:
-            if not component_found:
+            if component_found is None:
                 if re.match(f"{GIT_PATH}/{PLUGINS}{component.value.name.lower()}", cwd):
-                    component_found = True
+                    component_found = component
                 elif sentinel.lower() in commit.subject.lower():
-                    component_found = True
-                if component_found:
+                    component_found = component
+                if component_found is not None:
                     commits[component].append(commit)
-    if not component_found:
+    if component_found is None:
         commits[Components.otherwise].append(commit)
+    commit.component = component_found
 
 
 def init_components():
@@ -265,13 +268,17 @@
     return components
 
 
-def link_subject(commit, gerrit, options):
+def link_subject(commit, gerrit, options, cwd):
     if options.link:
         gerrit_change = gerrit.get(f"{COMMIT_URL}{commit.sha1}")
         if not gerrit_change:
             return
         change_number = gerrit_change[0]["_number"]
-        change_address = f"{GERRIT_URL}{CHANGE_URL}{change_number}"
+        plugin_wd = re.search(f"{GIT_PATH}/({PLUGINS}.+)", cwd)
+        if plugin_wd is not None:
+            change_address = f"{GERRIT_URL}/c/{plugin_wd.group(1)}/+/{change_number}"
+        else:
+            change_address = f"{GERRIT_URL}{CHANGE_URL}{change_number}"
         short_sha1 = commit.sha1[0:7]
         commit.subject = f"[{short_sha1}]({change_address})\n  {commit.subject}"