Merge "Apply diff preferences immediately after clicking save" into stable-2.16
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 00ac348..7e0b964 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -367,6 +367,7 @@
 * edit
 * elastic
 * git
+* git-upload-archive
 * notedb
 * pgm
 * rest
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 8606f37..ceada64 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -508,7 +508,7 @@
     return url;
   }
 
-  InetSocketAddress getSshdAddress() {
+  public InetSocketAddress getSshdAddress() {
     return sshdAddress;
   }
 
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index a50de1a..482a073 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -15,10 +15,15 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.joining;
 import static org.junit.Assert.fail;
 
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Streams;
+import com.google.common.io.ByteStreams;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -36,6 +41,10 @@
 import com.google.inject.Module;
 import com.google.inject.Provider;
 import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collections;
 import org.eclipse.jgit.lib.Config;
@@ -91,6 +100,10 @@
       return server.getTestInjector();
     }
 
+    public GerritServer getServer() {
+      return server;
+    }
+
     @Override
     public void close() throws Exception {
       try {
@@ -225,4 +238,50 @@
   protected static void runGerrit(Iterable<String>... multiArgs) throws Exception {
     runGerrit(Arrays.stream(multiArgs).flatMap(Streams::stream).toArray(String[]::new));
   }
+
+  protected static String execute(
+      ImmutableList<String> cmd, File dir, ImmutableMap<String, String> env) throws IOException {
+    return execute(cmd, dir, env, null);
+  }
+
+  protected static String execute(
+      ImmutableList<String> cmd,
+      File dir,
+      ImmutableMap<String, String> env,
+      @Nullable Path outputPath)
+      throws IOException {
+    ProcessBuilder pb = new ProcessBuilder(cmd);
+    pb.directory(dir);
+    if (outputPath != null) {
+      pb.redirectOutput(outputPath.toFile());
+    } else {
+      pb.redirectErrorStream(true);
+    }
+    pb.environment().putAll(env);
+    Process p = pb.start();
+    byte[] out;
+    try (InputStream in = p.getInputStream()) {
+      out = ByteStreams.toByteArray(in);
+    } finally {
+      p.getOutputStream().close();
+    }
+
+    int status;
+    try {
+      status = p.waitFor();
+    } catch (InterruptedException e) {
+      InterruptedIOException iioe =
+          new InterruptedIOException(
+              "interrupted waiting for: " + Joiner.on(' ').join(pb.command()));
+      iioe.initCause(e);
+      throw iioe;
+    }
+
+    String result = new String(out, UTF_8);
+    if (status != 0) {
+      throw new IOException(result);
+    }
+
+    return result.trim();
+  }
 }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index fb24cb0..746a386 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -27,7 +27,8 @@
   V7_3("7.3.*"),
   V7_4("7.4.*"),
   V7_5("7.5.*"),
-  V7_6("7.6.*");
+  V7_6("7.6.*"),
+  V7_7("7.7.*");
 
   private final String version;
   private final Pattern pattern;
diff --git a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index b5f3a4f..a3f6f7c 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -91,7 +92,7 @@
             ? permissionBackend.absentUser(user.getAccountId())
             : permissionBackend.user(
                 Optional.of(user)
-                    .filter(u -> u instanceof SingleGroupUser)
+                    .filter(u -> u instanceof SingleGroupUser || u instanceof InternalUser)
                     .orElseGet(anonymousUserProvider::get));
     try {
       withUser.indexedChange(cd, notes).database(db).check(ChangePermission.READ);
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index f8e64f6..214861c 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -32,7 +32,7 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV7() {
-    return getConfig(ElasticVersion.V7_6);
+    return getConfig(ElasticVersion.V7_7);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index e0e8bb4..74f25e4 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -31,7 +31,7 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV7() {
-    return getConfig(ElasticVersion.V7_6);
+    return getConfig(ElasticVersion.V7_7);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
deleted file mode 100644
index 93fc769..0000000
--- a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
+++ /dev/null
@@ -1,168 +0,0 @@
-// Copyright (C) 2015 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.ssh;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
-
-import com.google.common.base.Splitter;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GerritConfig;
-import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.UseSsh;
-import com.google.gerrit.testing.NoteDbMode;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.util.Set;
-import java.util.TreeSet;
-import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
-import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
-import org.eclipse.jgit.transport.PacketLineIn;
-import org.eclipse.jgit.transport.PacketLineOut;
-import org.eclipse.jgit.util.IO;
-import org.junit.Before;
-import org.junit.Test;
-
-@NoHttpd
-@UseSsh
-public class UploadArchiveIT extends AbstractDaemonTest {
-
-  @Before
-  public void setUp() {
-    // There is some Guice request scoping problem preventing this test from
-    // passing in CHECK mode.
-    assume().that(NoteDbMode.get()).isNotEqualTo(NoteDbMode.CHECK);
-  }
-
-  @Test
-  @GerritConfig(name = "download.archive", value = "off")
-  public void archiveFeatureOff() throws Exception {
-    assertArchiveNotPermitted();
-  }
-
-  @Test
-  @GerritConfig(
-      name = "download.archive",
-      values = {"tar", "tbz2", "tgz", "txz"})
-  public void zipFormatDisabled() throws Exception {
-    assertArchiveNotPermitted();
-  }
-
-  @Test
-  public void zipFormat() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String abbreviated = r.getCommit().abbreviate(8).name();
-    String c = command(r, "zip", abbreviated);
-
-    InputStream out =
-        adminSshSession.exec2("git-upload-archive " + project.get(), argumentsToInputStream(c));
-
-    // Wrap with PacketLineIn to read ACK bytes from output stream
-    PacketLineIn in = new PacketLineIn(out);
-    String tmp = in.readString();
-    assertThat(tmp).isEqualTo("ACK");
-    in.readString();
-
-    // Skip length (4 bytes) + 1 byte
-    // to position the output stream to the raw zip stream
-    byte[] buffer = new byte[5];
-    IO.readFully(out, buffer, 0, 5);
-    Set<String> entryNames = new TreeSet<>();
-    try (ZipArchiveInputStream zip = new ZipArchiveInputStream(out)) {
-      ZipArchiveEntry zipEntry = zip.getNextZipEntry();
-      while (zipEntry != null) {
-        String name = zipEntry.getName();
-        entryNames.add(name);
-        zipEntry = zip.getNextZipEntry();
-      }
-    }
-
-    assertThat(entryNames)
-        .containsExactly(
-            String.format("%s/", abbreviated),
-            String.format("%s/%s", abbreviated, PushOneCommit.FILE_NAME))
-        .inOrder();
-  }
-
-  // Make sure we have coverage for the dependency on xz.
-  @Test
-  public void txzFormat() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String abbreviated = r.getCommit().abbreviate(8).name();
-    String c = command(r, "tar.xz", abbreviated);
-
-    try (InputStream out =
-        adminSshSession.exec2("git-upload-archive " + project.get(), argumentsToInputStream(c))) {
-
-      // Wrap with PacketLineIn to read ACK bytes from output stream
-      PacketLineIn in = new PacketLineIn(out);
-      String packet = in.readString();
-      assertThat(packet).isEqualTo("ACK");
-
-      // Discard first bit of data, which should be empty.
-      packet = in.readString();
-      assertThat(packet).isEmpty();
-
-      // Make sure the next one is not on the error channel
-      packet = in.readString();
-
-      // 1 = DATA. It would be nicer to parse the OutputStream with SideBandInputStream from JGit,
-      // but
-      // that is currently not public.
-      char channel = packet.charAt(0);
-      if (channel != 1) {
-        fail("got packet on channel " + (int) channel, packet);
-      }
-    }
-  }
-
-  private String command(PushOneCommit.Result r, String format, String abbreviated) {
-    String c =
-        String.format(
-            "-f=%s --prefix=%s/ %s %s",
-            format, abbreviated, r.getCommit().name(), PushOneCommit.FILE_NAME);
-    return c;
-  }
-
-  private void assertArchiveNotPermitted() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String abbreviated = r.getCommit().abbreviate(8).name();
-    String c = command(r, "zip", abbreviated);
-
-    InputStream out =
-        adminSshSession.exec2("git-upload-archive " + project.get(), argumentsToInputStream(c));
-
-    // Wrap with PacketLineIn to read ACK bytes from output stream
-    PacketLineIn in = new PacketLineIn(out);
-    String tmp = in.readString();
-    assertThat(tmp).isEqualTo("ACK");
-    in.readString();
-    tmp = in.readString();
-    tmp = tmp.substring(1);
-    assertThat(tmp).isEqualTo("fatal: upload-archive not permitted for format zip");
-  }
-
-  private InputStream argumentsToInputStream(String c) throws Exception {
-    ByteArrayOutputStream out = new ByteArrayOutputStream();
-    PacketLineOut pctOut = new PacketLineOut(out);
-    for (String arg : Splitter.on(' ').split(c)) {
-      pctOut.writeString("argument " + arg);
-    }
-    pctOut.end();
-    return new ByteArrayInputStream(out.toByteArray());
-  }
-}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index df3ec5e..15094fd 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -58,6 +58,8 @@
         return "blacktop/elasticsearch:7.5.2";
       case V7_6:
         return "blacktop/elasticsearch:7.6.2";
+      case V7_7:
+        return "blacktop/elasticsearch:7.7.0";
     }
     throw new IllegalStateException("No tests for version: " + version.name());
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
index 4a5345a..bbddaa2 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -36,7 +36,7 @@
   public static void startIndexService() {
     if (container == null) {
       // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_7);
     }
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
index 979a7f6..e12df9c 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -42,7 +42,7 @@
   public static void startIndexService() {
     if (container == null) {
       // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_7);
       client = HttpAsyncClients.createDefault();
       client.start();
     }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
index 6b979b3..5fec685 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -36,7 +36,7 @@
   public static void startIndexService() {
     if (container == null) {
       // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_7);
     }
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
index 4106d8b..3e8bab4 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -36,7 +36,7 @@
   public static void startIndexService() {
     if (container == null) {
       // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_7);
     }
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index 825d221..309c63a 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -54,6 +54,9 @@
 
     assertThat(ElasticVersion.forVersion("7.6.0")).isEqualTo(ElasticVersion.V7_6);
     assertThat(ElasticVersion.forVersion("7.6.1")).isEqualTo(ElasticVersion.V7_6);
+
+    assertThat(ElasticVersion.forVersion("7.7.0")).isEqualTo(ElasticVersion.V7_7);
+    assertThat(ElasticVersion.forVersion("7.7.1")).isEqualTo(ElasticVersion.V7_7);
   }
 
   @Test
@@ -76,6 +79,7 @@
     assertThat(ElasticVersion.V7_4.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
     assertThat(ElasticVersion.V7_5.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
     assertThat(ElasticVersion.V7_6.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
+    assertThat(ElasticVersion.V7_7.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
   }
 
   @Test
@@ -90,6 +94,7 @@
     assertThat(ElasticVersion.V7_4.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V7_5.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V7_6.isV6OrLater()).isTrue();
+    assertThat(ElasticVersion.V7_7.isV6OrLater()).isTrue();
   }
 
   @Test
@@ -104,5 +109,6 @@
     assertThat(ElasticVersion.V7_4.isV7OrLater()).isTrue();
     assertThat(ElasticVersion.V7_5.isV7OrLater()).isTrue();
     assertThat(ElasticVersion.V7_6.isV7OrLater()).isTrue();
+    assertThat(ElasticVersion.V7_7.isV7OrLater()).isTrue();
   }
 }
diff --git a/javatests/com/google/gerrit/integration/git/BUILD b/javatests/com/google/gerrit/integration/git/BUILD
new file mode 100644
index 0000000..4f1be09
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/git/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = ["UploadArchiveIT.java"],
+    group = "upload-archive",
+    labels = ["git-upload-archive"],
+)
diff --git a/javatests/com/google/gerrit/integration/git/UploadArchiveIT.java b/javatests/com/google/gerrit/integration/git/UploadArchiveIT.java
new file mode 100644
index 0000000..e7a1a68
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/git/UploadArchiveIT.java
@@ -0,0 +1,209 @@
+// Copyright (C) 2020 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.integration.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.TreeSet;
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.ArchiveInputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
+import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
+import org.junit.Test;
+
+@NoHttpd
+@UseSsh
+public class UploadArchiveIT extends StandaloneSiteTest {
+  private static final String[] SSH_KEYGEN_CMD =
+      new String[] {"ssh-keygen", "-t", "rsa", "-q", "-P", "", "-f"};
+  private static final String GIT_SSH_COMMAND =
+      "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o 'IdentitiesOnly yes' -i";
+  private static final String ARCHIVE = "archive";
+  @Inject private GerritApi gApi;
+  private String sshDestination;
+  private String identityPath;
+  private Project.NameKey project;
+  private CommitInfo commit;
+
+  @Test
+  @GerritConfig(name = "download.archive", value = "off")
+  public void archiveFeatureOff() throws Exception {
+    try (ServerContext ctx = startServer()) {
+      setUpTestHarness(ctx);
+      assertArchiveNotPermitted();
+    }
+  }
+
+  @Test
+  @GerritConfig(
+      name = "download.archive",
+      values = {"tar", "tbz2", "tgz", "txz"})
+  public void zipFormatDisabled() throws Exception {
+    try (ServerContext ctx = startServer()) {
+      setUpTestHarness(ctx);
+      assertArchiveNotPermitted();
+    }
+  }
+
+  @Test
+  public void verifyUploadArchiveFormats() throws Exception {
+    try (ServerContext ctx = startServer()) {
+      setUpTestHarness(ctx);
+      setUpChange();
+      for (String f : Arrays.asList("zip", "tar", "tar.gz", "tar.bz2", "tar.xz")) {
+        verifyUploadArchive(f);
+      }
+    }
+  }
+
+  private void verifyUploadArchive(String format) throws Exception {
+    Path outputPath = sitePaths.data_dir.resolve(ARCHIVE);
+    execute(
+        cmd(format, commit.commit),
+        sitePaths.data_dir.toFile(),
+        ImmutableMap.of("GIT_SSH_COMMAND", GIT_SSH_COMMAND + identityPath),
+        outputPath);
+    try (InputStream fi = Files.newInputStream(outputPath);
+        InputStream bi = new BufferedInputStream(fi);
+        ArchiveInputStream archive = archiveStreamForFormat(bi, format)) {
+      assertEntries(archive);
+    }
+  }
+
+  private ArchiveInputStream archiveStreamForFormat(InputStream bi, String format)
+      throws IOException {
+    switch (format) {
+      case "zip":
+        return new ZipArchiveInputStream(bi);
+      case "tar":
+        return new TarArchiveInputStream(bi);
+      case "tar.gz":
+        return new TarArchiveInputStream(new GzipCompressorInputStream(bi));
+      case "tar.bz2":
+        return new TarArchiveInputStream(new BZip2CompressorInputStream(bi));
+      case "tar.xz":
+        return new TarArchiveInputStream(new XZCompressorInputStream(bi));
+      default:
+        throw new IllegalArgumentException("Unknown archive format: " + format);
+    }
+  }
+
+  private void setUpTestHarness(ServerContext ctx) throws RestApiException, Exception {
+    ctx.getInjector().injectMembers(this);
+    project = new Project.NameKey("upload-archive-project-test");
+    gApi.projects().create(project.get());
+    setUpAuthentication();
+    sshDestination =
+        String.format(
+            "ssh://%s@%s:%s/%s",
+            "admin",
+            ctx.getServer().getSshdAddress().getHostName(),
+            ctx.getServer().getSshdAddress().getPort(),
+            project.get());
+    identityPath = sitePaths.data_dir.resolve(String.format("id_rsa_%s", "admin")).toString();
+  }
+
+  private void setUpAuthentication() throws Exception {
+    execute(
+        ImmutableList.<String>builder()
+            .add(SSH_KEYGEN_CMD)
+            .add(String.format("id_rsa_%s", "admin"))
+            .build());
+    gApi.accounts()
+        .id("admin")
+        .addSshKey(
+            new String(
+                java.nio.file.Files.readAllBytes(
+                    sitePaths.data_dir.resolve(String.format("id_rsa_%s.pub", "admin"))),
+                UTF_8));
+  }
+
+  private ImmutableList<String> cmd(String format, String commit) {
+    return ImmutableList.<String>builder()
+        .add("git")
+        .add("archive")
+        .add("-f=" + format)
+        .add("--prefix=" + commit + "/")
+        .add("--remote=" + sshDestination)
+        .add(commit)
+        .add(FILE_NAME)
+        .build();
+  }
+
+  private String execute(ImmutableList<String> cmd) throws Exception {
+    return execute(cmd, sitePaths.data_dir.toFile(), ImmutableMap.of());
+  }
+
+  private void assertArchiveNotPermitted() {
+    IOException exception =
+        assertThrows(
+            IOException.class,
+            () ->
+                execute(
+                    cmd("zip", "master"),
+                    sitePaths.data_dir.toFile(),
+                    ImmutableMap.of("GIT_SSH_COMMAND", GIT_SSH_COMMAND + identityPath)));
+    assertThat(exception)
+        .hasMessageThat()
+        .contains("fatal: upload-archive not permitted for format zip");
+  }
+
+  private void setUpChange() throws Exception {
+    ChangeInput in = new ChangeInput(project.get(), "master", "Test change");
+    in.newBranch = true;
+    String changeId = gApi.changes().create(in).info().changeId;
+    gApi.changes().id(changeId).edit().modifyFile(FILE_NAME, RawInputUtil.create(FILE_CONTENT));
+    gApi.changes().id(changeId).edit().publish();
+    commit = gApi.changes().id(changeId).current().commit(false);
+  }
+
+  private void assertEntries(ArchiveInputStream o) throws IOException {
+    Set<String> entryNames = new TreeSet<>();
+    ArchiveEntry e;
+    while ((e = o.getNextEntry()) != null) {
+      entryNames.add(e.getName());
+    }
+    assertThat(entryNames)
+        .containsExactly(
+            String.format("%s/", commit.commit), String.format("%s/%s", commit.commit, FILE_NAME))
+        .inOrder();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 5311b0f..8b606a0 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1737,6 +1737,11 @@
         new AccountGroup.UUID(gApi.groups().id(g1).get().id));
     assertQuery(q + " visibleto:" + g1, change1);
 
+    // Both changes are visible to InternalUser
+    try (ManualRequestContext ctx = oneOffRequestContext.open()) {
+      assertQuery(q, change2, change1);
+    }
+
     requestContext.setContext(newRequestContext(user2));
     assertQuery("is:visible", change1);
 
diff --git a/plugins/replication b/plugins/replication
index 5c36d25..937b403 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 5c36d25ee92cfcfe48c0aa382eb8181985662472
+Subproject commit 937b403a596d48d01f5fc1ce380b6d6558cb6051
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 7e542f9..d63ac4c 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -94,8 +94,8 @@
     # and httpasyncclient as necessary.
     maven_jar(
         name = "elasticsearch-rest-client",
-        artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.6.2",
-        sha1 = "3da6691dcd1864243f11f07d51907320452c6400",
+        artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.7.0",
+        sha1 = "5fc25eec3940bc0e9b0ffddcf50554a609e9db8e",
     )
 
     maven_jar(