Merge "Minor improvements to non-personal dashboards"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index d9ab64d..5a2fc2e 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -384,6 +384,13 @@
 * `CUSTOM_KEYED_VALUES`: include the custom key-value map
 ---
 
+[[star]]
+--
+* `STAR`: include the `starred` field in
+   link:#change-info[ChangeInfo], which indicates if the change is starred
+   by the current user or not.
+--
+
 .Request
 ----
   GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES&o=DOWNLOAD_COMMANDS HTTP/1.0
@@ -6972,6 +6979,7 @@
 link:rest-api-accounts.html#account-info[ AccountInfo] entity.
 |`starred`            |not set if `false`|
 Whether the calling user has starred this change with the default label.
+Only set if link:#star[requested].
 |`stars`              |optional|
 A list of star labels that are applied by the calling user to this
 change. The labels are lexicographically sorted.
diff --git a/java/com/google/gerrit/extensions/client/ListChangesOption.java b/java/com/google/gerrit/extensions/client/ListChangesOption.java
index de4326e..e2a7c1e 100644
--- a/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -91,7 +91,10 @@
   SUBMIT_REQUIREMENTS(24),
 
   /** Include custom keyed values. */
-  CUSTOM_KEYED_VALUES(25);
+  CUSTOM_KEYED_VALUES(25),
+
+  /** Include the 'starred' field, that is if the change is starred by the current user . */
+  STAR(26);
 
   private final int value;
 
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 2d18054..cf04029 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -49,10 +49,12 @@
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.NavigableSet;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -217,6 +219,29 @@
   }
 
   /**
+   * Returns a subset of change IDs among the input {@code changeIds} list that are starred by the
+   * {@code caller} user.
+   */
+  public Set<Change.Id> areStarred(
+      Repository allUsersRepo, List<Change.Id> changeIds, Account.Id caller) {
+    List<String> starRefs =
+        changeIds.stream()
+            .map(c -> RefNames.refsStarredChanges(c, caller))
+            .collect(Collectors.toList());
+    try {
+      return allUsersRepo.getRefDatabase().exactRef(starRefs.toArray(new String[0])).keySet()
+          .stream()
+          .map(r -> Change.Id.fromAllUsersRef(r))
+          .collect(Collectors.toSet());
+    } catch (IOException e) {
+      logger.atWarning().withCause(e).log(
+          "Failed getting starred changes for account %d within changes: %s",
+          caller.get(), Joiner.on(", ").join(changeIds));
+      return ImmutableSet.of();
+    }
+  }
+
+  /**
    * Unstar the given change for all users.
    *
    * <p>Intended for use only when we're about to delete a change. For that reason, the change is
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 839629e..4875978 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -31,6 +31,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWER_UPDATES;
 import static com.google.gerrit.extensions.client.ListChangesOption.SKIP_DIFFSTAT;
+import static com.google.gerrit.extensions.client.ListChangesOption.STAR;
 import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
 import static com.google.gerrit.extensions.client.ListChangesOption.SUBMIT_REQUIREMENTS;
 import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
@@ -100,10 +101,12 @@
 import com.google.gerrit.server.account.AccountInfoComparator;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.cancellation.RequestCancelledException;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.experiments.ExperimentFeatures;
 import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -133,6 +136,7 @@
 import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
 
 /**
  * Produces {@link ChangeInfo} (which is serialized to JSON afterwards) from {@link ChangeData}.
@@ -222,12 +226,15 @@
     }
   }
 
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsers;
   private final Provider<CurrentUser> userProvider;
   private final PermissionBackend permissionBackend;
   private final ChangeData.Factory changeDataFactory;
   private final AccountLoader.Factory accountLoaderFactory;
   private final ImmutableSet<ListChangesOption> options;
   private final ChangeMessagesUtil cmUtil;
+  private final StarredChangesUtil starredChangesUtil;
   private final Provider<ConsistencyChecker> checkerProvider;
   private final ActionJson actionJson;
   private final ChangeNotes.Factory notesFactory;
@@ -247,12 +254,15 @@
 
   @Inject
   ChangeJson(
+      GitRepositoryManager repoManager,
+      AllUsersName allUsers,
       ExperimentFeatures experimentFeatures,
       Provider<CurrentUser> user,
       PermissionBackend permissionBackend,
       ChangeData.Factory cdf,
       AccountLoader.Factory ailf,
       ChangeMessagesUtil cmUtil,
+      StarredChangesUtil starredChangesUtil,
       Provider<ConsistencyChecker> checkerProvider,
       ActionJson actionJson,
       ChangeNotes.Factory notesFactory,
@@ -264,12 +274,15 @@
       @GerritServerConfig Config cfg,
       @Assisted Iterable<ListChangesOption> options,
       @Assisted Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory) {
+    this.repoManager = repoManager;
+    this.allUsers = allUsers;
     this.experimentFeatures = experimentFeatures;
     this.userProvider = user;
     this.changeDataFactory = cdf;
     this.permissionBackend = permissionBackend;
     this.accountLoaderFactory = ailf;
     this.cmUtil = cmUtil;
+    this.starredChangesUtil = starredChangesUtil;
     this.checkerProvider = checkerProvider;
     this.actionJson = actionJson;
     this.notesFactory = notesFactory;
@@ -539,6 +552,9 @@
               "Omitting corrupt change %s from results", cd.getId());
         }
       }
+      if (has(STAR)) {
+        populateStarField(changeInfos);
+      }
       return changeInfos;
     }
   }
@@ -956,6 +972,25 @@
     return map.build();
   }
 
+  /** Populate the 'starred' field. */
+  private void populateStarField(List<ChangeInfo> changeInfos) {
+    // We populate the 'starred' field for all change infos together so that we open the All-Users
+    // repository only once
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
+      List<Change.Id> changeIds =
+          changeInfos.stream().map(c -> Change.id(c._number)).collect(Collectors.toList());
+      Set<Change.Id> starredChanges =
+          starredChangesUtil.areStarred(
+              allUsersRepo, changeIds, userProvider.get().asIdentifiedUser().getAccountId());
+      if (starredChanges.isEmpty()) {
+        return;
+      }
+      changeInfos.stream().forEach(c -> c.starred = starredChanges.contains(Change.id(c._number)));
+    } catch (IOException e) {
+      logger.atWarning().withCause(e).log("Failed to open All-Users repo.");
+    }
+  }
+
   private List<PluginDefinedInfo> getPluginInfos(ChangeData cd) {
     return getPluginInfos(Collections.singleton(cd)).get(cd.getId());
   }
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index a188251..688e5e4 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -83,6 +83,7 @@
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -2886,7 +2887,8 @@
   }
 
   @Test
-  public void byStar() throws Exception {
+  public void byStar_withStarOptionSet() throws Exception {
+    // When star option is set, the 'starred' field is set in the change infos in response.
     repo = createAndOpenProject("repo");
     Change change1 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
 
@@ -2899,6 +2901,32 @@
     // check default star
     assertQuery("has:star", change1);
     assertQuery("is:starred", change1);
+
+    // The 'Star' bit in the change data is also set correctly
+    List<ChangeInfo> changeInfos =
+        gApi.changes().query("has:star").withOptions(ListChangesOption.STAR).get();
+    assertThat(changeInfos.get(0).starred).isTrue();
+  }
+
+  @Test
+  public void byStar_withStarOptionNotSet() throws Exception {
+    // When star option is not set, the 'starred' field is not set in the change infos in response.
+    repo = createAndOpenProject("repo");
+    Change change1 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
+
+    Account.Id user2 =
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
+    requestContext.setContext(newRequestContext(user2));
+
+    gApi.accounts().self().starChange(change1.getId().toString());
+
+    // check default star
+    assertQuery("has:star", change1);
+    assertQuery("is:starred", change1);
+
+    // The 'Star' bit in the change data is not set if the backfilling option is not set
+    List<ChangeInfo> changeInfos = gApi.changes().query("has:star").get();
+    assertThat(changeInfos.get(0).starred).isNull();
   }
 
   @Test
diff --git a/plugins/delete-project b/plugins/delete-project
index b183ee5..79674d9 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit b183ee5230273670f3235cc5b3cf32562ccfb7ee
+Subproject commit 79674d9e00f8458a2f4f6d3a91bd032579c3f25c
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index 6bdabec..2ba0cf8 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -317,7 +317,7 @@
             >
               ${this.renderMarkers(
                 submittedTogetherMarkersPredicate(index)
-              )}${this.renderSubmittedTogetherLine(change, true)}
+              )}${this.renderSubmittedTogetherLine(change)}
             </div>`
         )}
       </gr-related-collapse>
@@ -327,17 +327,14 @@
     </section>`;
   }
 
-  private renderSubmittedTogetherLine(
-    change: ChangeInfo,
-    showSubmittabilityCheck: boolean
-  ) {
+  private renderSubmittedTogetherLine(change: ChangeInfo) {
     const truncatedRepo = truncatePath(change.project, 2);
     return html`
       <gr-related-change
         .label=${this.renderChangeTitle(change)}
         .change=${change}
         .href=${createChangeUrl({change, usp: 'submitted-together'})}
-        ?show-submittable-check=${showSubmittabilityCheck}
+        show-submittable-check
         >${change.subject}</gr-related-change
       >
       <span class="repo" .title=${change.project}>${truncatedRepo}</span
@@ -376,7 +373,7 @@
             >
               ${this.renderMarkers(
                 sameTopicMarkersPredicate(index)
-              )}${this.renderSubmittedTogetherLine(change, false)}
+              )}${this.renderSubmittedTogetherLine(change)}
             </div>`
         )}
       </gr-related-collapse>
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index b8758e2..61534a4 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -88,6 +88,9 @@
   [Timing.WEB_COMPONENTS_READY]: 0,
 };
 
+// List of timers that should NOT be reset before a location change.
+const LOCATION_CHANGE_OK_TIMERS: (string | Timing)[] = [Timing.SEND_REPLY];
+
 const SLOW_RPC_THRESHOLD = 500;
 
 export function initErrorReporter(reportingService: ReportingService) {
@@ -600,6 +603,7 @@
 
   beforeLocationChanged() {
     for (const prop of Object.keys(this._baselines)) {
+      if (LOCATION_CHANGE_OK_TIMERS.includes(prop)) continue;
       delete this._baselines[prop];
     }
     this.time(Timing.CHANGE_DISPLAYED);
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index b1f9646..68ec76a 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -1805,7 +1805,8 @@
     }
     const options = listChangesOptionsToHex(
       ListChangesOption.CURRENT_REVISION,
-      ListChangesOption.CURRENT_COMMIT
+      ListChangesOption.CURRENT_COMMIT,
+      ListChangesOption.SUBMITTABLE
     );
     const params = {
       O: options,
@@ -1855,7 +1856,8 @@
       ListChangesOption.LABELS,
       ListChangesOption.CURRENT_REVISION,
       ListChangesOption.CURRENT_COMMIT,
-      ListChangesOption.DETAILED_LABELS
+      ListChangesOption.DETAILED_LABELS,
+      ListChangesOption.SUBMITTABLE
     );
     const queryTerms = [`topic:${escapeAndWrapSearchOperatorValue(topic)}`];
     if (options?.openChangesOnly) {