Merge "Make the trailing-whitespace CSS rule more specific"
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 81c9927..8324a72 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -143,6 +143,18 @@
 rights directly to a single user, since in Gerrit access rights can
 only be assigned to groups.
 
+[[webhooks]]
+=== webhooks
+
+This plugin allows to propagate Gerrit events to remote http endpoints.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/webhooks[
+Project] |
+link:https://gerrit.googlesource.com/plugins/webhooks/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/webhooks/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
 [[other-plugins]]
 == Other Plugins
 
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index cb80c74..ff62da1 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -35,6 +35,11 @@
 
 The following endpoints are available to plugins.
 
+=== banner
+The `banner` extension point is located at the top of all pages. The purpose
+is to allow plugins to show outage information and important announcements to
+all users.
+
 === change-view-integration
 The `change-view-integration` extension point is located between `Files` and
 `Messages` section on the change view page, and it may take full page's
diff --git a/WORKSPACE b/WORKSPACE
index b39bc78..3addf85 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -829,15 +829,15 @@
 # elasticsearch-rest-client explicitly depends on this version
 maven_jar(
     name = "httpasyncclient",
-    artifact = "org.apache.httpcomponents:httpasyncclient:4.1.2",
-    sha1 = "95aa3e6fb520191a0970a73cf09f62948ee614be",
+    artifact = "org.apache.httpcomponents:httpasyncclient:4.1.4",
+    sha1 = "f3a3240681faae3fa46b573a4c7e50cec9db0d86",
 )
 
 # elasticsearch-rest-client explicitly depends on this version
 maven_jar(
     name = "httpcore-nio",
-    artifact = "org.apache.httpcomponents:httpcore-nio:4.4.5",
-    sha1 = "f4be009e7505f6ceddf21e7960c759f413f15056",
+    artifact = "org.apache.httpcomponents:httpcore-nio:4.4.11",
+    sha1 = "7d0a97d01d39cff9aa3e6db81f21fddb2435f4e6",
 )
 
 # Test-only dependencies below.
@@ -1059,8 +1059,8 @@
 # and httpasyncclient as necessary.
 maven_jar(
     name = "elasticsearch-rest-client",
-    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.7.1",
-    sha1 = "a00aa6cc593a464e4a890d1a226ded5c625659ab",
+    artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.0.0",
+    sha1 = "121d12f1c71f318be1a654e8a956e38d5b68e98a",
 )
 
 JACKSON_VERSION = "2.9.8"
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 103edd8..b68be77 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -1842,7 +1842,7 @@
     if (magicBranch.deprecatedTopicSeen) {
       messages.add(
           new ValidationMessage(
-              "WARNING: deprecated topic syntax. Use %topic=TOPIC instead", false));
+              "WARNING: deprecated topic syntax. Use -o topic=TOPIC instead", false));
       logger.atInfo().log("deprecated topic push seen for project %s", project.getName());
     }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 479788a..8400d02 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -51,7 +51,7 @@
       case V6_7:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.7.1";
       case V7_0:
-        return "docker.elastic.co/elasticsearch/elasticsearch-oss:7.0.0-rc2";
+        return "docker.elastic.co/elasticsearch/elasticsearch-oss:7.0.0";
     }
     throw new IllegalStateException("No tests for version: " + version.name());
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
index 0ce66e8..f01138a 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_2);
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_7);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/plugins/BUILD b/plugins/BUILD
index c7ad8a4..4837b67 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -42,6 +42,7 @@
     "//java/com/google/gerrit/metrics",
     "//java/com/google/gerrit/metrics/dropwizard",
     "//java/com/google/gerrit/reviewdb:server",
+    "//java/com/google/gerrit/server/api",
     "//java/com/google/gerrit/server/audit",
     "//java/com/google/gerrit/server/cache/mem",
     "//java/com/google/gerrit/server/logging",
@@ -50,6 +51,8 @@
     "//java/com/google/gerrit/util/cli",
     "//java/com/google/gerrit/util/http",
     "//java/com/google/gwtorm",
+    "//lib/antlr:java-runtime",
+    "//lib/auto:auto-value-annotations",
     "//lib/commons:compress",
     "//lib/commons:dbcp",
     "//lib/commons:lang",
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index 7c6fbc6..a1af8e1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -151,7 +151,8 @@
               sectionIndex: 0,
             };
 
-            content = this._splitCommonGroupsWithComments(content);
+            content = this._splitLargeChunks(content);
+            content = this._splitUnchangedChunksWithComments(content);
 
             let currentBatch = 0;
             const nextStep = () => {
@@ -384,60 +385,78 @@
       return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]);
     },
 
+
+    /**
+     * Split chunks into smaller chunks of the same kind.
+     *
+     * This is done to prevent doing too much work on the main thread in one
+     * uninterrupted rendering step, which would make the browser unresponsive.
+     *
+     * Note that in the case of unmodified chunks, we only split chunks if the
+     * context is set to file (because otherwise they are split up further down
+     * the processing into the visible and hidden context), and only split it
+     * into 2 chunks, one max sized one and the rest (for reasons that are
+     * unclear to me).
+     *
+     * @param {!Array<!Object>} chunks Chunks as returned from the server
+     * @return {!Array<!Object>} Finer grained chunks.
+     */
+    _splitLargeChunks(chunks) {
+      const newChunks = [];
+
+      for (const chunk of chunks) {
+        if (!chunk.ab) {
+          for (const group of this._breakdownGroup(chunk)) {
+            newChunks.push(group);
+          }
+          continue;
+        }
+
+        // If the context is set to "whole file", then break down the shared
+        // chunks so they can be rendered incrementally. Note: this is not
+        // enabled for any other context preference because manipulating the
+        // chunks in this way violates assumptions by the context grouper logic.
+        if (this.context === -1 && chunk.ab.length > MAX_GROUP_SIZE * 2) {
+          // Split large shared groups in two, where the first is the maximum
+          // group size.
+          newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)});
+          newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)});
+        } else {
+          newChunks.push(chunk);
+        }
+      }
+      return newChunks;
+    },
+
     /**
      * In order to show comments out of the bounds of the selected context,
      * treat them as separate chunks within the model so that the content (and
      * context surrounding it) renders correctly.
-     * @param {?} content The diff content object. (has to be iterable)
-     * @return {!Object} A new diff content object with regions split up.
+     * @param {!Array<!Object>} chunks DiffContents as returned from server.
+     * @return {!Array<!Object>} Finer grained DiffContents.
      */
-    _splitCommonGroupsWithComments(content) {
+    _splitUnchangedChunksWithComments(chunks) {
       const result = [];
       let leftLineNum = 0;
       let rightLineNum = 0;
 
-      // If the context is set to "whole file", then break down the shared
-      // chunks so they can be rendered incrementally. Note: this is not enabled
-      // for any other context preference because manipulating the chunks in
-      // this way violates assumptions by the context grouper logic.
-      if (this.context === -1) {
-        const newContent = [];
-        for (const group of content) {
-          if (group.ab && group.ab.length > MAX_GROUP_SIZE * 2) {
-            // Split large shared groups in two, where the first is the maximum
-            // group size.
-            newContent.push({ab: group.ab.slice(0, MAX_GROUP_SIZE)});
-            newContent.push({ab: group.ab.slice(MAX_GROUP_SIZE)});
-          } else {
-            newContent.push(group);
+      for (const chunk of chunks) {
+        // If it isn't a common chunk, append it as-is and update line numbers.
+        if (!chunk.ab) {
+          if (chunk.a) {
+            leftLineNum += chunk.a.length;
           }
-        }
-        content = newContent;
-      }
-
-      // For each section in the diff.
-      for (let i = 0; i < content.length; i++) {
-        // If it isn't a common group, append it as-is and update line numbers.
-        if (!content[i].ab) {
-          if (content[i].a) {
-            leftLineNum += content[i].a.length;
+          if (chunk.b) {
+            rightLineNum += chunk.b.length;
           }
-          if (content[i].b) {
-            rightLineNum += content[i].b.length;
-          }
-
-          for (const group of this._breakdownGroup(content[i])) {
-            result.push(group);
-          }
-
+          result.push(chunk);
           continue;
         }
 
-        const chunk = content[i].ab;
         let currentChunk = {ab: []};
 
         // For each line in the common group.
-        for (const subChunk of chunk) {
+        for (const line of chunk.ab) {
           leftLineNum++;
           rightLineNum++;
 
@@ -453,10 +472,10 @@
             }
 
             // Add the non-collapse line as its own chunk.
-            result.push({ab: [subChunk]});
+            result.push({ab: [line]});
           } else {
             // Append the current line to the current chunk.
-            currentChunk.ab.push(subChunk);
+            currentChunk.ab.push(line);
           }
         }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 7ccd9f8..186a49e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -280,7 +280,6 @@
           left: {1: true},
           right: {10: true},
         };
-        const lineNums = {left: 0, right: 0};
 
         const content = [
           {
@@ -304,7 +303,7 @@
           },
         ];
         const result =
-            element._splitCommonGroupsWithComments(content, lineNums);
+            element._splitUnchangedChunksWithComments(content);
         assert.deepEqual(result, [
           {
             ab: ['Copyright (C) 2015 The Android Open Source Project'],
@@ -337,28 +336,25 @@
         ]);
       });
 
-      test('breaks-down shared chunks w/ whole-file', () => {
+      test('breaks down shared chunks w/ whole-file', () => {
         const size = 120 * 2 + 5;
-        const lineNums = {left: 0, right: 0};
         const content = [{
           ab: _.times(size, () => { return `${Math.random()}`; }),
         }];
         element.context = -1;
-        const result =
-            element._splitCommonGroupsWithComments(content, lineNums);
+        const result = element._splitLargeChunks(content);
         assert.equal(result.length, 2);
         assert.deepEqual(result[0].ab, content[0].ab.slice(0, 120));
         assert.deepEqual(result[1].ab, content[0].ab.slice(120));
       });
 
       test('does not break-down shared chunks w/ context', () => {
-        const lineNums = {left: 0, right: 0};
         const content = [{
           ab: _.times(75, () => { return `${Math.random()}`; }),
         }];
         element.context = 4;
         const result =
-            element._splitCommonGroupsWithComments(content, lineNums);
+            element._splitUnchangedChunksWithComments(content);
         assert.deepEqual(result, content);
       });
 
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 1cdf035..e537ac4 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -15,16 +15,18 @@
 limitations under the License.
 -->
 <script>
-  // This must be set prior to loading Polymer for the first time.
-  if (localStorage.getItem('USE_SHADOW_DOM') === 'true') {
-    window.Polymer = {
-      dom: 'shadow',
-      passiveTouchGestures: true,
-    };
-  } else if (!window.Polymer) {
-    window.Polymer = {
-      passiveTouchGestures: true,
-    };
+  if (!window.POLYMER2) {
+    // This must be set prior to loading Polymer for the first time.
+    if (localStorage.getItem('USE_SHADOW_DOM') === 'true') {
+      window.Polymer = {
+        dom: 'shadow',
+        passiveTouchGestures: true,
+      };
+    } else if (!window.Polymer) {
+      window.Polymer = {
+        passiveTouchGestures: true,
+      };
+    }
   }
   // Needed for JSCompiler to understand it's global.
   // eslint-disable-next-line no-unused-vars, prefer-const
@@ -148,6 +150,7 @@
         color: var(--error-text-color);
       }
     </style>
+    <gr-endpoint-decorator name="banner"></gr-endpoint-decorator>
     <gr-fixed-panel id="header">
       <gr-main-header
           id="mainHeader"
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 5889054..a2a495b 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -20,9 +20,10 @@
   // Eagerly render Polymer components when backgrounded. (Skips
   // requestAnimationFrame.)
   // @see https://github.com/Polymer/polymer/issues/3851
-  // TODO: Reassess after Polymer 2.0 upgrade.
   // @see Issue 4699
-  Polymer.RenderStatus._makeReady();
+  if (!window.POLYMER2) {
+    Polymer.RenderStatus._makeReady();
+  }
 
   Polymer({
     is: 'gr-app',
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index e4f699c..85f338c 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -24,6 +24,7 @@
   {@param? faviconPath: ?}
   {@param? versionInfo: ?}
   {@param? deprecateGwtUi: ?}
+  {@param? polymer2: ?}
   <!DOCTYPE html>{\n}
   <html lang="en">{\n}
   <meta charset="utf-8">{\n}
@@ -42,6 +43,7 @@
     {if $versionInfo}window.VERSION_INFO = '{$versionInfo}';{/if}
     {if $staticResourcePath != ''}window.STATIC_RESOURCE_PATH = '{$staticResourcePath}';{/if}
     {if $assetsPath}window.ASSETS_PATH = '{$assetsPath}';{/if}
+    {if $polymer2}window.POLYMER2 = true;{/if}
   </script>{\n}
 
   {if $faviconPath}