Merge "Switch one more CharMatcher.WHITESPACE to CharMatcher.whitespace()."
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 2d96b84..6efabff 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -360,7 +360,7 @@
 * Determine the sha1 hash of the zip file:
 +
 ----
- openssl sha1 4.10.0-6-gd0a2dda.zip
+ openssl sha1 codemirror-4.10.0-6-gd0a2dda.zip
 ----
 
 * Upload the zip file to the
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 01dfcc2..e32e30d 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -381,10 +381,16 @@
 
 * `com.google.gerrit.common.EventListener`:
 +
-Allows to listen to events. These are the same
-link:cmd-stream-events.html#events[events] that are also streamed by
+Allows to listen to events without user visibility restrictions. These
+are the same link:cmd-stream-events.html#events[events] that are also streamed by
 the link:cmd-stream-events.html[gerrit stream-events] command.
 
+* `com.google.gerrit.common.UserScopedEventListener`:
++
+Allows to listen to events visible to the specified user. These are the
+same link:cmd-stream-events.html#events[events] that are also streamed
+by the link:cmd-stream-events.html[gerrit stream-events] command.
+
 * `com.google.gerrit.extensions.events.LifecycleListener`:
 +
 Plugin start and stop
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index e37bde8..2b7f930 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -31,8 +31,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
-import com.google.gerrit.common.EventListener;
-import com.google.gerrit.common.EventSource;
+import com.google.gerrit.common.UserScopedEventListener;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -43,6 +42,8 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
@@ -106,9 +107,9 @@
   private Submit submitHandler;
 
   @Inject
-  EventSource source;
+  DynamicSet<UserScopedEventListener> eventListeners;
 
-  private EventListener eventListener;
+  private RegistrationHandle eventListenerRegistration;
 
   private String systemTimeZone;
 
@@ -127,26 +128,30 @@
   @Before
   public void setUp() throws Exception {
     mergeResults = Maps.newHashMap();
-    CurrentUser listenerUser = factory.create(user.id);
-    eventListener = new EventListener() {
-      @Override
-      public void onEvent(Event event) {
-        if (!(event instanceof ChangeMergedEvent)) {
-          return;
-        }
-        ChangeMergedEvent e = (ChangeMergedEvent) event;
-        ChangeAttribute c = e.change.get();
-        PatchSetAttribute ps = e.patchSet.get();
-        log.debug("Merged {},{} as {}", ps.number, c.number, e.newRev);
-        mergeResults.put(e.change.get().number, e.newRev);
-      }
-    };
-    source.addEventListener(eventListener, listenerUser);
+    eventListenerRegistration =
+        eventListeners.add(new UserScopedEventListener() {
+          @Override
+          public void onEvent(Event event) {
+            if (!(event instanceof ChangeMergedEvent)) {
+              return;
+            }
+            ChangeMergedEvent e = (ChangeMergedEvent) event;
+            ChangeAttribute c = e.change.get();
+            PatchSetAttribute ps = e.patchSet.get();
+            log.debug("Merged {},{} as {}", ps.number, c.number, e.newRev);
+            mergeResults.put(e.change.get().number, e.newRev);
+          }
+
+          @Override
+          public CurrentUser getUser() {
+            return factory.create(user.id);
+          }
+        });
   }
 
   @After
   public void cleanup() {
-    source.removeEventListener(eventListener);
+    eventListenerRegistration.remove();
     db.close();
   }
 
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index 3ef4fdb..2a55afe 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -43,6 +43,7 @@
     '//lib/log:api',
     '//lib/mina:sshd',
     '@jgit//org.eclipse.jgit:jgit',
+    '@jgit//org.eclipse.jgit.http.server:jgit-servlet',
   ],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
index ddfc8c6..bc7ebc9 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -18,6 +18,7 @@
 import com.google.gwtorm.client.CompoundKey;
 
 import java.sql.Timestamp;
+import java.util.Date;
 import java.util.Objects;
 
 /** An approval (or negative approval) on a patch set. */
@@ -95,7 +96,7 @@
   protected PatchSetApproval() {
   }
 
-  public PatchSetApproval(PatchSetApproval.Key k, short v, Timestamp ts) {
+  public PatchSetApproval(PatchSetApproval.Key k, short v, Date ts) {
     key = k;
     setValue(v);
     setGranted(ts);
@@ -136,8 +137,12 @@
     return granted;
   }
 
-  public void setGranted(Timestamp ts) {
-    granted = ts;
+  public void setGranted(Date when) {
+    if (when instanceof Timestamp) {
+      granted = (Timestamp) when;
+    } else {
+      granted = new Timestamp(when.getTime());
+    }
   }
 
   public String getLabel() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 410e532..400c1c0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -92,7 +92,6 @@
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.Callable;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.FutureTask;
@@ -102,7 +101,7 @@
 /** Spawns local executables when a hook action occurs. */
 @Singleton
 public class ChangeHookRunner implements ChangeHooks, EventDispatcher,
-  EventSource, LifecycleListener, NewProjectCreatedListener {
+    LifecycleListener, NewProjectCreatedListener {
     /** A logger for this class. */
     private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class);
 
@@ -112,22 +111,11 @@
         bind(ChangeHookRunner.class);
         bind(ChangeHooks.class).to(ChangeHookRunner.class);
         bind(EventDispatcher.class).to(ChangeHookRunner.class);
-        bind(EventSource.class).to(ChangeHookRunner.class);
         DynamicSet.bind(binder(), NewProjectCreatedListener.class).to(ChangeHookRunner.class);
         listener().to(ChangeHookRunner.class);
       }
     }
 
-    private static class EventListenerHolder {
-      final EventListener listener;
-      final CurrentUser user;
-
-      EventListenerHolder(EventListener l, CurrentUser u) {
-        listener = l;
-        user = u;
-      }
-    }
-
     /** Container class used to hold the return code and output of script hook execution */
     public static class HookResult {
       private int exitValue = -1;
@@ -177,9 +165,8 @@
     }
 
     /** Listeners to receive changes as they happen (limited by visibility
-     *  of holder's user). */
-    private final Map<EventListener, EventListenerHolder> listeners =
-        new ConcurrentHashMap<>();
+     *  of user). */
+    private final DynamicSet<UserScopedEventListener> listeners;
 
     /** Listeners to receive all changes as they happen. */
     private final DynamicSet<EventListener> unrestrictedListeners;
@@ -268,6 +255,7 @@
       ProjectCache projectCache,
       AccountCache accountCache,
       EventFactory eventFactory,
+      DynamicSet<UserScopedEventListener> listeners,
       DynamicSet<EventListener> unrestrictedListeners,
       ChangeNotes.Factory notesFactory) {
         this.anonymousCowardName = anonymousCowardName;
@@ -277,6 +265,7 @@
         this.accountCache = accountCache;
         this.eventFactory = eventFactory;
         this.sitePaths = sitePath;
+        this.listeners = listeners;
         this.unrestrictedListeners = unrestrictedListeners;
         this.notesFactory = notesFactory;
 
@@ -319,16 +308,6 @@
       return Files.exists(p) ? Optional.of(p) : Optional.<Path>absent();
     }
 
-    @Override
-    public void addEventListener(EventListener listener, CurrentUser user) {
-      listeners.put(listener, new EventListenerHolder(listener, user));
-    }
-
-    @Override
-    public void removeEventListener(EventListener listener) {
-      listeners.remove(listener);
-    }
-
     /**
      * Get the Repository for the given project name, or null on error.
      *
@@ -923,9 +902,9 @@
 
     private void fireEvent(Change change, ChangeEvent event, ReviewDb db)
         throws OrmException {
-      for (EventListenerHolder holder : listeners.values()) {
-        if (isVisibleTo(change, holder.user, db)) {
-          holder.listener.onEvent(event);
+      for (UserScopedEventListener listener : listeners) {
+        if (isVisibleTo(change, listener.getUser(), db)) {
+          listener.onEvent(event);
         }
       }
 
@@ -933,9 +912,9 @@
     }
 
     private void fireEvent(Project.NameKey project, ProjectEvent event) {
-      for (EventListenerHolder holder : listeners.values()) {
-        if (isVisibleTo(project, holder.user)) {
-          holder.listener.onEvent(event);
+      for (UserScopedEventListener listener : listeners) {
+        if (isVisibleTo(project, listener.getUser())) {
+          listener.onEvent(event);
         }
       }
 
@@ -943,9 +922,9 @@
     }
 
     private void fireEvent(Branch.NameKey branchName, RefEvent event) {
-      for (EventListenerHolder holder : listeners.values()) {
-        if (isVisibleTo(branchName, holder.user)) {
-          holder.listener.onEvent(event);
+      for (UserScopedEventListener listener : listeners) {
+        if (isVisibleTo(branchName, listener.getUser())) {
+          listener.onEvent(event);
         }
       }
 
@@ -954,9 +933,9 @@
 
     private void fireEvent(com.google.gerrit.server.events.Event event,
         ReviewDb db) throws OrmException {
-      for (EventListenerHolder holder : listeners.values()) {
-        if (isVisibleTo(event, holder.user, db)) {
-          holder.listener.onEvent(event);
+      for (UserScopedEventListener listener : listeners) {
+        if (isVisibleTo(event, listener.getUser(), db)) {
+          listener.onEvent(event);
         }
       }
 
@@ -999,7 +978,7 @@
         RefEvent refEvent = (RefEvent) event;
         String ref = refEvent.getRefName();
         if (PatchSet.isChangeRef(ref)) {
-          Change.Id cid= PatchSet.Id.fromRef(ref).getParentKey();
+          Change.Id cid = PatchSet.Id.fromRef(ref).getParentKey();
           Change change = notesFactory
               .create(db, refEvent.getProjectNameKey(), cid).getChange();
           return isVisibleTo(change, user, db);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index 633c1a4..832b3d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.ProjectEvent;
@@ -34,12 +33,7 @@
 import java.util.Set;
 
 /** Does not invoke hooks. */
-public final class DisabledChangeHooks implements ChangeHooks, EventDispatcher,
-    EventSource {
-  @Override
-  public void addEventListener(EventListener listener, CurrentUser user) {
-  }
-
+public final class DisabledChangeHooks implements ChangeHooks, EventDispatcher {
   @Override
   public void doChangeAbandonedHook(Change change, Account account,
       PatchSet patchSet, String reason, ReviewDb db) {
@@ -106,10 +100,6 @@
   }
 
   @Override
-  public void removeEventListener(EventListener listener) {
-  }
-
-  @Override
   public HookResult doRefUpdateHook(Project project, String refName,
       Account uploader, ObjectId oldId, ObjectId newId) {
     return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventListener.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventListener.java
index 97be844..b2d5680 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/EventListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventListener.java
@@ -17,6 +17,10 @@
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.server.events.Event;
 
+/**
+ * Allows to listen to events without user visibility restrictions. To listen to
+ * events visible to a specific user, use {@link UserScopedEventListener}.
+ */
 @ExtensionPoint
 public interface EventListener {
   void onEvent(Event event);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java b/gerrit-server/src/main/java/com/google/gerrit/common/UserScopedEventListener.java
similarity index 62%
rename from gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java
rename to gerrit-server/src/main/java/com/google/gerrit/common/UserScopedEventListener.java
index bde6f5d..22435ba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/UserScopedEventListener.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2016 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.
@@ -11,14 +11,16 @@
 // 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 com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.server.CurrentUser;
 
-/** Distributes Events to ChangeListeners.  Register listeners here. */
-public interface EventSource {
-  void addEventListener(EventListener listener, CurrentUser user);
-
-  void removeEventListener(EventListener listener);
+/**
+ * Allows to listen to events visible to the specified user. To listen to events
+ * without user visibility restrictions, use {@link EventListener}.
+ */
+@ExtensionPoint
+public interface UserScopedEventListener extends EventListener {
+  CurrentUser getUser();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 7551d5f..677f849 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -31,7 +31,6 @@
 import com.google.common.collect.Ordering;
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
@@ -56,6 +55,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -216,7 +216,7 @@
     for (Account.Id account : need) {
       cells.add(new PatchSetApproval(
           new PatchSetApproval.Key(psId, account, labelId),
-          (short) 0, TimeUtil.nowTs()));
+          (short) 0, update.getWhen()));
       update.putReviewer(account, REVIEWER);
     }
     db.patchSetApprovals().insert(cells);
@@ -229,7 +229,7 @@
     if (!approvals.isEmpty()) {
       checkApprovals(approvals, changeCtl);
       List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
-      Timestamp ts = TimeUtil.nowTs();
+      Date ts = update.getWhen();
       for (Map.Entry<String, Short> vote : approvals.entrySet()) {
         LabelType lt = labelTypes.byLabel(vote.getKey());
         cells.add(new PatchSetApproval(new PatchSetApproval.Key(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index af3c576..058c7d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsPost;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -27,7 +26,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeFinder;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmException;
@@ -35,7 +33,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import java.io.IOException;
 import java.util.List;
 
 @Singleton
@@ -48,7 +45,6 @@
   private final DynamicMap<RestView<ChangeResource>> views;
   private final ChangeFinder changeFinder;
   private final CreateChange createChange;
-  private final ChangeIndexer changeIndexer;
 
   @Inject
   ChangesCollection(
@@ -57,15 +53,13 @@
       Provider<QueryChanges> queryFactory,
       DynamicMap<RestView<ChangeResource>> views,
       ChangeFinder changeFinder,
-      CreateChange createChange,
-      ChangeIndexer changeIndexer) {
+      CreateChange createChange) {
     this.db = db;
     this.user = user;
     this.queryFactory = queryFactory;
     this.views = views;
     this.changeFinder = changeFinder;
     this.createChange = createChange;
-    this.changeIndexer = changeIndexer;
   }
 
   @Override
@@ -81,22 +75,10 @@
   @Override
   public ChangeResource parse(TopLevelResource root, IdString id)
       throws ResourceNotFoundException, OrmException {
-    List<ChangeControl> ctls =
-        changeFinder.find(id.encoded(), user.get());
-    if (ctls.isEmpty()) {
-      Integer changeId = Ints.tryParse(id.get());
-      if (changeId != null) {
-        try {
-          changeIndexer.delete(new Change.Id(changeId));
-        } catch (IOException e) {
-          throw new ResourceNotFoundException(id.get(), e);
-        }
-      }
-    }
+    List<ChangeControl> ctls = changeFinder.find(id.encoded(), user.get());
     if (ctls.isEmpty()) {
       throw new ResourceNotFoundException(id);
-    }
-    if (ctls.size() != 1) {
+    } else if (ctls.size() != 1) {
       throw new ResourceNotFoundException("Multiple changes found for " + id);
     }
 
@@ -111,16 +93,11 @@
       throws ResourceNotFoundException, OrmException {
     List<ChangeControl> ctls = changeFinder.find(id, user.get());
     if (ctls.isEmpty()) {
-      try {
-        changeIndexer.delete(id);
-      } catch (IOException e) {
-        throw new ResourceNotFoundException(toIdString(id).get(), e);
-      }
       throw new ResourceNotFoundException(toIdString(id));
-    }
-    if (ctls.size() != 1) {
+    } else if (ctls.size() != 1) {
       throw new ResourceNotFoundException("Multiple changes found for " + id);
     }
+
     ChangeControl ctl = ctls.get(0);
     if (!ctl.isVisible(db.get())) {
       throw new ResourceNotFoundException(toIdString(id));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index 50efbb6..513a34f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -41,7 +42,6 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
-import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -62,10 +62,10 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.ChangeIdUtil;
 
 import java.io.IOException;
+import java.sql.Timestamp;
 import java.util.List;
 import java.util.TimeZone;
 
@@ -83,7 +83,6 @@
   private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeMessagesUtil changeMessagesUtil;
   private final PatchSetUtil psUtil;
-  private final ChangeUpdate.Factory updateFactory;
   private final BatchUpdate.Factory batchUpdateFactory;
 
   @Inject
@@ -98,7 +97,6 @@
       MergeUtil.Factory mergeUtilFactory,
       ChangeMessagesUtil changeMessagesUtil,
       PatchSetUtil psUtil,
-      ChangeUpdate.Factory updateFactory,
       BatchUpdate.Factory batchUpdateFactory) {
     this.db = db;
     this.seq = seq;
@@ -111,7 +109,6 @@
     this.mergeUtilFactory = mergeUtilFactory;
     this.changeMessagesUtil = changeMessagesUtil;
     this.psUtil = psUtil;
-    this.updateFactory = updateFactory;
     this.batchUpdateFactory = batchUpdateFactory;
   }
 
@@ -149,9 +146,9 @@
       CodeReviewCommit commitToCherryPick =
           revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
 
+      Timestamp now = TimeUtil.nowTs();
       PersonIdent committerIdent =
-          identifiedUser.newCommitterIdent(TimeUtil.nowTs(),
-              serverTimeZone);
+          identifiedUser.newCommitterIdent(now, serverTimeZone);
 
       final ObjectId computedChangeId =
           ChangeIdUtil
@@ -187,29 +184,35 @@
               + changeKey + " reside on the same branch. "
               + "Cannot create a new patch set.");
         }
-        if (destChanges.size() == 1) {
-          // The change key exists on the destination branch. The cherry pick
-          // will be added as a new patch set.
-          ChangeControl destCtl = refControl.getProjectControl()
-              .controlFor(destChanges.get(0).notes());
-          return insertPatchSet(git, revWalk, oi, destCtl,
-              cherryPickCommit, refControl, identifiedUser);
-        } else {
-          // Change key not found on destination branch. We can create a new
-          // change.
-          String newTopic = null;
-          if (!Strings.isNullOrEmpty(change.getTopic())) {
-            newTopic = change.getTopic() + "-" + newDest.getShortName();
+        try (BatchUpdate bu = batchUpdateFactory.create(
+            db.get(), change.getDest().getParentKey(), identifiedUser, now)) {
+          bu.setRepository(git, revWalk, oi);
+          Change.Id result;
+          if (destChanges.size() == 1) {
+            // The change key exists on the destination branch. The cherry pick
+            // will be added as a new patch set.
+            ChangeControl destCtl = refControl.getProjectControl()
+                .controlFor(destChanges.get(0).notes());
+            result = insertPatchSet(
+                bu, git, destCtl, cherryPickCommit, refControl);
+          } else {
+            // Change key not found on destination branch. We can create a new
+            // change.
+            String newTopic = null;
+            if (!Strings.isNullOrEmpty(change.getTopic())) {
+              newTopic = change.getTopic() + "-" + newDest.getShortName();
+            }
+            result =
+                createNewChange(bu, cherryPickCommit,
+                    refControl.getRefName(), newTopic, change.getDest());
+
+            bu.addOp(change.getId(),
+                new AddMessageToSourceChangeOp(
+                    changeMessagesUtil, patch.getId(), destinationBranch,
+                    cherryPickCommit));
           }
-          Change.Id newChangeId =
-              createNewChange(git, revWalk, oi, project, cherryPickCommit,
-                  refControl.getRefName(), identifiedUser, newTopic,
-                  change.getDest());
-
-          addMessageToSourceChange(change, patch.getId(), destinationBranch,
-              cherryPickCommit, identifiedUser, refControl);
-
-          return newChangeId;
+          bu.execute();
+          return result;
         }
       } catch (MergeIdenticalTreeException | MergeConflictException e) {
         throw new IntegrationException("Cherry pick failed: " + e.getMessage());
@@ -219,10 +222,9 @@
     }
   }
 
-  private Change.Id insertPatchSet(Repository git, RevWalk revWalk,
-      ObjectInserter oi, ChangeControl ctl, CodeReviewCommit cherryPickCommit,
-      RefControl refControl, IdentifiedUser identifiedUser)
-      throws IOException, OrmException, UpdateException, RestApiException {
+  private Change.Id insertPatchSet(BatchUpdate bu, Repository git,
+      ChangeControl ctl, CodeReviewCommit cherryPickCommit,
+      RefControl refControl) throws IOException, OrmException {
     Change change = ctl.getChange();
     PatchSet.Id psId =
         ChangeUtil.nextPatchSetId(git, change.currentPatchSetId());
@@ -231,24 +233,16 @@
     PatchSet.Id newPatchSetId = inserter.getPatchSetId();
     PatchSet current = psUtil.current(db.get(), ctl.getNotes());
 
-    try (BatchUpdate bu = batchUpdateFactory.create(
-        db.get(), change.getDest().getParentKey(), identifiedUser,
-        TimeUtil.nowTs())) {
-      bu.setRepository(git, revWalk, oi);
-      bu.addOp(change.getId(), inserter
-          .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
-          .setDraft(current.isDraft())
-          .setSendMail(false));
-      bu.execute();
-    }
+    bu.addOp(change.getId(), inserter
+        .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
+        .setDraft(current.isDraft())
+        .setSendMail(false));
     return change.getId();
   }
 
-  private Change.Id createNewChange(Repository git, RevWalk revWalk,
-      ObjectInserter oi, Project.NameKey project,
-      CodeReviewCommit cherryPickCommit, String refName,
-      IdentifiedUser identifiedUser, String topic, Branch.NameKey sourceBranch)
-          throws RestApiException, UpdateException, OrmException {
+  private Change.Id createNewChange(BatchUpdate bu,
+      CodeReviewCommit cherryPickCommit, String refName, String topic,
+      Branch.NameKey sourceBranch) throws OrmException {
     Change.Id changeId = new Change.Id(seq.nextChangeId());
     ChangeInserter ins = changeInserterFactory.create(
           changeId, cherryPickCommit, refName)
@@ -257,38 +251,44 @@
 
     ins.setMessage(
         messageForDestinationChange(ins.getPatchSetId(), sourceBranch));
-    try (BatchUpdate bu = batchUpdateFactory.create(
-        db.get(), project, identifiedUser, TimeUtil.nowTs())) {
-      bu.setRepository(git, revWalk, oi);
-      bu.insertChange(ins);
-      bu.execute();
-    }
+    bu.insertChange(ins);
     return changeId;
   }
 
-  private void addMessageToSourceChange(Change change, PatchSet.Id patchSetId,
-      String destinationBranch, CodeReviewCommit cherryPickCommit,
-      IdentifiedUser identifiedUser, RefControl refControl)
-          throws OrmException, IOException {
-    ChangeMessage changeMessage = new ChangeMessage(
-        new ChangeMessage.Key(
-            patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
-            identifiedUser.getAccountId(), TimeUtil.nowTs(), patchSetId);
-    StringBuilder sb = new StringBuilder("Patch Set ")
-        .append(patchSetId.get())
-        .append(": Cherry Picked")
-        .append("\n\n")
-        .append("This patchset was cherry picked to branch ")
-        .append(destinationBranch)
-        .append(" as commit ")
-        .append(cherryPickCommit.getId().getName());
-    changeMessage.setMessage(sb.toString());
+  private static class AddMessageToSourceChangeOp extends BatchUpdate.Op {
+    private final ChangeMessagesUtil cmUtil;
+    private final PatchSet.Id psId;
+    private final String destBranch;
+    private final ObjectId cherryPickCommit;
 
-    ChangeControl ctl = refControl.getProjectControl()
-        .controlFor(db.get(), change);
-    ChangeUpdate update = updateFactory.create(ctl, TimeUtil.nowTs());
-    changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
-    update.commit();
+    private AddMessageToSourceChangeOp(ChangeMessagesUtil cmUtil,
+        PatchSet.Id psId, String destBranch, ObjectId cherryPickCommit) {
+      this.cmUtil = cmUtil;
+      this.psId = psId;
+      this.destBranch = destBranch;
+      this.cherryPickCommit = cherryPickCommit;
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext ctx) throws OrmException {
+      ChangeMessage changeMessage = new ChangeMessage(
+          new ChangeMessage.Key(
+              ctx.getChange().getId(), ChangeUtil.messageUUID(ctx.getDb())),
+              ctx.getUser().getAccountId(), ctx.getWhen(), psId);
+      StringBuilder sb = new StringBuilder("Patch Set ")
+          .append(psId.get())
+          .append(": Cherry Picked")
+          .append("\n\n")
+          .append("This patchset was cherry picked to branch ")
+          .append(destBranch)
+          .append(" as commit ")
+          .append(cherryPickCommit.name());
+      changeMessage.setMessage(sb.toString());
+
+      cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), changeMessage);
+      ctx.saveChange(); // Bump lastUpdatedOn to match message.
+      return true;
+    }
   }
 
   private String messageForDestinationChange(PatchSet.Id patchSetId,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index 37ca9cf..dc3b26b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -124,7 +124,7 @@
             new ChangeMessage(new ChangeMessage.Key(ctx.getChange().getId(),
                 ChangeUtil.messageUUID(ctx.getDb())),
                 ctx.getUser().getAccountId(),
-                TimeUtil.nowTs(), currPs);
+                ctx.getWhen(), currPs);
         changeMessage.setMessage(msg.toString());
         cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index d1cc992..bf40bbb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -553,7 +553,7 @@
                   psId,
                   user.getAccountId(),
                   lt.getLabelId()),
-              ent.getValue(), TimeUtil.nowTs());
+              ent.getValue(), ctx.getWhen());
           c.setGranted(ctx.getWhen());
           ups.add(c);
           addLabelDelta(normName, c.getValue());
@@ -587,7 +587,7 @@
               user.getAccountId(),
               ctx.getControl().getLabelTypes().getLabelTypes().get(0)
                   .getLabelId()),
-              (short) 0, TimeUtil.nowTs());
+              (short) 0, ctx.getWhen());
           c.setGranted(ctx.getWhen());
           ups.add(c);
         } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index 0707dc7..8a1f290 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -43,6 +43,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.sql.Timestamp;
 import java.util.Collections;
 
 @Singleton
@@ -143,20 +144,21 @@
             comment.getParentUuid(), ctx.getWhen());
         setCommentRevId(comment, patchListCache, ctx.getChange(), ps);
         plcUtil.putComments(ctx.getDb(), update,
-            Collections.singleton(update(comment, in)));
+            Collections.singleton(update(comment, in, ctx.getWhen())));
       } else {
         if (comment.getRevId() == null) {
           setCommentRevId(
               comment, patchListCache, ctx.getChange(), ps);
         }
         plcUtil.putComments(ctx.getDb(), update,
-            Collections.singleton(update(comment, in)));
+            Collections.singleton(update(comment, in, ctx.getWhen())));
       }
       return true;
     }
   }
 
-  private static PatchLineComment update(PatchLineComment e, DraftInput in) {
+  private static PatchLineComment update(PatchLineComment e, DraftInput in,
+      Timestamp when) {
     if (in.side != null) {
       e.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
     }
@@ -168,7 +170,7 @@
       e.setRange(in.range);
       e.setLine(in.range != null ? in.range.endLine : in.line);
     }
-    e.setWrittenOn(TimeUtil.nowTs());
+    e.setWrittenOn(when);
     return e;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index b5c8bb7..7d9bd15 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -19,6 +19,7 @@
 import com.google.common.cache.Cache;
 import com.google.gerrit.audit.AuditModule;
 import com.google.gerrit.common.EventListener;
+import com.google.gerrit.common.UserScopedEventListener;
 import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.config.CloneCommand;
@@ -281,6 +282,7 @@
         .to(ProjectConfigEntry.UpdateChecker.class);
     DynamicSet.setOf(binder(), EventListener.class);
     DynamicSet.bind(binder(), EventListener.class).to(EventsMetrics.class);
+    DynamicSet.setOf(binder(), UserScopedEventListener.class);
     DynamicSet.setOf(binder(), CommitValidationListener.class);
     DynamicSet.setOf(binder(), RefOperationValidationListener.class);
     DynamicSet.setOf(binder(), MergeValidationListener.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index e44c810..ca4ac04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -67,6 +67,7 @@
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.sql.Timestamp;
 import java.util.Map;
 import java.util.TimeZone;
 
@@ -136,8 +137,8 @@
         ObjectId revision = ObjectId.fromString(ps.getRevision().get());
         String editRefName = RefNames.refsEdit(me.getAccountId(), change.getId(),
             ps.getId());
-        Result res =
-            update(repo, me, editRefName, rw, ObjectId.zeroId(), revision);
+        Result res = update(repo, me, editRefName, rw, ObjectId.zeroId(),
+            revision, TimeUtil.nowTs());
         indexer.index(reviewDb.get(), change);
         return res;
       }
@@ -244,11 +245,12 @@
         RevWalk rw = new RevWalk(repo);
         ObjectInserter inserter = repo.newObjectInserter()) {
       String refName = edit.getRefName();
+      Timestamp now = TimeUtil.nowTs();
       ObjectId commit = createCommit(me, inserter, prevEdit,
           prevEdit.getTree(),
-          msg);
+          msg, now);
       inserter.flush();
-      return update(repo, me, refName, rw, prevEdit, commit);
+      return update(repo, me, refName, rw, prevEdit, commit, now);
     }
   }
 
@@ -344,9 +346,10 @@
         throw new InvalidChangeOperationException("no changes were made");
       }
 
-      ObjectId commit = createCommit(me, inserter, prevEdit, newTree);
+      Timestamp now = TimeUtil.nowTs();
+      ObjectId commit = createCommit(me, inserter, prevEdit, newTree, now);
       inserter.flush();
-      return update(repo, me, refName, rw, prevEdit, commit);
+      return update(repo, me, refName, rw, prevEdit, commit, now);
     }
   }
 
@@ -365,30 +368,30 @@
   }
 
   private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter,
-      RevCommit revision, ObjectId tree) throws IOException {
+      RevCommit revision, ObjectId tree, Timestamp when) throws IOException {
     return createCommit(me, inserter, revision, tree,
-        revision.getFullMessage());
+        revision.getFullMessage(), when);
   }
 
   private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter,
-      RevCommit revision, ObjectId tree, String msg)
+      RevCommit revision, ObjectId tree, String msg, Timestamp when)
       throws IOException {
     CommitBuilder builder = new CommitBuilder();
     builder.setTreeId(tree);
     builder.setParentIds(revision.getParents());
     builder.setAuthor(revision.getAuthorIdent());
-    builder.setCommitter(getCommitterIdent(me));
+    builder.setCommitter(getCommitterIdent(me, when));
     builder.setMessage(msg);
     return inserter.insert(builder);
   }
 
   private RefUpdate.Result update(Repository repo, IdentifiedUser me,
-      String refName, RevWalk rw, ObjectId oldObjectId, ObjectId newEdit)
-      throws IOException {
+      String refName, RevWalk rw, ObjectId oldObjectId, ObjectId newEdit,
+      Timestamp when) throws IOException {
     RefUpdate ru = repo.updateRef(refName);
     ru.setExpectedOldObjectId(oldObjectId);
     ru.setNewObjectId(newEdit);
-    ru.setRefLogIdent(getRefLogIdent(me));
+    ru.setRefLogIdent(getRefLogIdent(me, when));
     ru.setRefLogMessage("inline edit (amend)", false);
     ru.setForceUpdate(true);
     RefUpdate.Result res = ru.update(rw);
@@ -476,11 +479,11 @@
     return dc;
   }
 
-  private PersonIdent getCommitterIdent(IdentifiedUser user) {
-    return user.newCommitterIdent(TimeUtil.nowTs(), tz);
+  private PersonIdent getCommitterIdent(IdentifiedUser user, Timestamp when) {
+    return user.newCommitterIdent(when, tz);
   }
 
-  private PersonIdent getRefLogIdent(IdentifiedUser user) {
-    return user.newRefLogIdent(TimeUtil.nowTs(), tz);
+  private PersonIdent getRefLogIdent(IdentifiedUser user, Timestamp when) {
+    return user.newRefLogIdent(when, tz);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index a79d862..e213927 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -900,7 +900,7 @@
     try {
       for (ChangeData cd : internalChangeQuery.byProjectOpen(destProject)) {
         try (BatchUpdate bu = batchUpdateFactory.create(db, destProject,
-            internalUserFactory.create(), TimeUtil.nowTs())) {
+            internalUserFactory.create(), ts)) {
           bu.addOp(cd.getId(), new BatchUpdate.Op() {
             @Override
             public boolean updateChange(ChangeContext ctx) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
index a0860c7..c96ed65 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -184,15 +184,6 @@
         : Futures.<Object, IOException> immediateCheckedFuture(null);
   }
 
-  /**
-   * Synchronously delete a change.
-   *
-   * @param id change ID to delete.
-   */
-  public void delete(Change.Id id) throws IOException {
-    new DeleteTask(id).call();
-  }
-
   private Collection<ChangeIndex> getWriteIndexes() {
     return indexes != null
         ? indexes.getWriteIndexes()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 6d03df28..aded275 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -384,6 +384,9 @@
 
   private RevisionNoteMap getRevisionNoteMap(RevWalk rw, ObjectId curr)
       throws ConfigInvalidException, OrmException, IOException {
+    if (curr.equals(ObjectId.zeroId())) {
+      return RevisionNoteMap.emptyMap();
+    }
     if (migration.readChanges()) {
       // If reading from changes is enabled, then the old ChangeNotes already
       // parsed the revision notes. We can reuse them as long as the ref hasn't
@@ -394,12 +397,7 @@
         return checkNotNull(ctl.getNotes().revisionNoteMap);
       }
     }
-    NoteMap noteMap;
-    if (!curr.equals(ObjectId.zeroId())) {
-      noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr));
-    } else {
-      noteMap = NoteMap.newEmptyMap();
-    }
+    NoteMap noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr));
     // Even though reading from changes might not be enabled, we need to
     // parse any existing revision notes so we can merge them.
     return RevisionNoteMap.parse(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
index 17f33b0..a804d2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
@@ -43,6 +43,11 @@
     return new RevisionNoteMap(noteMap, ImmutableMap.copyOf(result));
   }
 
+  static RevisionNoteMap emptyMap() {
+    return new RevisionNoteMap(NoteMap.newEmptyMap(),
+        ImmutableMap.<RevId, RevisionNote> of());
+  }
+
   private RevisionNoteMap(NoteMap noteMap,
       ImmutableMap<RevId, RevisionNote> revisionNotes) {
     this.noteMap = noteMap;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 82018fb..312ac89 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -541,6 +541,12 @@
 
     gApi.changes().id(change.getId().get()).current()
       .review(new ReviewInput().label("Code-Review", 1));
+    Map<String, Short> m = gApi.changes()
+        .id(change.getId().get())
+        .reviewer(user.getAccountId().toString())
+        .votes();
+    assertThat(m).hasSize(1);
+    assertThat(m).containsEntry("Code-Review", new Short((short)1));
 
     assertQuery("label:Code-Review=-2");
     assertQuery("label:Code-Review-2");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index bead9b8..29b7987 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -18,10 +18,12 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Supplier;
-import com.google.gerrit.common.EventListener;
-import com.google.gerrit.common.EventSource;
+import com.google.gerrit.common.UserScopedEventListener;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.EventTypes;
@@ -63,7 +65,7 @@
   private IdentifiedUser currentUser;
 
   @Inject
-  private EventSource source;
+  private DynamicSet<UserScopedEventListener> eventListeners;
 
   @Inject
   @StreamCommandExecutor
@@ -75,6 +77,8 @@
 
   private Gson gson;
 
+  private RegistrationHandle eventListenerRegistration;
+
   /** Special event to notify clients they missed other events. */
   private static final class DroppedOutputEvent extends Event {
     private final static String TYPE = "dropped-output";
@@ -87,16 +91,6 @@
     EventTypes.register(DroppedOutputEvent.TYPE, DroppedOutputEvent.class);
   }
 
-  private final EventListener listener = new EventListener() {
-    @Override
-    public void onEvent(final Event event) {
-      if (subscribedToEvents.isEmpty()
-          || subscribedToEvents.contains(event.getType())) {
-        offer(event);
-      }
-    }
-  };
-
   private final CancelableRunnable writer = new CancelableRunnable() {
     @Override
     public void run() {
@@ -150,7 +144,21 @@
     }
 
     stdout = toPrintWriter(out);
-    source.addEventListener(listener, currentUser);
+    eventListenerRegistration =
+        eventListeners.add(new UserScopedEventListener() {
+          @Override
+          public void onEvent(final Event event) {
+            if (subscribedToEvents.isEmpty()
+                || subscribedToEvents.contains(event.getType())) {
+              offer(event);
+            }
+          }
+
+          @Override
+          public CurrentUser getUser() {
+            return currentUser;
+          }
+        });
 
     gson = new GsonBuilder()
         .registerTypeAdapter(Supplier.class, new SupplierSerializer())
@@ -159,7 +167,7 @@
 
   @Override
   protected void onExit(final int rc) {
-    source.removeEventListener(listener);
+    eventListenerRegistration.remove();
 
     synchronized (taskLock) {
       done = true;
@@ -170,7 +178,7 @@
 
   @Override
   public void destroy() {
-    source.removeEventListener(listener);
+    eventListenerRegistration.remove();
 
     final boolean exit;
     synchronized (taskLock) {
@@ -218,7 +226,7 @@
         // destroy() above, or it closed the stream and is no longer
         // accepting output. Either way terminate this instance.
         //
-        source.removeEventListener(listener);
+        eventListenerRegistration.remove();
         flush();
         onExit(0);
         return;
diff --git a/lib/BUCK b/lib/BUCK
index 8bf12af..f21f207 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -61,8 +61,8 @@
 
 maven_jar(
   name = 'guava',
-  id = 'com.google.guava:guava:19.0-rc2',
-  sha1 = '93e17f60bc524c2610b41c494bb829c11ca89436',
+  id = 'com.google.guava:guava:19.0',
+  sha1 = '6ce200f6b23222af3d8abb6b6459e6c44f4bb0e9',
   license = 'Apache2.0',
 )
 
diff --git a/lib/codemirror/cm.defs b/lib/codemirror/cm.defs
index 9677058..f970048 100644
--- a/lib/codemirror/cm.defs
+++ b/lib/codemirror/cm.defs
@@ -41,6 +41,7 @@
 
 # Available modes must be enumerated here,
 # in gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java,
+# gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java,
 # and in CodeMirror's own mode/meta.js script.
 CM_MODES = [
   'clike',
diff --git a/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html
rename to polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index 7615d15..0d549d9 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -16,7 +16,7 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="../gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
 
 <dom-module id="gr-account-dropdown">
   <style>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
similarity index 100%
rename from polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.js
rename to polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
diff --git a/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
similarity index 100%
rename from polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html
rename to polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
diff --git a/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
similarity index 98%
rename from polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
rename to polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 9988c28..a47c50f 100644
--- a/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -15,7 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
 
 <dom-module id="gr-keyboard-shortcuts-dialog">
   <template>
diff --git a/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
similarity index 100%
rename from polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
rename to polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
new file mode 100644
index 0000000..129a321
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -0,0 +1,87 @@
+<!--
+Copyright (C) 2016 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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html">
+<link rel="import" href="../gr-search-bar/gr-search-bar.html">
+
+<dom-module id="gr-main-header">
+  <template>
+    <style>
+      :host {
+        align-items: center;
+        display: flex;
+        overflow: hidden;
+      }
+      .bigTitle {
+        color: var(--primary-text-color);
+        font-size: 1.75em;
+        text-decoration: none;
+      }
+      .bigTitle:hover {
+        text-decoration: underline;
+      }
+      .rightItems {
+        display: flex;
+        flex: 1;
+        justify-content: flex-end;
+      }
+      gr-search-bar {
+        margin-left: .5em;
+        width: 500px;
+      }
+      .accountContainer:not(.loggedIn):not(.loggedOut) .loginButton,
+      .accountContainer:not(.loggedIn):not(.loggedOut) gr-account-dropdown,
+      .accountContainer.loggedIn .loginButton,
+      .accountContainer.loggedOut gr-account-dropdown {
+        display: none;
+      }
+      .accountContainer {
+        align-items: center;
+        display: flex;
+        margin-left: var(--default-horizontal-margin);
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+      @media screen and (max-width: 50em) {
+        .bigTitle {
+          font-size: 14px;
+          font-weight: bold;
+        }
+        gr-search-bar {
+          display: none;
+        }
+        .accountContainer {
+          margin-left: .5em !important;
+        }
+      }
+    </style>
+    <a href="/" class="bigTitle">PolyGerrit</a>
+    <div class="rightItems">
+      <gr-search-bar value="{{params.query}}" role="search"></gr-search-bar>
+      <div class="accountContainer" id="accountContainer">
+        <a class="loginButton" href="/login" on-tap="_loginTapHandler">Login</a>
+        <gr-account-dropdown account="[[account]]"></gr-account-dropdown>
+      </div>
+    </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-main-header.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
new file mode 100644
index 0000000..ca57625
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -0,0 +1,35 @@
+// Copyright (C) 2016 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.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-main-header',
+
+    hostAttributes: {
+      role: 'banner'
+    },
+
+    properties: {
+    },
+
+    attached: function() {
+      this.$.restAPI.getAccount().then(function(account) {
+        var loggedIn = !!account;
+        this.$.accountContainer.classList.toggle('loggedIn', loggedIn);
+        this.$.accountContainer.classList.toggle('loggedOut', !loggedIn);
+      }.bind(this));
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
similarity index 95%
rename from polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html
rename to polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index 900245b..c8c4523 100644
--- a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -17,7 +17,7 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
-<link rel="import" href="../gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
 
 <dom-module id="gr-search-bar">
   <template>
diff --git a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
similarity index 100%
rename from polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.js
rename to polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
diff --git a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
similarity index 100%
rename from polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html
rename to polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index e7fa31c..fd6525d 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -18,16 +18,17 @@
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../styles/app-theme.html">
 
+<link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
+<link rel="import" href="./core/gr-main-header/gr-main-header.html">
+
 <link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
 <link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
 <link rel="import" href="./change/gr-change-view/gr-change-view.html">
 <link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
 
-<link rel="import" href="./shared/gr-account-dropdown/gr-account-dropdown.html">
 <link rel="import" href="./shared/gr-ajax/gr-ajax.html">
-<link rel="import" href="./shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
 <link rel="import" href="./shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="./shared/gr-search-bar/gr-search-bar.html">
+<link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <script src="../bower_components/page/page.js"></script>
 <script src="../scripts/app.js"></script>
@@ -42,83 +43,25 @@
         min-height: 100vh;
         flex-direction: column;
       }
-      header,
+      gr-main-header,
       footer {
         background-color: var(--primary-color);
         color: var(--primary-text-color);
         padding: .5rem var(--default-horizontal-margin);
       }
-      header {
-        align-items: center;
-        display: flex;
-        overflow: hidden;
-      }
       main {
         flex: 1;
       }
-      .bigTitle {
-        color: var(--primary-text-color);
-        font-size: 1.75em;
-        text-decoration: none;
-      }
-      .bigTitle:hover {
-        text-decoration: underline;
-      }
-      .headerRightItems {
-        display: flex;
-        flex: 1;
-        justify-content: flex-end;
-      }
-      gr-search-bar {
-        margin-left: .5em;
-        width: 500px;
-      }
-      .accountContainer:not(.loggedIn):not(.loggedOut) .loginButton,
-      .accountContainer:not(.loggedIn):not(.loggedOut) gr-account-dropdown,
-      .accountContainer.loggedIn .loginButton,
-      .accountContainer.loggedOut gr-account-dropdown {
-        display: none;
-      }
-      .accountContainer {
-        align-items: center;
-        display: flex;
-        margin-left: var(--default-horizontal-margin);
-        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis;
-      }
       .feedback {
         color: #b71c1c;
       }
-      @media screen and (max-width: 50em) {
-        .bigTitle {
-          font-size: 14px;
-          font-weight: bold;
-        }
-        gr-search-bar {
-          display: none;
-        }
-        .accountContainer {
-          margin-left: .5em !important;
-        }
-      }
     </style>
-    <gr-ajax auto url="/accounts/self/detail" last-response="{{account}}"></gr-ajax>
     <gr-ajax auto url="/config/server/info" last-response="{{config}}"></gr-ajax>
     <gr-ajax auto url="/config/server/version" last-response="{{version}}"></gr-ajax>
     <gr-ajax id="diffPreferencesXHR"
         url="/accounts/self/preferences.diff"
         last-response="{{_diffPreferences}}"></gr-ajax>
-    <header role="banner">
-      <a href="/" class="bigTitle">PolyGerrit</a>
-      <div class="headerRightItems">
-        <gr-search-bar value="{{params.query}}" role="search"></gr-search-bar>
-        <div class="accountContainer" id="accountContainer">
-          <a class="loginButton" href="/login" on-tap="_loginTapHandler">Login</a>
-          <gr-account-dropdown account="[[account]]"></gr-account-dropdown>
-        </div>
-      </div>
-    </header>
+    <gr-main-header></gr-main-header>
     <main>
       <template is="dom-if" if="{{_showChangeListView}}" restamp="true">
         <gr-change-list-view
@@ -167,6 +110,7 @@
           view="[[params.view]]"
           on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
     </gr-overlay>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-app.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 23ee6c6..475ea0d 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -77,6 +77,12 @@
       return !!(this.account && Object.keys(this.account).length > 0);
     },
 
+    attached: function() {
+      this.$.restAPI.getAccount().then(function(account) {
+        this.account = account;
+      }.bind(this));
+    },
+
     ready: function() {
       this._viewState = {
         changeView: {
@@ -98,8 +104,6 @@
 
     _accountChanged: function() {
       this._resolveAccountReady();
-      this.$.accountContainer.classList.toggle('loggedIn', this.loggedIn);
-      this.$.accountContainer.classList.toggle('loggedOut', !this.loggedIn);
       if (this.loggedIn) {
         this.$.diffPreferencesXHR.generateRequest();
       } else {
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 7367439..f69f8b1 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
@@ -20,6 +20,17 @@
   Polymer({
     is: 'gr-rest-api-interface',
 
+    properties: {
+      _cache: {
+        type: Object,
+        value: {},  // Intentional to share the object accross instances.
+      },
+      _sharedFetchPromises: {
+        type: Object,
+        value: {},  // Intentional to share the object accross instances.
+      },
+    },
+
     fetchJSON: function(url, opt_cancelCondition, opt_params, opt_opts) {
       opt_opts = opt_opts || {};
 
@@ -52,7 +63,13 @@
         }
 
         return response.text().then(function(text) {
-          return JSON.parse(text.substring(JSON_PREFIX.length));
+          var result;
+          try {
+            result = JSON.parse(text.substring(JSON_PREFIX.length));
+          } catch (_) {
+            result = null;
+          }
+          return result;
         });
       }).catch(function(err) {
         if (opt_opts.noCredentials) {
@@ -65,14 +82,35 @@
       }.bind(this));
     },
 
-    getAccountDetail: function() {
-      return this.fetchJSON('/accounts/self/detail');
+    getAccount: function() {
+      return this._fetchSharedCacheURL('/accounts/self/detail');
+    },
+
+    _fetchSharedCacheURL: function(url) {
+      if (this._sharedFetchPromises[url]) {
+        return this._sharedFetchPromises[url];
+      }
+      // TODO(andybons): Periodic cache invalidation.
+      if (this._cache[url] !== undefined) {
+        return this._cache[url];
+      }
+      this._sharedFetchPromises[url] = this.fetchJSON(url).then(
+        function(response) {
+          if (response !== undefined) {
+            this._cache[url] = response;
+          }
+          this._sharedFetchPromises[url] = undefined;
+          return response;
+        }.bind(this)).catch(function(err) {
+          this._sharedFetchPromises[url] = undefined;
+          throw err;
+        });
+      return this._sharedFetchPromises[url];
     },
 
     getDiff: function(changeNum, basePatchNum, patchNum, path,
         opt_cancelCondition) {
-      var url = this._changeBaseURL(changeNum, patchNum) + '/files/' +
-          encodeURIComponent(path) + '/diff';
+      var url = this._getDiffFetchURL(changeNum, patchNum, path);
       var params =  {
         context: 'ALL',
         intraline: null
@@ -84,6 +122,11 @@
       return this.fetchJSON(url, opt_cancelCondition, params);
     },
 
+    _getDiffFetchURL: function(changeNum, patchNum, path) {
+      return this._changeBaseURL(changeNum, patchNum) + '/files/' +
+          encodeURIComponent(path) + '/diff';
+    },
+
     getDiffComments: function(changeNum, basePatchNum, patchNum, path) {
       return this._getDiffComments(changeNum, basePatchNum, patchNum, path,
           '/comments');
@@ -102,7 +145,7 @@
       var promises = [];
       var comments;
       var baseComments;
-      var url = this._getDiffFetchURL(changeNum, patchNum, endpoint);
+      var url = this._getDiffCommentsFetchURL(changeNum, patchNum, endpoint);
       promises.push(this.fetchJSON(url).then(function(response) {
         comments = response[path] || [];
         if (basePatchNum == PARENT_PATCH_NUM) {
@@ -112,7 +155,8 @@
       }.bind(this)));
 
       if (basePatchNum != PARENT_PATCH_NUM) {
-        var baseURL = this._getDiffFetchURL(changeNum, basePatchNum, endpoint);
+        var baseURL = this._getDiffCommentsFetchURL(changeNum, basePatchNum,
+            endpoint);
         promises.push(this.fetchJSON(baseURL).then(function(response) {
           baseComments = (response[path] || []).filter(withoutParent);
         }));
@@ -126,7 +170,7 @@
       });
     },
 
-    _getDiffFetchURL: function(changeNum, patchNum, endpoint) {
+    _getDiffCommentsFetchURL: function(changeNum, patchNum, endpoint) {
       return this._changeBaseURL(changeNum, patchNum) + endpoint;
     },
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 79c175d..c397c8f 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -53,6 +53,23 @@
       });
     });
 
+    test('cached results', function(done) {
+      var n = 0;
+      var fetchJSONStub = sinon.stub(element, 'fetchJSON', function() {
+        return Promise.resolve(++n);
+      });
+      var promises = [];
+      promises.push(element._fetchSharedCacheURL('/foo'));
+      promises.push(element._fetchSharedCacheURL('/foo'));
+      promises.push(element._fetchSharedCacheURL('/foo'));
+
+      Promise.all(promises).then(function(results) {
+        assert.deepEqual(results, [1, 1, 1]);
+        fetchJSONStub.restore();
+        done();
+      });
+    });
+
     test('params are properly encoded', function() {
       var fetchStub = sinon.stub(window, 'fetch', function() {
         return Promise.resolve({ text: function() {
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index fcfaaa3..31d93b7 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -37,6 +37,8 @@
     '../elements/change/gr-reviewer-list/gr-reviewer-list_test.html',
     '../elements/change-list/gr-change-list/gr-change-list_test.html',
     '../elements/change-list/gr-change-list-item/gr-change-list-item_test.html',
+    '../elements/core/gr-account-dropdown/gr-account-dropdown_test.html',
+    '../elements/core/gr-search-bar/gr-search-bar_test.html',
     '../elements/diff/gr-diff/gr-diff_test.html',
     '../elements/diff/gr-diff-comment/gr-diff-comment_test.html',
     '../elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
@@ -44,7 +46,6 @@
     '../elements/diff/gr-diff-side/gr-diff-side_test.html',
     '../elements/diff/gr-diff-view/gr-diff-view_test.html',
     '../elements/diff/gr-patch-range-select/gr-patch-range-select_test.html',
-    '../elements/shared/gr-account-dropdown/gr-account-dropdown_test.html',
     '../elements/shared/gr-account-label/gr-account-label_test.html',
     '../elements/shared/gr-account-link/gr-account-link_test.html',
     '../elements/shared/gr-avatar/gr-avatar_test.html',
@@ -53,7 +54,6 @@
     '../elements/shared/gr-date-formatter/gr-date-formatter_test.html',
     '../elements/shared/gr-linked-text/gr-linked-text_test.html',
     '../elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
-    '../elements/shared/gr-search-bar/gr-search-bar_test.html',
   ].forEach(function(file) {
     testFiles.push(file);
     testFiles.push(file + '?dom=shadow');