Merge changes I1a7e024c,Ia2bfd2cf

* changes:
  Add comments about decoding fields from change index
  ElasticChangeIndex: Decode hashtags and stars
diff --git a/.gitignore b/.gitignore
index 5e9b659..d75067d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
 # Keep following lines sorted according to `LC_COLLATE=C sort`
-*.asc
 *.eml
 *.iml
 *.pyc
diff --git a/java/com/google/gerrit/common/BUILD b/java/com/google/gerrit/common/BUILD
index ceb060a..5f3c1ec 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -35,6 +35,7 @@
         ["**/*.java"],
         exclude = ANNOTATIONS,
     ),
+    resources = [":Version"],
     visibility = ["//visibility:public"],
     deps = [
         ":annotations",
@@ -51,20 +52,9 @@
     ],
 )
 
-java_import(
-    name = "version",
-    jars = [":gen_version"],
-    visibility = ["//visibility:public"],
-)
-
-genrule2(
+genrule(
     name = "gen_version",
-    outs = ["gen_version.jar"],
-    cmd = " && ".join([
-        "cd $$TMP",
-        "mkdir -p com/google/gerrit/common",
-        "cat $$ROOT/$(location //:version.txt) >com/google/gerrit/common/Version",
-        "zip -9Dqr $$ROOT/$@ .",
-    ]),
-    tools = ["//:version.txt"],
+    srcs = ["//:version.txt"],
+    outs = ["Version"],
+    cmd = "cat $< > $@",
 )
diff --git a/java/com/google/gerrit/common/Version.java b/java/com/google/gerrit/common/Version.java
index 8d902a7..1777c3c 100644
--- a/java/com/google/gerrit/common/Version.java
+++ b/java/com/google/gerrit/common/Version.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.GwtIncompatible;
+import com.google.common.annotations.VisibleForTesting;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
@@ -27,20 +28,23 @@
 @GwtIncompatible("Unemulated com.google.gerrit.common.Version")
 public class Version {
   private static final Logger log = LoggerFactory.getLogger(Version.class);
-  private static final String version;
+
+  @VisibleForTesting static final String DEV = "(dev)";
+
+  private static final String VERSION;
 
   public static String getVersion() {
-    return version;
+    return VERSION;
   }
 
   static {
-    version = loadVersion();
+    VERSION = loadVersion();
   }
 
   private static String loadVersion() {
     try (InputStream in = Version.class.getResourceAsStream("Version")) {
       if (in == null) {
-        return "(dev)";
+        return DEV;
       }
       try (BufferedReader r = new BufferedReader(new InputStreamReader(in, UTF_8))) {
         String vs = r.readLine();
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index 0e1b91a..618d754 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -38,6 +38,7 @@
 import java.nio.file.Paths;
 import java.security.CodeSource;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
@@ -595,8 +596,27 @@
   }
 
   /**
+   * Check whether the process is running in Eclipse.
+   *
+   * <p>Unlike {@link #getDeveloperEclipseOut()}, this method checks the actual runtime stack, not
+   * the classpath.
+   *
+   * @return true if any thread has a stack frame in {@code org.eclipse.jdt}.
+   */
+  public static boolean isRunningInEclipse() {
+    return Thread.getAllStackTraces()
+        .values()
+        .stream()
+        .flatMap(Arrays::stream)
+        .anyMatch(e -> e.getClassName().startsWith("org.eclipse.jdt."));
+  }
+
+  /**
    * Locate the path of the {@code eclipse-out} directory in a source tree.
    *
+   * <p>Unlike {@link #isRunningInEclipse()}, this method only inspects files relative to the
+   * classpath, not the runtime stack.
+   *
    * @return local path of the {@code eclipse-out} directory in a source tree.
    * @throws FileNotFoundException if the directory cannot be found.
    */
@@ -669,7 +689,7 @@
   }
 
   private static ClassLoader useDevClasspath() throws MalformedURLException, FileNotFoundException {
-    Path out = resolveInSourceRoot("eclipse-out");
+    Path out = getDeveloperEclipseOut();
     List<URL> dirs = new ArrayList<>();
     dirs.add(out.resolve("classes").toUri().toURL());
     ClassLoader cl = GerritLauncher.class.getClassLoader();
diff --git a/javatests/com/google/gerrit/common/BUILD b/javatests/com/google/gerrit/common/BUILD
index 64a2a46..ca04a4a 100644
--- a/javatests/com/google/gerrit/common/BUILD
+++ b/javatests/com/google/gerrit/common/BUILD
@@ -1,12 +1,15 @@
 load("//tools/bzl:junit.bzl", "junit_tests")
 
-AUTO_VALUE_TEST_SRCS = ["AutoValueTest.java"]
+SERVER_TEST_SRCS = [
+    "AutoValueTest.java",
+    "VersionTest.java",
+]
 
 junit_tests(
     name = "client_tests",
     srcs = glob(
         ["**/*.java"],
-        exclude = AUTO_VALUE_TEST_SRCS,
+        exclude = SERVER_TEST_SRCS,
     ),
     deps = [
         "//java/com/google/gerrit/common:client",
@@ -17,9 +20,12 @@
 )
 
 junit_tests(
-    name = "auto_value_tests",
-    srcs = AUTO_VALUE_TEST_SRCS,
+    name = "server_tests",
+    srcs = SERVER_TEST_SRCS,
     deps = [
+        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/launcher",
+        "//lib:guava",
         "//lib:truth",
         "//lib/auto:auto-value",
     ],
diff --git a/javatests/com/google/gerrit/common/VersionTest.java b/javatests/com/google/gerrit/common/VersionTest.java
new file mode 100644
index 0000000..bceb203
--- /dev/null
+++ b/javatests/com/google/gerrit/common/VersionTest.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.launcher.GerritLauncher;
+import java.util.regex.Pattern;
+import org.junit.Test;
+
+public final class VersionTest {
+  private static final Pattern DEV_PATTERN =
+      Pattern.compile("^" + Pattern.quote(Version.DEV) + "$");
+
+  private static final Pattern GIT_DESCRIBE_PATTERN =
+      Pattern.compile(
+          "^[1-9]+\\.[0-9]+(\\.[0-9]+)*(-rc[0-9]+)?(-[0-9]+" + "-g[0-9a-f]{7,})?(-dirty)?$");
+
+  @Test
+  public void version() {
+    Pattern expected =
+        GerritLauncher.isRunningInEclipse()
+            ? DEV_PATTERN // Different source line so it shows up in coverage.
+            : GIT_DESCRIBE_PATTERN;
+    assertThat(Version.getVersion()).matches(expected);
+    // Try again in case of caching issues.
+    assertThat(Version.getVersion()).matches(expected);
+  }
+
+  @Test
+  public void gitDescribePattern() {
+    for (String suffix : ImmutableList.of("", "-dirty")) {
+      assertThat("2.15-rc0" + suffix).matches(GIT_DESCRIBE_PATTERN);
+      assertThat("2.15-rc0" + suffix).matches(GIT_DESCRIBE_PATTERN);
+      assertThat("2.15-rc1" + suffix).matches(GIT_DESCRIBE_PATTERN);
+      assertThat("2.15" + suffix).matches(GIT_DESCRIBE_PATTERN);
+      assertThat("2.15.1" + suffix).matches(GIT_DESCRIBE_PATTERN);
+      assertThat("2.15.1.2" + suffix).matches(GIT_DESCRIBE_PATTERN);
+      assertThat("2.15.1.2.3" + suffix).matches(GIT_DESCRIBE_PATTERN);
+      assertThat("2.15.1-rc1" + suffix).matches(GIT_DESCRIBE_PATTERN);
+      assertThat("2.15-rc2-123-gabcd123" + suffix).matches(GIT_DESCRIBE_PATTERN);
+      assertThat("2.15-123-gabcd123" + suffix).matches(GIT_DESCRIBE_PATTERN);
+    }
+
+    assertThat("2.15-ugly").doesNotMatch(GIT_DESCRIBE_PATTERN);
+    assertThat("(dev)").doesNotMatch(GIT_DESCRIBE_PATTERN);
+    assertThat("1").doesNotMatch(GIT_DESCRIBE_PATTERN);
+    assertThat("v2.15").doesNotMatch(GIT_DESCRIBE_PATTERN);
+  }
+}
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
index 86b1ae4..3850894 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -23,7 +23,6 @@
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
-<link rel="import" href="../../shared/gr-placeholder/gr-placeholder.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-admin-group-list/gr-admin-group-list.html">
 <link rel="import" href="../gr-group/gr-group.html">
@@ -133,9 +132,6 @@
             project="[[params.project]]"></gr-project-access>
       </main>
     </template>
-    <template is="dom-if" if="[[params.placeholder]]" restamp="true">
-      <gr-placeholder title="Admin" path="[[path]]"></gr-placeholder>
-    </template>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-admin-view.js"></script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 79f06fe..aa40a6e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -104,7 +104,7 @@
     },
 
     _computeProjectURL(project) {
-      return Gerrit.Nav.getUrlForProject(project, true);
+      return Gerrit.Nav.getUrlForProjectChanges(project, true);
     },
 
     _computeProjectBranchURL(change) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index e95c494..32fa7a6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -314,7 +314,7 @@
     },
 
     _computeProjectURL(project) {
-      return Gerrit.Nav.getUrlForProject(project);
+      return Gerrit.Nav.getUrlForProjectChanges(project);
     },
 
     _computeBranchURL(project, branch) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 21cd100..02a0514 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -39,6 +39,8 @@
 
   const REPLY_REFIT_DEBOUNCE_INTERVAL_MS = 500;
 
+  const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
+
   Polymer({
     is: 'gr-change-view',
 
@@ -298,7 +300,8 @@
     },
 
     _handleCommitMessageSave(e) {
-      const message = e.detail.content;
+      // Trim trailing whitespace from each line.
+      const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
 
       this.$.jsAPI.handleCommitMessage(this._change, message);
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 6c95046..d0f3d90 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -638,6 +638,22 @@
           _change));
     });
 
+    test('_handleCommitMessageSave trims trailing whitespace', () => {
+      const putStub = sandbox.stub(element.$.restAPI, 'putChangeCommitMessage')
+          .returns(Promise.resolve({}));
+
+      const mockEvent = content => { return {detail: {content}}; };
+
+      element._handleCommitMessageSave(mockEvent('test \n  test '));
+      assert.equal(putStub.lastCall.args[1], 'test\n  test');
+
+      element._handleCommitMessageSave(mockEvent('  test\ntest'));
+      assert.equal(putStub.lastCall.args[1], '  test\ntest');
+
+      element._handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n'));
+      assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
+    });
+
     test('_computeChangeIdCommitMessageError', () => {
       let commitMessage =
         'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483';
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 054f0f1..ac15882 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -430,6 +430,10 @@
     },
 
     _getFiles() {
+      if (this.editLoaded) {
+        return this.$.restAPI.getChangeEditFilesAsSpeciallySortedArray(
+            this.changeNum, this.patchRange);
+      }
       return this.$.restAPI.getChangeFilesAsSpeciallySortedArray(
           this.changeNum, this.patchRange);
     },
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index be03e38..7e9f843 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -116,6 +116,48 @@
       });
     });
 
+    test('get file list with change edit', done => {
+      element.editLoaded = true;
+
+      sandbox.stub(element.$.restAPI,
+          'getChangeEditFiles', () => {
+            return Promise.resolve({
+              commit: {},
+              files: {
+                '/COMMIT_MSG': {
+                  lines_inserted: 9,
+                },
+                'tags.html': {
+                  lines_deleted: 123,
+                },
+                'about.txt': {},
+              },
+            });
+          });
+
+      element._getFiles().then(files => {
+        const filenames = files.map(f => { return f.__path; });
+        assert.deepEqual(filenames, ['/COMMIT_MSG', 'about.txt', 'tags.html']);
+        assert.deepEqual(files[0], {
+          lines_inserted: 9,
+          lines_deleted: 0,
+          __path: '/COMMIT_MSG',
+        });
+        assert.deepEqual(files[1], {
+          lines_inserted: 0,
+          lines_deleted: 0,
+          __path: 'about.txt',
+        });
+        assert.deepEqual(files[2], {
+          lines_inserted: 0,
+          lines_deleted: 123,
+          __path: 'tags.html',
+        });
+
+        done();
+      });
+    });
+
     test('calculate totals for patch number', () => {
       element._files = [
         {__path: '/COMMIT_MSG', lines_inserted: 9},
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index 6ff78d8..afb3585 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -130,7 +130,7 @@
        *     the project.
        * @return {string}
        */
-      getUrlForProject(project, opt_openOnly) {
+      getUrlForProjectChanges(project, opt_openOnly) {
         return this._getUrlFor({
           view: Gerrit.Nav.View.SEARCH,
           project,
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index e76fecb..aefc3b9 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -20,7 +20,6 @@
     DASHBOARD: /^\/dashboard\/(.+)$/,
     CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
 
-    ADMIN_PLACEHOLDER: '/admin/(.*)',
     AGREEMENTS: /^\/settings\/(agreements|new-agreement)/,
     REGISTER: /^\/register(\/.*)?$/,
 
@@ -600,9 +599,6 @@
 
       this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true);
 
-      this._mapRoute(RoutePattern.ADMIN_PLACEHOLDER,
-          '_handleAdminPlaceholderRoute', true);
-
       this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
 
       this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY,
@@ -1003,12 +999,6 @@
       });
     },
 
-    _handleAdminPlaceholderRoute(data) {
-      data.params.view = Gerrit.Nav.View.ADMIN;
-      data.params.placeholder = true;
-      this._setParams(data.params);
-    },
-
     _handleQueryRoute(data) {
       this._setParams({
         view: Gerrit.Nav.View.SEARCH,
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index b6ee5b5..c2b46f8 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -123,7 +123,6 @@
       actualDoesNotRequireAuth.sort();
 
       const shouldRequireAutoAuth = [
-        '_handleAdminPlaceholderRoute',
         '_handleAgreementsRoute',
         '_handleCreateGroupRoute',
         '_handleCreateProjectRoute',
@@ -522,13 +521,6 @@
         setParamsStub = sandbox.stub(element, '_setParams');
       });
 
-      test('_handleAdminPlaceholderRoute', () => {
-        element._handleAdminPlaceholderRoute({params: {}});
-        assert.equal(setParamsStub.lastCall.args[0].view,
-            Gerrit.Nav.View.ADMIN);
-        assert.isTrue(setParamsStub.lastCall.args[0].placeholder);
-      });
-
       test('_handleAgreementsRoute', () => {
         element._handleAgreementsRoute({params: {}});
         assert.isTrue(setParamsStub.calledOnce);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index dd18b65..1cfab26 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -24,7 +24,7 @@
 <dom-module id="gr-diff-builder">
   <template>
     <div class="contentWrapper">
-      <content></content>
+      <slot></slot>
     </div>
     <gr-ranged-comment-layer
         id="rangeLayer"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index 335ed36..dc75073 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -46,7 +46,8 @@
       :host([disabled]) {
         pointer-events: none;
       }
-      :host([disabled]) .container {
+      :host([disabled]) .body,
+      :host([disabled]) .date {
         opacity: .5;
       }
       :host([is-robot-comment]) {
@@ -210,6 +211,12 @@
       #deleteBtn.showDeleteButtons {
         display: block;
       }
+      #savingMessage {
+        display: none;
+      }
+      :host([disabled]) #savingMessage {
+        display: inline;
+      }
     </style>
     <div id="container"
         class="container"
@@ -224,6 +231,7 @@
               title="This draft is only visible to you. To publish drafts, click the red 'Reply' button at the top of the change or press the 'A' key."
               max-width="20em"
               show-icon></gr-tooltip-content>
+          <span id="savingMessage">[[_savingMessage]]</span>
         </div>
         <div class="headerMiddle">
           <span class="collapsedContent">[[comment.message]]</span>
@@ -249,64 +257,66 @@
           </label>
         </div>
       </div>
-      <template is="dom-if" if="[[comment.robot_id]]">
-        <div class="robotId" hidden$="[[collapsed]]">
-          [[comment.robot_id]]
+      <div class="body">
+        <template is="dom-if" if="[[comment.robot_id]]">
+          <div class="robotId" hidden$="[[collapsed]]">
+            [[comment.robot_id]]
+          </div>
+        </template>
+        <gr-textarea
+            id="editTextarea"
+            class="editMessage"
+            autocomplete="on"
+            monospace
+            disabled="{{disabled}}"
+            rows="4"
+            text="{{_messageText}}"></gr-textarea>
+        <gr-formatted-text class="message"
+            content="[[comment.message]]"
+            no-trailing-margin="[[!comment.__draft]]"
+            collapsed="[[collapsed]]"
+            config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+        <div hidden$="[[!comment.robot_run_id]]">
+          <div class="runIdInformation" hidden$="[[collapsed]]">
+            Run ID:
+            <a class="robotRunLink" href$="[[comment.url]]">
+              <span class="robotRun">[[comment.robot_run_id]]</span>
+            </a>
+          </div>
         </div>
-      </template>
-      <gr-textarea
-          id="editTextarea"
-          class="editMessage"
-          autocomplete="on"
-          monospace
-          disabled="{{disabled}}"
-          rows="4"
-          text="{{_messageText}}"></gr-textarea>
-      <gr-formatted-text class="message"
-          content="[[comment.message]]"
-          no-trailing-margin="[[!comment.__draft]]"
-          collapsed="[[collapsed]]"
-          config="[[projectConfig.commentlinks]]"></gr-formatted-text>
-      <div hidden$="[[!comment.robot_run_id]]">
-        <div class="runIdInformation" hidden$="[[collapsed]]">
-          Run ID:
-          <a class="robotRunLink" href$="[[comment.url]]">
-            <span class="robotRun">[[comment.robot_run_id]]</span>
-          </a>
+        <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
+          <div class="action resolve hideOnPublished">
+            <label>
+              <input type="checkbox"
+                  checked$="[[resolved]]"
+                  on-change="_handleToggleResolved">
+              Resolved
+            </label>
+          </div>
+          <div class="action unresolved hideOnPublished" hidden$="[[resolved]]">
+            Unresolved
+          </div>
+          <div class="rightActions">
+            <gr-button link class="action cancel hideOnPublished"
+                on-tap="_handleCancel" hidden>Cancel</gr-button>
+            <gr-button link class="action discard hideOnPublished"
+                on-tap="_handleDiscard">Discard</gr-button>
+            <gr-button link class="action edit hideOnPublished"
+                on-tap="_handleEdit">Edit</gr-button>
+            <gr-button link class="action save hideOnPublished"
+                on-tap="_handleSave"
+                disabled$="[[_computeSaveDisabled(_messageText)]]">Save
+            </gr-button>
+          </div>
         </div>
-      </div>
-      <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
-        <div class="action resolve hideOnPublished">
-          <label>
-            <input type="checkbox"
-                checked$="[[resolved]]"
-                on-change="_handleToggleResolved">
-            Resolved
-          </label>
-        </div>
-        <div class="action unresolved hideOnPublished" hidden$="[[resolved]]">
-          Unresolved
-        </div>
-        <div class="rightActions">
-          <gr-button link class="action cancel hideOnPublished"
-              on-tap="_handleCancel" hidden>Cancel</gr-button>
-          <gr-button link class="action discard hideOnPublished"
-              on-tap="_handleDiscard">Discard</gr-button>
-          <gr-button link class="action edit hideOnPublished"
-              on-tap="_handleEdit">Edit</gr-button>
-          <gr-button link class="action save hideOnPublished"
-              on-tap="_handleSave"
-              disabled$="[[_computeSaveDisabled(_messageText)]]">Save
+        <div class="actions robotActions" hidden$="[[!_showRobotActions]]">
+          <gr-button link class="action fix"
+              on-tap="_handleFix"
+              disabled="[[robotButtonDisabled]]">
+            Please Fix
           </gr-button>
         </div>
       </div>
-      <div class="actions robotActions" hidden$="[[!_showRobotActions]]">
-        <gr-button link class="action fix"
-            on-tap="_handleFix"
-            disabled="[[robotButtonDisabled]]">
-          Please Fix
-        </gr-button>
-      </div>
     </div>
     <gr-overlay id="confirmDeleteOverlay" with-backdrop>
       <gr-confirm-delete-comment-dialog id="confirmDeleteComment"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index e8717b5..c5dd524 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -21,6 +21,8 @@
   const DRAFT_SINGULAR = 'draft...';
   const DRAFT_PLURAL = 'drafts...';
   const SAVED_MESSAGE = 'All changes saved';
+  const SAVING_PROGRESS_MESSAGE = 'Saving draft...';
+  const DiSCARDING_PROGRESS_MESSAGE = 'Discarding draft...';
 
   Polymer({
     is: 'gr-diff-comment',
@@ -120,6 +122,8 @@
         type: Object,
         value: {number: 0}, // Intentional to share the object across instances.
       },
+
+      _savingMessage: String,
     },
 
     observers: [
@@ -433,6 +437,7 @@
       if (!this.comment.__draft) {
         throw Error('Cannot discard a non-draft comment.');
       }
+      this._savingMessage = DiSCARDING_PROGRESS_MESSAGE;
       this.editing = false;
       this.disabled = true;
       this._eraseDraftComment();
@@ -497,6 +502,7 @@
     },
 
     _saveDraft(draft) {
+      this._savingMessage = SAVING_PROGRESS_MESSAGE;
       this._showStartRequest();
       return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
           .then(result => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index 1dc6c45..0d0b90e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -698,5 +698,22 @@
         assert.equal(element._numPendingDraftRequests.number, 0);
       });
     });
+
+    suite('saving progress indicators', () => {
+      setup(() => {
+        sandbox.stub(element, '_deleteDraft').returns(Promise.resolve());
+        element._savingMessage = '';
+      });
+
+      test('saving', () => {
+        element._saveDraft();
+        assert.equal(element._savingMessage, 'Saving draft...');
+      });
+
+      test('discarding', () => {
+        element._discardDraft();
+        assert.equal(element._savingMessage, 'Discarding draft...');
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
index b6a2ad2..c6cf7ce 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
@@ -99,6 +99,8 @@
       <gr-endpoint-decorator name="editor">
         <gr-endpoint-param name="fileContent" value="[[_newContent]]">
         </gr-endpoint-param>
+        <gr-endpoint-param name="prefs" value="[[_prefs]]">
+        </gr-endpoint-param>
         <textarea value="{{_newContent::input}}" id="file"></textarea>
       </gr-endpoint-decorator>
     </div>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index 5652793..52ce943 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -35,7 +35,6 @@
       _change: Object,
       _changeEditDetail: Object,
       _changeNum: String,
-      _loggedIn: Boolean,
       _path: String,
       _content: String,
       _newContent: String,
@@ -44,6 +43,7 @@
         value: true,
         computed: '_computeSaveDisabled(_content, _newContent)',
       },
+      _prefs: Object,
     },
 
     behaviors: [
@@ -53,13 +53,17 @@
     ],
 
     attached() {
-      this._getLoggedIn().then(loggedIn => { this._loggedIn = loggedIn; });
+      this._getEditPrefs().then(prefs => { this._prefs = prefs; });
     },
 
     _getLoggedIn() {
       return this.$.restAPI.getLoggedIn();
     },
 
+    _getEditPrefs() {
+      return this.$.restAPI.getEditPrefs();
+    },
+
     _paramsChanged(value) {
       if (value.view !== Gerrit.Nav.View.EDIT) { return; }
 
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
index e3e6474..b3bcb22 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -46,7 +46,7 @@
 
   setup(() => {
     stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
+      getEditPrefs() { return Promise.resolve({}); },
     });
     sandbox = sinon.sandbox.create();
     element = fixture('basic');
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 5fca577..1252f7d 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -60,6 +60,11 @@
       }
     },
 
+    _getAccounts(account) {
+      return account._account_id || account.email || account.username ||
+          account.name;
+    },
+
     _buildAvatarURL(account) {
       if (!account) { return ''; }
       const avatars = account.avatars || [];
@@ -69,7 +74,8 @@
         }
       }
       return this.getBaseUrl() + '/accounts/' +
-        account._account_id + '/avatar?s=' + this.imageSize;
+        encodeURIComponent(this._getAccounts(account)) +
+        '/avatar?s=' + this.imageSize;
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index 4a51142..8187471 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -50,6 +50,21 @@
           '/accounts/123/avatar?s=16');
       assert.equal(element._buildAvatarURL(
           {
+            email: 'test@example.com',
+          }),
+          '/accounts/test%40example.com/avatar?s=16');
+      assert.equal(element._buildAvatarURL(
+          {
+            name: 'John Doe',
+          }),
+          '/accounts/John%20Doe/avatar?s=16');
+      assert.equal(element._buildAvatarURL(
+          {
+            username: 'John_Doe',
+          }),
+          '/accounts/John_Doe/avatar?s=16');
+      assert.equal(element._buildAvatarURL(
+          {
             _account_id: 123,
             avatars: [
               {
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index 9be55cf..483882f 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -24,6 +24,7 @@
 <dom-module id="gr-button">
   <template strip-whitespace>
     <style include="shared-styles">
+      /* general styles for all buttons */
       :host {
         display: inline-block;
         font-family: var(--font-family-bold);
@@ -33,56 +34,14 @@
       :host([hidden]) {
         display: none;
       }
-      :host([link]) {
-        background-color: transparent;
-        border: none;
-        color: var(--color-link);
-        font-size: inherit;
-        font-family: var(--font-family-bold);
-        text-transform: none;
-      }
-      :host([link][tertiary]) {
-        color: var(--color-link-tertiary);
-      }
-      :host([link]) paper-button {
-        margin: 0;
-        padding: 0;
-        @apply --gr-button;
-      }
-      paper-button[raised] {
-        background-color: var(--gr-button-background, #fff);
-        color: var(--gr-button-color, var(--color-link));
-      }
       :host([no-uppercase]) paper-button {
         text-transform: none;
       }
-      /* todo (beckysiegel) switch all secondary to primary as there is no color
-        distinction anymore. */
-      :host([primary]) paper-button[raised],
-      :host([secondary]) paper-button[raised] {
-        background-color: var(--color-link);
-        color: #fff;
-      }
-      :host([primary][disabled]) paper-button[raised],
-      :host([disabled]) paper-button {
-        opacity: .5;
-      }
-      :host([link]) paper-button:hover,
-      :host([link]) paper-button:focus,
-      paper-button[raised]:hover,
-      paper-button[raised]:focus  {
-        color: var(--gr-button-hover-color, var(--color-button-hover));
-      }
-      :host([primary]) paper-button[raised]:hover,
-      :host([primary]) paper-button[raised]:focus,
-      :host([secondary]) paper-button[raised]:hover,
-      :host([secondary]) paper-button[raised]:focus {
-        background-color: var(--gr-button-hover-background-color, var(--color-button-hover));
-        color: var(--gr-button-color, #fff);
-      }
-      paper-button,
-      paper-button[raised],
-      paper-button[link] {
+      paper-button {
+        /* Some of these are overridden for link style buttons since buttons
+         without the link attribute are raised */
+        background-color: var(--gr-button-background, #fff);
+        color: var(--gr-button-color, var(--color-link));
         display: flex;
         align-items: center;
         justify-content: center;
@@ -91,11 +50,15 @@
         padding: .4em .85em;
         @apply --gr-button;
       }
-      :host([link]) paper-button {
-        --paper-button: {
-          padding: 0;
-        }
+      paper-button:hover,
+      paper-button:focus {
+        color: var(--gr-button-hover-color, var(--color-button-hover));
       }
+      :host([disabled]) paper-button {
+        color: #a8a8a8;
+        cursor: wait;
+      }
+      /* styles for the optional down arrow */
       :host:not([down-arrow]) .downArrow {display: none; }
       :host([down-arrow]) .downArrow {
         border-top: .36em solid var(--gr-button-arrow-color, #ccc);
@@ -108,20 +71,56 @@
       :host([down-arrow]) paper-button:hover .downArrow {
         border-top-color: var(--gr-button-arrow-hover-color, #666);
       }
-      :host([loading]) paper-button,
-      :host([disabled]) paper-button {
-        color: #aaa;
+
+      /* styles for raised buttons specifically*/
+      :host([primary]) paper-button[raised],
+      :host([secondary]) paper-button[raised] {
+        background-color: var(--color-link);
+        color: #fff;
       }
-      :host([loading]) paper-button,
-      :host([loading][disabled]) paper-button {
-        cursor: wait;
-        background-color: #efefef;
-        color: #aaa;
+      :host([primary]) paper-button[raised]:hover,
+      :host([primary]) paper-button[raised]:focus,
+      :host([secondary]) paper-button[raised]:hover,
+      :host([secondary]) paper-button[raised]:focus {
+        background-color: var(--gr-button-hover-background-color, var(--color-button-hover));
+        color: var(--gr-button-color, #fff);
+      }
+      :host([disabled]) paper-button[raised] {
+        background-color: #eaeaea;
+        color: #a8a8a8;
+      }
+      /* styles for link buttons specifically */
+      :host([link]) {
+        background-color: transparent;
+        border: none;
+        color: var(--color-link);
+        font-size: inherit;
+        font-family: var(--font-family-bold);
+        text-transform: none;
+      }
+      :host([link][tertiary]) {
+        color: var(--color-link-tertiary);
+      }
+      :host([link]) paper-button {
+        background-color: transparent;
+        margin: 0;
+        padding: 0;
+        --paper-button: {
+          padding: 0;
+        }
+        @apply --gr-button;
+      }
+      :host([disabled][link]) paper-button {
+        background-color: transparent;
+      }
+      :host([link]) paper-button:hover,
+      :host([link]) paper-button:focus {
+        color: var(--gr-button-hover-color, var(--color-button-hover));
       }
     </style>
     <paper-button
         raised="[[!link]]"
-        disabled="[[disabled]]"
+        disabled="[[_computeDisabled(disabled, loading)]]"
         tabindex="-1">
       <content></content>
       <i class="downArrow"></i>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index 8e66e11..f368c8e 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -28,6 +28,11 @@
         value: false,
         reflectToAttribute: true,
       },
+      loading: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
       tertiary: {
         type: Boolean,
         value: false,
@@ -54,6 +59,10 @@
       keydown: '_handleKeydown',
     },
 
+    observers: [
+      '_computeDisabled(disabled, loading)',
+    ],
+
     behaviors: [
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.TooltipBehavior,
@@ -78,6 +87,10 @@
       this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex);
     },
 
+    _computeDisabled(disabled, loading) {
+      return disabled || loading;
+    },
+
     _handleKeydown(e) {
       if (this.modifierPressed(e)) { return; }
       e = this.getKeyboardEvent(e);
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
index d78427b..c0ceb33 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -51,6 +51,16 @@
       sandbox.restore();
     });
 
+    test('disabled is set by disabled or loading', () => {
+      assert.isFalse(element.$$('paper-button').disabled);
+      element.disabled = true;
+      assert.isTrue(element.$$('paper-button').disabled);
+      element.disabled = false;
+      assert.isFalse(element.$$('paper-button').disabled);
+      element.loading = true;
+      assert.isTrue(element.$$('paper-button').disabled);
+    });
+
     for (const eventName of ['tap', 'click']) {
       test('dispatches ' + eventName + ' event', () => {
         const spy = addSpyOn(eventName);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 9a7a59e..3bb0178 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -915,6 +915,18 @@
 
     /**
      * @param {number|string} changeNum
+     * @param {!Promise<?Object>} patchRange
+     */
+    getChangeEditFiles(changeNum, patchRange) {
+      let endpoint = '/edit?list';
+      if (patchRange.basePatchNum !== 'PARENT') {
+        endpoint += '&base=' + encodeURIComponent(patchRange.basePatchNum);
+      }
+      return this._getChangeURLAndFetch(changeNum, endpoint);
+    },
+
+    /**
+     * @param {number|string} changeNum
      * @param {number|string} patchNum
      * @param {string} query
      * @return {!Promise<!Object>}
@@ -929,6 +941,11 @@
           this._normalizeChangeFilesResponse.bind(this));
     },
 
+    getChangeEditFilesAsSpeciallySortedArray(changeNum, patchRange) {
+      return this.getChangeEditFiles(changeNum, patchRange).then(files =>
+            this._normalizeChangeFilesResponse(files.files));
+    },
+
     /**
      * The closure compiler doesn't realize this.specialFilePathCompare is
      * valid.
@@ -1330,6 +1347,10 @@
           '/edit:publish');
     },
 
+    getEditPrefs() {
+      return this._fetchSharedCacheURL('/accounts/self/preferences.edit');
+    },
+
     putChangeCommitMessage(changeNum, message) {
       const p = {message};
       return this.getChangeURLAndSend(changeNum, 'PUT', null, '/message', p);
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index d5d2193..c42bc81 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -257,6 +257,7 @@
         "version_json": "%{name}-versions.json",
     },
 )
+
 """Groups a set of bower components together in a zip file.
 
 Outputs:
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index c9cd5d7..12e4b5f 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -17,7 +17,6 @@
 jar_filetype = FileType([".jar"])
 
 LIBS = [
-    "//java/com/google/gerrit/common:version",
     "//java/com/google/gerrit/httpd/init",
     "//lib:postgresql",
     "//lib/bouncycastle:bcpkix",
@@ -28,7 +27,7 @@
 ]
 
 PGMLIBS = [
-    "//java/com/google/gerrit/pgm"
+    "//java/com/google/gerrit/pgm",
 ]
 
 def _add_context(in_file, output):
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index e2de4ed..f43ff91 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -10,6 +10,7 @@
 )
 
 PLUGIN_DEPS = ["//plugins:plugin-lib"]
+
 PLUGIN_DEPS_NEVERLINK = ["//plugins:plugin-lib-neverlink"]
 
 PLUGIN_TEST_DEPS = [