Merge "PG: Don't get gitweb weblink from ServerInfo" into stable-2.16
diff --git a/java/com/google/gerrit/extensions/api/changes/FileApi.java b/java/com/google/gerrit/extensions/api/changes/FileApi.java
index 89dc269..39cf2b7 100644
--- a/java/com/google/gerrit/extensions/api/changes/FileApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/FileApi.java
@@ -39,6 +39,9 @@
    */
   DiffRequest diffRequest() throws RestApiException;
 
+  /** Set the file reviewed or not reviewed */
+  void setReviewed(boolean reviewed) throws RestApiException;
+
   abstract class DiffRequest {
     private String base;
     private Integer context;
@@ -123,5 +126,10 @@
     public DiffRequest diffRequest() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public void setReviewed(boolean reviewed) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/java/com/google/gerrit/server/api/changes/FileApiImpl.java
index 6e18bb8..f2d0ef8 100644
--- a/java/com/google/gerrit/server/api/changes/FileApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -18,11 +18,13 @@
 
 import com.google.gerrit.extensions.api.changes.FileApi;
 import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.restapi.change.GetContent;
 import com.google.gerrit.server.restapi.change.GetDiff;
+import com.google.gerrit.server.restapi.change.Reviewed;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -33,12 +35,21 @@
 
   private final GetContent getContent;
   private final GetDiff getDiff;
+  private final Reviewed.PutReviewed putReviewed;
+  private final Reviewed.DeleteReviewed deleteReviewed;
   private final FileResource file;
 
   @Inject
-  FileApiImpl(GetContent getContent, GetDiff getDiff, @Assisted FileResource file) {
+  FileApiImpl(
+      GetContent getContent,
+      GetDiff getDiff,
+      Reviewed.PutReviewed putReviewed,
+      Reviewed.DeleteReviewed deleteReviewed,
+      @Assisted FileResource file) {
     this.getContent = getContent;
     this.getDiff = getDiff;
+    this.putReviewed = putReviewed;
+    this.deleteReviewed = deleteReviewed;
     this.file = file;
   }
 
@@ -88,6 +99,19 @@
     };
   }
 
+  @Override
+  public void setReviewed(boolean reviewed) throws RestApiException {
+    try {
+      if (reviewed) {
+        putReviewed.apply(file, new Input());
+      } else {
+        deleteReviewed.apply(file, new Input());
+      }
+    } catch (Exception e) {
+      throw asRestApiException(String.format("Cannot set %sreviewed", reviewed ? "" : "un"), e);
+    }
+  }
+
   private DiffInfo get(DiffRequest r) throws RestApiException {
     if (r.getBase() != null) {
       getDiff.setBase(r.getBase());
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index aa455e6..39475fa 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.toSet;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
@@ -308,4 +309,14 @@
       }
     }
   }
+
+  @VisibleForTesting
+  public void evictAllByName() {
+    byName.invalidateAll();
+  }
+
+  @VisibleForTesting
+  public long sizeAllByName() {
+    return byName.size();
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index e5f14064..a503323 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -16,8 +16,6 @@
 
 import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
@@ -62,7 +60,6 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -75,6 +72,7 @@
 import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
@@ -352,7 +350,8 @@
     PermissionBackend.WithUser perm = permissionBackend.user(currentUser);
     final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>();
     try {
-      for (Project.NameKey projectName : filter(perm)) {
+      Iterable<Project.NameKey> projectNames = filter(perm)::iterator;
+      for (Project.NameKey projectName : projectNames) {
         final ProjectState e = projectCache.get(projectName);
         if (e == null || (e.getProject().getState() == HIDDEN && !all && state != HIDDEN)) {
           // If we can't get it from the cache, pretend it's not present.
@@ -506,55 +505,28 @@
     }
   }
 
-  private Collection<Project.NameKey> filter(PermissionBackend.WithUser perm)
-      throws BadRequestException, PermissionBackendException {
-    Stream<Project.NameKey> matches = scan();
+  private Stream<Project.NameKey> filter(PermissionBackend.WithUser perm)
+      throws BadRequestException {
+    Stream<Project.NameKey> matches = StreamSupport.stream(scan().spliterator(), false);
     if (type == FilterType.PARENT_CANDIDATES) {
-      matches = parentsOf(matches);
+      matches =
+          matches.map(projectCache::get).map(this::parentOf).filter(Objects::nonNull).sorted();
     }
-
-    List<Project.NameKey> results = new ArrayList<>();
-    List<Project.NameKey> projectNameKeys = matches.sorted().collect(toList());
-    for (Project.NameKey nameKey : projectNameKeys) {
-      ProjectState state = projectCache.get(nameKey);
-      requireNonNull(state, () -> String.format("Failed to load project %s", nameKey));
-
-      // Hidden projects(permitsRead = false) should only be accessible by the project owners.
-      // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
-      // be allowed for other users). Allowing project owners to access here will help them to view
-      // and update the config of hidden projects easily.
-      ProjectPermission permissionToCheck =
-          state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
-      try {
-        perm.project(nameKey).check(permissionToCheck);
-        results.add(nameKey);
-      } catch (AuthException e) {
-        // Not added to results.
-      }
-    }
-
-    return results;
+    return matches.filter(p -> perm.project(p).testOrFalse(ProjectPermission.ACCESS));
   }
 
-  private Stream<Project.NameKey> parentsOf(Stream<Project.NameKey> matches) {
-    return matches
-        .map(
-            p -> {
-              ProjectState ps = projectCache.get(p);
-              if (ps != null) {
-                Project.NameKey parent = ps.getProject().getParent();
-                if (parent != null) {
-                  if (projectCache.get(parent) != null) {
-                    return parent;
-                  }
-                  logger.atWarning().log(
-                      "parent project %s of project %s not found", parent.get(), ps.getName());
-                }
-              }
-              return null;
-            })
-        .filter(Objects::nonNull)
-        .distinct();
+  private Project.NameKey parentOf(ProjectState ps) {
+    if (ps == null) {
+      return null;
+    }
+    Project.NameKey parent = ps.getProject().getParent();
+    if (parent != null) {
+      if (projectCache.get(parent) != null) {
+        return parent;
+      }
+      logger.atWarning().log("parent project %s of project %s not found", ps.getName());
+    }
+    return null;
   }
 
   private boolean isParentAccessible(
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index bde042f..4594689 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -960,6 +960,21 @@
   }
 
   @Test
+  public void setUnsetReviewedFlagByFileApi() throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r = push.to("refs/for/master");
+
+    gApi.changes().id(r.getChangeId()).current().file(PushOneCommit.FILE_NAME).setReviewed(true);
+
+    assertThat(Iterables.getOnlyElement(gApi.changes().id(r.getChangeId()).current().reviewed()))
+        .isEqualTo(PushOneCommit.FILE_NAME);
+
+    gApi.changes().id(r.getChangeId()).current().file(PushOneCommit.FILE_NAME).setReviewed(false);
+
+    assertThat(gApi.changes().id(r.getChangeId()).current().reviewed()).isEmpty();
+  }
+
+  @Test
   public void mergeable() throws Exception {
     ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index cd88a56..8aa42a2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.testing.Util;
 import java.util.List;
 import java.util.Map;
@@ -87,6 +88,7 @@
 
   @Test
   public void listProjectsWithLimit() throws Exception {
+    ProjectCacheImpl projectCacheImpl = (ProjectCacheImpl) projectCache;
     for (int i = 0; i < 5; i++) {
       createProject("someProject" + i);
     }
@@ -94,9 +96,12 @@
     String p = name("");
     // 5, plus p which was automatically created.
     int n = 6;
+    projectCacheImpl.evictAllByName();
     for (int i = 1; i <= n + 2; i++) {
       assertThatNameList(gApi.projects().list().withPrefix(p).withLimit(i).get())
           .hasSize(Math.min(i, n));
+      assertThat(projectCacheImpl.sizeAllByName())
+          .isAtMost((long) (i + 2)); // 2 = AllProjects + AllUsers
     }
   }
 
@@ -190,6 +195,27 @@
   }
 
   @Test
+  public void listParentCandidates() throws Exception {
+    Map<String, ProjectInfo> result =
+        gApi.projects().list().withType(FilterType.PARENT_CANDIDATES).getAsMap();
+    assertThat(result).hasSize(1);
+    assertThat(result).containsKey(allProjects.get());
+
+    // Create a new project with 'project' as parent
+    Project.NameKey testProject = createProject(name("test"), project);
+
+    // Parent candidates are All-Projects and 'project'
+    assertThatNameList(filter(gApi.projects().list().withType(FilterType.PARENT_CANDIDATES).get()))
+        .containsExactly(allProjects, project)
+        .inOrder();
+
+    // All projects are listed
+    assertThatNameList(filter(gApi.projects().list().get()))
+        .containsExactly(allProjects, allUsers, testProject, project)
+        .inOrder();
+  }
+
+  @Test
   public void listWithHiddenAndReadonlyProjects() throws Exception {
     Project.NameKey hidden = createProject("project-to-hide");
     Project.NameKey readonly = createProject("project-to-read");