Merge "Allow admins to index a change even if the branch is not Readable for them" into stable-2.16
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 3ec989e..7be8c32 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1376,6 +1376,28 @@
   }
 ----
 
+[[index.changes]]
+=== Index a set of changes
+
+This endpoint allows Gerrit admins to index a set of changes with one request
+by providing a link:#index-changes-input[IndexChangesInput] entity.
+
+Using this endpoint Gerrit admins can also index change(s) which are not visible to them.
+
+.Request
+----
+  POST /config/server/index.changes HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {changes: ["foo~101", "bar~202"]}
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+----
+
 
 [[ids]]
 == IDs
@@ -1825,6 +1847,17 @@
 Only set for disk caches.
 |==================================
 
+[[index-changes-input]]
+=== IndexChangesInput
+The `IndexChangesInput` contains a list of numerical changes IDs to index.
+
+[options="header",cols="1,^2,4"]
+|================================
+|Field Name         ||Description
+|`changes`   ||
+List of link:rest-api-changes.html#change-id[change-ids]
+|================================
+
 [[jvm-summary-info]]
 === JvmSummaryInfo
 The `JvmSummaryInfo` entity contains information about the JVM.
diff --git a/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java b/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
index 286b045..27ed603 100644
--- a/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
+++ b/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
@@ -37,7 +37,7 @@
     countsByChange.clear();
   }
 
-  long getCount(ChangeInfo info) {
+  public long getCount(ChangeInfo info) {
     return countsByChange.get(info._number);
   }
 
diff --git a/java/com/google/gerrit/server/restapi/config/IndexChanges.java b/java/com/google/gerrit/server/restapi/config/IndexChanges.java
new file mode 100644
index 0000000..b178118
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/IndexChanges.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2019 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.server.restapi.config;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangeFinder;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.restapi.config.IndexChanges.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Set;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@Singleton
+public class IndexChanges implements RestModifyView<ConfigResource, Input> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public static class Input {
+    public Set<String> changes;
+  }
+
+  private final ChangeFinder changeFinder;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final ChangeData.Factory changeDataFactory;
+  private final ChangeIndexer indexer;
+
+  @Inject
+  IndexChanges(
+      ChangeFinder changeFinder,
+      SchemaFactory<ReviewDb> schemaFactory,
+      ChangeData.Factory changeDataFactory,
+      ChangeIndexer indexer) {
+    this.changeFinder = changeFinder;
+    this.schemaFactory = schemaFactory;
+    this.changeDataFactory = changeDataFactory;
+    this.indexer = indexer;
+  }
+
+  @Override
+  public Object apply(ConfigResource resource, Input input) throws OrmException {
+    if (input == null || input.changes == null) {
+      return Response.ok("Nothing to index");
+    }
+
+    try (ReviewDb db = schemaFactory.open()) {
+      for (String id : input.changes) {
+        for (ChangeNotes n : changeFinder.find(id)) {
+          try {
+            indexer.index(changeDataFactory.create(db, n));
+            logger.atFine().log("Indexed change %s", id);
+          } catch (IOException e) {
+            logger.atSevere().withCause(e).log("Failed to index change %s", id);
+          }
+        }
+      }
+    }
+
+    return Response.ok("Indexed changes " + input.changes);
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/Module.java b/java/com/google/gerrit/server/restapi/config/Module.java
index c4a6f56..78a7f48 100644
--- a/java/com/google/gerrit/server/restapi/config/Module.java
+++ b/java/com/google/gerrit/server/restapi/config/Module.java
@@ -37,6 +37,7 @@
     get(CONFIG_KIND, "version").to(GetVersion.class);
     get(CONFIG_KIND, "info").to(GetServerInfo.class);
     post(CONFIG_KIND, "check.consistency").to(CheckConsistency.class);
+    post(CONFIG_KIND, "index.changes").to(IndexChanges.class);
     post(CONFIG_KIND, "reload").to(ReloadConfig.class);
     get(CONFIG_KIND, "preferences").to(GetPreferences.class);
     put(CONFIG_KIND, "preferences").to(SetPreferences.class);
diff --git a/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java
index 4a2c81b..2e5ea00 100644
--- a/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java
@@ -57,7 +57,8 @@
           RestCall.get("/config/server/capabilities"),
           RestCall.get("/config/server/caches"),
           RestCall.post("/config/server/caches"),
-          RestCall.get("/config/server/tasks"));
+          RestCall.get("/config/server/tasks"),
+          RestCall.post("/config/server/index.changes"));
 
   /**
    * Cache REST endpoints to be tested, the URLs contain a placeholder for the cache identifier.
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/IndexChangesIT.java b/javatests/com/google/gerrit/acceptance/rest/config/IndexChangesIT.java
new file mode 100644
index 0000000..fa35d19
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/config/IndexChangesIT.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2019 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.acceptance.rest.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ChangeIndexedCounter;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.restapi.config.IndexChanges;
+import com.google.inject.Inject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class IndexChangesIT extends AbstractDaemonTest {
+
+  @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
+
+  private ChangeIndexedCounter changeIndexedCounter;
+  private RegistrationHandle changeIndexedCounterHandle;
+
+  @Before
+  public void addChangeIndexedCounter() {
+    changeIndexedCounter = new ChangeIndexedCounter();
+    changeIndexedCounterHandle = changeIndexedListeners.add("gerrit", changeIndexedCounter);
+  }
+
+  @After
+  public void removeChangeIndexedCounter() {
+    if (changeIndexedCounterHandle != null) {
+      changeIndexedCounterHandle.remove();
+    }
+  }
+
+  @Test
+  public void indexRequestFromNonAdminRejected() throws Exception {
+    String changeId = createChange().getChangeId();
+    IndexChanges.Input in = new IndexChanges.Input();
+    in.changes = ImmutableSet.of(changeId);
+    changeIndexedCounter.clear();
+    userRestSession.post("/config/server/index.changes", in).assertForbidden();
+    assertThat(changeIndexedCounter.getCount(info(changeId))).isEqualTo(0);
+  }
+
+  @Test
+  public void indexVisibleChange() throws Exception {
+    String changeId = createChange().getChangeId();
+    IndexChanges.Input in = new IndexChanges.Input();
+    in.changes = ImmutableSet.of(changeId);
+    changeIndexedCounter.clear();
+    adminRestSession.post("/config/server/index.changes", in).assertOK();
+    assertThat(changeIndexedCounter.getCount(info(changeId))).isEqualTo(1);
+  }
+
+  @Test
+  public void indexNonVisibleChange() throws Exception {
+    String changeId = createChange().getChangeId();
+    ChangeInfo changeInfo = info(changeId);
+    blockRead("refs/heads/master");
+    IndexChanges.Input in = new IndexChanges.Input();
+    changeIndexedCounter.clear();
+    in.changes = ImmutableSet.of(changeId);
+    adminRestSession.post("/config/server/index.changes", in).assertOK();
+    assertThat(changeIndexedCounter.getCount(changeInfo)).isEqualTo(1);
+  }
+
+  @Test
+  public void indexMultipleChanges() throws Exception {
+    ImmutableSet.Builder<String> changeIds = ImmutableSet.builder();
+    for (int i = 0; i < 10; i++) {
+      changeIds.add(createChange().getChangeId());
+    }
+    IndexChanges.Input in = new IndexChanges.Input();
+    in.changes = changeIds.build();
+    changeIndexedCounter.clear();
+    adminRestSession.post("/config/server/index.changes", in).assertOK();
+    for (String changeId : in.changes) {
+      assertThat(changeIndexedCounter.getCount(info(changeId))).isEqualTo(1);
+    }
+  }
+}