Merge "Fix handling of ported comments (broken after big comment refactor)"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 3109ec7..a56055a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -940,12 +940,6 @@
+
If direct updates are made to `All-Users`, this cache should be flushed.
-cache `"approvals"`::
-+
-Cache entries contain approvals for a given patch set. This includes
-approvals granted on this patch set as well as approvals copied from
-earlier patch sets.
-
cache `"adv_bases"`::
+
Used only for push over smart HTTP when branch level access controls
@@ -4902,16 +4896,6 @@
replicate = replication start
----
-[[ssh]]
-=== Section ssh
-
-[[ssh.clientImplementation]]ssh.clientImplementation::
-+
-JCraft JSch client is supported in addition to Apache MINA SSH client.
-To use JSch client set the value to `JSCH`.
-+
-By default, `APACHE`.
-
[[sshd]]
=== Section sshd
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index c3237ed..4069446 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -317,18 +317,6 @@
bazel test //javatests/com/google/gerrit/acceptance/rest/account:rest_account
----
-To run SSH tests using JSch ssh client:
-
-----
- bazel test --test_env=SSH_CLIENT_IMPLEMENTATION=JSCH //...
-----
-
-To run SSH tests using Apache MINA ssh client:
-
-----
- bazel test --test_env=SSH_CLIENT_IMPLEMENTATION=APACHE //...
-----
-
To run only tests that do not use SSH:
----
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
index c50a293..802b484 100644
--- a/Documentation/dev-e2e-tests.txt
+++ b/Documentation/dev-e2e-tests.txt
@@ -102,8 +102,7 @@
=== SSH keys
If you are running SSH commands, the private keys of the users used for testing need to go in
-`/tmp/ssh-keys`. The keys need to be generated this way (JSch won't validate them
-link:https://stackoverflow.com/questions/53134212/invalid-privatekey-when-using-jsch[otherwise,role=external,window=_blank]):
+`/tmp/ssh-keys`. The keys need to be generated this way and won't be validated.
----
mkdir /tmp/ssh-keys
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 86ff904..1302329 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -32,7 +32,7 @@
Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
uploads of changes directly from `git push` command line clients.
-Gerrit includes an SSH client (JSch), to support authenticated
+Gerrit includes an SSH client (Apache SSHD), to support authenticated
replication of changes to remote systems, such as for automatic
updates of mirror servers, or realtime backups.
@@ -2394,43 +2394,6 @@
----
-[[jsch]]
-jsch
-
-* jsch
-
-[[jsch_license]]
-----
-Copyright (c) 2002-2012 Atsuhiko Yamanaka, JCraft,Inc.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
- 1. Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
-
- 2. Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in
- the documentation and/or other materials provided with the distribution.
-
- 3. The names of the authors may not be used to endorse or promote products
- derived from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
-INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
-INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
-OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
-EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-----
-
-
[[jsoup]]
jsoup
diff --git a/contrib/convertkey/src/main/java/com/googlesource/gerrit/convertkey/ConvertKey.java b/contrib/convertkey/src/main/java/com/googlesource/gerrit/convertkey/ConvertKey.java
deleted file mode 100644
index 08a529c..0000000
--- a/contrib/convertkey/src/main/java/com/googlesource/gerrit/convertkey/ConvertKey.java
+++ /dev/null
@@ -1,69 +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.googlesource.gerrit.convertkey;
-
-import com.jcraft.jsch.HostKey;
-import com.jcraft.jsch.JSchException;
-import java.io.File;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import org.apache.sshd.common.util.Buffer;
-import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
-import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
-
-public class ConvertKey {
- public static void main(String[] args)
- throws GeneralSecurityException, JSchException, IOException {
- SimpleGeneratorHostKeyProvider p;
-
- if (args.length != 1) {
- System.err.println("Error: requires path to the SSH host key");
- return;
- } else {
- File file = new File(args[0]);
- if (!file.exists() || !file.isFile() || !file.canRead()) {
- System.err.println("Error: ssh key should exist and be readable");
- return;
- }
- }
-
- p = new SimpleGeneratorHostKeyProvider();
- // Gerrit's SSH "simple" keys are always RSA.
- p.setPath(args[0]);
- p.setAlgorithm("RSA");
- Iterable<KeyPair> keys = p.loadKeys(); // forces the key to generate.
- for (KeyPair k : keys) {
- System.out.println("Public Key (" + k.getPublic().getAlgorithm() + "):");
- // From Gerrit's SshDaemon class; use JSch to get the public
- // key/type
- final Buffer buf = new Buffer();
- buf.putRawPublicKey(k.getPublic());
- final byte[] keyBin = buf.getCompactData();
- HostKey pub = new HostKey("localhost", keyBin);
- System.out.println(pub.getType() + " " + pub.getKey());
- System.out.println("Private Key:");
- // Use Bouncy Castle to write the private key back in PEM format
- // (PKCS#1)
- // http://stackoverflow.com/questions/25129822/export-rsa-public-key-to-pem-string-using-java
- StringWriter privout = new StringWriter();
- JcaPEMWriter privWriter = new JcaPEMWriter(privout);
- privWriter.writeObject(k.getPrivate());
- privWriter.close();
- System.out.println(privout);
- }
- }
-}
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 2fb2127..4faa9ab 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -168,7 +168,6 @@
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.security.KeyPair;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
@@ -586,8 +585,7 @@
&& SshMode.useSsh()
&& (adminSshSession == null || userSshSession == null)) {
// Create Ssh sessions
- KeyPair adminKeyPair = sshKeys.getKeyPair(admin);
- SshSessionFactory.initSsh(adminKeyPair);
+ SshSessionFactory.initSsh();
Context ctx = newRequestContext(user);
atrScope.set(ctx);
userSshSession = ctx.getSession();
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index fe6e160..cb5f667 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -39,9 +39,7 @@
"//lib:gson",
"//lib:guava-retrying",
"//lib:jgit",
- "//lib:jgit-ssh-jsch",
"//lib:jgit-ssh-apache",
- "//lib:jsch",
"//lib/commons:compress",
"//lib/commons:lang",
"//lib/flogger:api",
diff --git a/java/com/google/gerrit/acceptance/SshSessionJsch.java b/java/com/google/gerrit/acceptance/SshSessionJsch.java
deleted file mode 100644
index a86c2d6..0000000
--- a/java/com/google/gerrit/acceptance/SshSessionJsch.java
+++ /dev/null
@@ -1,174 +0,0 @@
-// Copyright (C) 2021 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;
-
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.gerrit.acceptance.testsuite.account.TestAccount;
-import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
-import com.jcraft.jsch.ChannelExec;
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.JSchException;
-import com.jcraft.jsch.Session;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.StringWriter;
-import java.net.InetSocketAddress;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Properties;
-import java.util.Scanner;
-import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
-import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
-import org.bouncycastle.util.io.pem.PemObject;
-import org.eclipse.jgit.transport.JschConfigSessionFactory;
-import org.eclipse.jgit.transport.OpenSshConfig.Host;
-import org.eclipse.jgit.transport.SshSessionFactory;
-import org.eclipse.jgit.util.FS;
-
-public class SshSessionJsch extends SshSession {
-
- private Session session;
-
- public static void initClient(KeyPair keyPair) {
- Properties config = new Properties();
- config.put("StrictHostKeyChecking", "no");
- JSch.setConfig(config);
-
- // register a JschConfigSessionFactory that adds the private key as identity
- // to the JSch instance of JGit so that SSH communication via JGit can
- // succeed
- SshSessionFactory.setInstance(
- new JschConfigSessionFactory() {
- @Override
- protected void configure(Host hc, Session session) {
- try {
- JSch jsch = getJSch(hc, FS.DETECTED);
- jsch.addIdentity(
- "KeyPair", privateKey(keyPair), TestSshKeys.publicKeyBlob(keyPair), null);
- } catch (JSchException | GeneralSecurityException | IOException e) {
- throw new RuntimeException(e);
- }
- }
- });
- }
-
- public static KeyPairGenerator initKeyPairGenerator() throws NoSuchAlgorithmException {
- KeyPairGenerator gen;
- gen = KeyPairGenerator.getInstance("RSA");
- gen.initialize(512, new SecureRandom());
- return gen;
- }
-
- public SshSessionJsch(TestSshKeys sshKeys, InetSocketAddress addr, TestAccount account) {
- super(sshKeys, addr, account);
- }
-
- @Override
- public void open() throws Exception {
- getJschSession();
- }
-
- @Override
- public void close() {
- if (session != null) {
- session.disconnect();
- session = null;
- }
- }
-
- @SuppressWarnings("resource")
- @Override
- public String exec(String command) throws Exception {
- ChannelExec channel = (ChannelExec) getJschSession().openChannel("exec");
- try {
- channel.setCommand(command);
- InputStream in = channel.getInputStream();
- InputStream err = channel.getErrStream();
- channel.connect();
-
- Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
- error = s.hasNext() ? s.next() : null;
-
- s = new Scanner(in, UTF_8.name()).useDelimiter("\\A");
- return s.hasNext() ? s.next() : "";
- } finally {
- channel.disconnect();
- }
- }
-
- @SuppressWarnings("resource")
- @Override
- public int execAndReturnStatus(String command) throws Exception {
- ChannelExec channel = (ChannelExec) getJschSession().openChannel("exec");
- try {
- channel.setCommand(command);
- InputStream err = channel.getErrStream();
- channel.connect();
-
- Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
- error = s.hasNext() ? s.next() : null;
- return channel.getExitStatus();
- } finally {
- channel.disconnect();
- }
- }
-
- @Override
- public Reader execAndReturnReader(String command) throws Exception {
- ChannelExec channel = (ChannelExec) getJschSession().openChannel("exec");
- channel.setCommand(command);
- channel.connect();
-
- return new InputStreamReader(channel.getInputStream(), StandardCharsets.UTF_8) {
- @Override
- public void close() throws IOException {
- super.close();
- channel.disconnect();
- }
- };
- }
-
- private Session getJschSession() throws Exception {
- if (session == null) {
- KeyPair keyPair = sshKeys.getKeyPair(account);
- JSch jsch = new JSch();
- jsch.addIdentity("KeyPair", privateKey(keyPair), TestSshKeys.publicKeyBlob(keyPair), null);
- String username = getUsername();
- session = jsch.getSession(username, addr.getAddress().getHostAddress(), addr.getPort());
- session.setConfig("StrictHostKeyChecking", "no");
- session.connect();
- }
- return session;
- }
-
- private static byte[] privateKey(KeyPair keyPair) throws IOException {
- // unencrypted form of PKCS#8 file
- JcaPKCS8Generator gen1 = new JcaPKCS8Generator(keyPair.getPrivate(), null);
- PemObject obj1 = gen1.generate();
- StringWriter sw1 = new StringWriter();
- try (JcaPEMWriter pw = new JcaPEMWriter(sw1)) {
- pw.writeObject(obj1);
- }
- return sw1.toString().getBytes(US_ASCII.name());
- }
-}
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/SshSessionFactory.java b/java/com/google/gerrit/acceptance/testsuite/request/SshSessionFactory.java
index d5dd28a..3442b6e 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/SshSessionFactory.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/SshSessionFactory.java
@@ -14,10 +14,7 @@
package com.google.gerrit.acceptance.testsuite.request;
-import static com.google.gerrit.server.config.SshClientImplementation.getFromEnvironment;
-
import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.acceptance.SshSessionJsch;
import com.google.gerrit.acceptance.SshSessionMina;
import com.google.gerrit.acceptance.testsuite.account.TestAccount;
import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
@@ -28,25 +25,16 @@
public class SshSessionFactory {
public static SshSession createSession(
TestSshKeys testSshKeys, InetSocketAddress sshAddress, TestAccount testAccount) {
- return getFromEnvironment().isMina()
- ? new SshSessionMina(testSshKeys, sshAddress, testAccount)
- : new SshSessionJsch(testSshKeys, sshAddress, testAccount);
+ return new SshSessionMina(testSshKeys, sshAddress, testAccount);
}
- public static void initSsh(KeyPair keyPair) {
- if (getFromEnvironment().isMina()) {
- SshSessionMina.initClient();
- } else {
- SshSessionJsch.initClient(keyPair);
- }
+ public static void initSsh() {
+ SshSessionMina.initClient();
}
private SshSessionFactory() {}
public static KeyPair genSshKey() throws GeneralSecurityException {
- return (getFromEnvironment().isMina()
- ? SshSessionMina.initKeyPairGenerator()
- : SshSessionJsch.initKeyPairGenerator())
- .generateKeyPair();
+ return SshSessionMina.initKeyPairGenerator().generateKeyPair();
}
}
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 13df520..fbdbd90 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -343,7 +343,7 @@
});
modules.add(new DefaultUrlFormatterModule());
- SshSessionFactoryInitializer.init(config);
+ SshSessionFactoryInitializer.init();
modules.add(SshKeyCacheImpl.module());
modules.add(
new AbstractModule() {
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index f3691ed..402d839 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -491,7 +491,7 @@
});
}
modules.add(new DefaultUrlFormatterModule());
- SshSessionFactoryInitializer.init(config);
+ SshSessionFactoryInitializer.init();
if (sshd) {
modules.add(SshKeyCacheImpl.module());
} else {
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 468623b..ce433d7 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -41,7 +41,6 @@
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.account.ServiceUserClassifierImpl;
import com.google.gerrit.server.account.externalids.ExternalIdCacheModule;
-import com.google.gerrit.server.approval.ApprovalCacheImpl;
import com.google.gerrit.server.cache.CacheRemovalListener;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -177,7 +176,6 @@
modules.add(new GroupModule());
modules.add(new NoteDbModule());
modules.add(AccountCacheImpl.module());
- modules.add(ApprovalCacheImpl.module());
modules.add(ConflictsCacheImpl.module());
modules.add(DefaultPreferencesCacheImpl.module());
modules.add(GroupCacheImpl.module());
diff --git a/java/com/google/gerrit/server/approval/ApprovalCache.java b/java/com/google/gerrit/server/approval/ApprovalCache.java
deleted file mode 100644
index 5637249..0000000
--- a/java/com/google/gerrit/server/approval/ApprovalCache.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2021 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.approval;
-
-import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.entities.PatchSetApproval;
-import com.google.gerrit.server.notedb.ChangeNotes;
-
-/**
- * Cache that holds approvals per patch set and NoteDb state. This includes approvals copied forward
- * from older patch sets.
- */
-public interface ApprovalCache {
- /** Returns {@link PatchSetApproval}s for the given patch set. */
- Iterable<PatchSetApproval> get(ChangeNotes notes, PatchSet.Id psId);
-}
diff --git a/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java b/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java
deleted file mode 100644
index fd31da9..0000000
--- a/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright (C) 2021 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.approval;
-
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.entities.PatchSetApproval;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.proto.Entities;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.proto.Cache;
-import com.google.gerrit.server.cache.serialize.ObjectIdCacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.Singleton;
-import com.google.inject.name.Named;
-import com.google.protobuf.ByteString;
-import java.util.concurrent.ExecutionException;
-
-/** Implementation of the {@link ApprovalCache} interface */
-public class ApprovalCacheImpl implements ApprovalCache {
- private static final String CACHE_NAME = "approvals";
-
- public static Module module() {
- return new CacheModule() {
- @Override
- protected void configure() {
- bind(ApprovalCache.class).to(ApprovalCacheImpl.class);
- persist(
- CACHE_NAME,
- Cache.PatchSetApprovalsKeyProto.class,
- Cache.AllPatchSetApprovalsProto.class)
- .version(2)
- .loader(Loader.class)
- .keySerializer(new ProtobufSerializer<>(Cache.PatchSetApprovalsKeyProto.parser()))
- .valueSerializer(new ProtobufSerializer<>(Cache.AllPatchSetApprovalsProto.parser()));
- }
- };
- }
-
- private final LoadingCache<Cache.PatchSetApprovalsKeyProto, Cache.AllPatchSetApprovalsProto>
- cache;
-
- @Inject
- ApprovalCacheImpl(
- @Named(CACHE_NAME)
- LoadingCache<Cache.PatchSetApprovalsKeyProto, Cache.AllPatchSetApprovalsProto> cache) {
- this.cache = cache;
- }
-
- @Override
- public Iterable<PatchSetApproval> get(ChangeNotes notes, PatchSet.Id psId) {
- try {
- return fromProto(
- cache.get(
- Cache.PatchSetApprovalsKeyProto.newBuilder()
- .setChangeId(notes.getChangeId().get())
- .setPatchSetId(psId.get())
- .setProject(notes.getProjectName().get())
- .setId(
- ByteString.copyFrom(
- ObjectIdCacheSerializer.INSTANCE.serialize(notes.getMetaId())))
- .build()));
- } catch (ExecutionException e) {
- throw new StorageException(e);
- }
- }
-
- @Singleton
- static class Loader
- extends CacheLoader<Cache.PatchSetApprovalsKeyProto, Cache.AllPatchSetApprovalsProto> {
- private final ApprovalInference approvalInference;
- private final ChangeNotes.Factory changeNotesFactory;
-
- @Inject
- Loader(ApprovalInference approvalInference, ChangeNotes.Factory changeNotesFactory) {
- this.approvalInference = approvalInference;
- this.changeNotesFactory = changeNotesFactory;
- }
-
- @Override
- public Cache.AllPatchSetApprovalsProto load(Cache.PatchSetApprovalsKeyProto key)
- throws Exception {
- Change.Id changeId = Change.id(key.getChangeId());
- return toProto(
- approvalInference.forPatchSet(
- changeNotesFactory.createChecked(
- Project.nameKey(key.getProject()),
- changeId,
- ObjectIdCacheSerializer.INSTANCE.deserialize(key.getId().toByteArray())),
- PatchSet.id(changeId, key.getPatchSetId()),
- null
- /* revWalk= */ ,
- null
- /* repoConfig= */ ));
- }
- }
-
- private static Iterable<PatchSetApproval> fromProto(Cache.AllPatchSetApprovalsProto proto) {
- ImmutableList.Builder<PatchSetApproval> builder = ImmutableList.builder();
- for (Entities.PatchSetApproval psa : proto.getApprovalList()) {
- builder.add(PatchSetApprovalProtoConverter.INSTANCE.fromProto(psa));
- }
- return builder.build();
- }
-
- private static Cache.AllPatchSetApprovalsProto toProto(Iterable<PatchSetApproval> autoValue) {
- Cache.AllPatchSetApprovalsProto.Builder builder = Cache.AllPatchSetApprovalsProto.newBuilder();
- for (PatchSetApproval psa : autoValue) {
- builder.addApproval(PatchSetApprovalProtoConverter.INSTANCE.toProto(psa));
- }
- return builder.build();
- }
-}
diff --git a/java/com/google/gerrit/server/approval/ApprovalInference.java b/java/com/google/gerrit/server/approval/ApprovalInference.java
index 5517ab1..fba2fcb 100644
--- a/java/com/google/gerrit/server/approval/ApprovalInference.java
+++ b/java/com/google/gerrit/server/approval/ApprovalInference.java
@@ -40,7 +40,7 @@
import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.DiffOperations;
import com.google.gerrit.server.patch.DiffOptions;
-import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.approval.ApprovalContext;
@@ -51,7 +51,6 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Collection;
-import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.lib.Config;
@@ -98,20 +97,11 @@
}
/**
- * Returns all approvals that apply to the given patch set. Honors direct and indirect (approval
- * on parents) approvals.
+ * Returns all approvals that apply to the given patch set. Honors copied approvals from previous
+ * patch-set.
*/
Iterable<PatchSetApproval> forPatchSet(
- ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
- PatchSet patchset = notes.getPatchSets().get(psId);
- if (patchset == null) {
- return Collections.emptyList();
- }
- return forPatchSet(notes, patchset, rw, repoConfig);
- }
-
- Iterable<PatchSetApproval> forPatchSet(
- ChangeNotes notes, PatchSet ps, @Nullable RevWalk rw, @Nullable Config repoConfig) {
+ ChangeNotes notes, PatchSet ps, RevWalk rw, Config repoConfig) {
ProjectState project;
try (TraceTimer traceTimer =
TraceContext.newTimer(
@@ -136,9 +126,9 @@
PatchSet.Id psId,
ChangeKind kind,
LabelType type,
- @Nullable Map<String, FileDiffOutput> baseVsCurrentDiff,
- @Nullable Map<String, FileDiffOutput> baseVsPriorDiff,
- @Nullable Map<String, FileDiffOutput> priorVsCurrentDiff) {
+ @Nullable Map<String, ModifiedFile> baseVsCurrentDiff,
+ @Nullable Map<String, ModifiedFile> baseVsPriorDiff,
+ @Nullable Map<String, ModifiedFile> priorVsCurrentDiff) {
int n = psa.key().patchSetId().get();
checkArgument(n != psId.get());
@@ -326,11 +316,14 @@
PatchSetApproval psa,
PatchSet patchSet,
LabelType type,
- ChangeKind changeKind) {
+ ChangeKind changeKind,
+ RevWalk revWalk,
+ Config repoConfig) {
if (!type.getCopyCondition().isPresent()) {
return false;
}
- ApprovalContext ctx = ApprovalContext.create(changeNotes, psa, patchSet, changeKind);
+ ApprovalContext ctx =
+ ApprovalContext.create(changeNotes, psa, patchSet, changeKind, revWalk, repoConfig);
try {
// Use a request context to run checks as an internal user with expanded visibility. This is
// so that the output of the copy condition does not depend on who is running the current
@@ -347,11 +340,7 @@
}
private Collection<PatchSetApproval> getForPatchSetWithoutNormalization(
- ChangeNotes notes,
- ProjectState project,
- PatchSet patchSet,
- @Nullable RevWalk rw,
- @Nullable Config repoConfig) {
+ ChangeNotes notes, ProjectState project, PatchSet patchSet, RevWalk rw, Config repoConfig) {
checkState(
project.getNameKey().equals(notes.getProjectName()),
"project must match %s, %s",
@@ -361,34 +350,23 @@
PatchSet.Id psId = patchSet.id();
// Add approvals on the given patch set to the result
Table<String, Account.Id, PatchSetApproval> resultByUser = HashBasedTable.create();
- ImmutableList<PatchSetApproval> approvalsForGivenPatchSet =
+ ImmutableList<PatchSetApproval> nonCopiedApprovalsForGivenPatchSet =
notes.load().getApprovals().get(patchSet.id());
- approvalsForGivenPatchSet.forEach(psa -> resultByUser.put(psa.label(), psa.accountId(), psa));
+ nonCopiedApprovalsForGivenPatchSet.forEach(
+ psa -> resultByUser.put(psa.label(), psa.accountId(), psa));
// Bail out immediately if this is the first patch set. Return only approvals granted on the
// given patch set.
if (psId.get() == 1) {
return resultByUser.values();
}
-
- // Call this algorithm recursively to check if the prior patch set had approvals. This has the
- // advantage that all caches - most importantly ChangeKindCache - have values cached for what we
- // need for this computation.
- // The way this algorithm is written is that any approval will be copied forward by one patch
- // set at a time if configs and change kind allow so. Once an approval is held back - for
- // example because the patch set is a REWORK - it will not be picked up again in a future
- // patch set.
Map.Entry<PatchSet.Id, PatchSet> priorPatchSet = notes.load().getPatchSets().lowerEntry(psId);
if (priorPatchSet == null) {
return resultByUser.values();
}
- Iterable<PatchSetApproval> priorApprovals =
- getForPatchSetWithoutNormalization(
- notes, project, priorPatchSet.getValue(), rw, repoConfig);
- if (!priorApprovals.iterator().hasNext()) {
- return resultByUser.values();
- }
+ ImmutableList<PatchSetApproval> priorApprovalsIncludingCopied =
+ notes.load().getApprovalsWithCopied().get(priorPatchSet.getKey());
// Add labels from the previous patch set to the result in case the label isn't already there
// and settings as well as change kind allow copying.
@@ -406,11 +384,11 @@
priorPatchSet.getValue().id().changeId(),
changeKind);
- Map<String, FileDiffOutput> baseVsCurrent = null;
- Map<String, FileDiffOutput> baseVsPrior = null;
- Map<String, FileDiffOutput> priorVsCurrent = null;
+ Map<String, ModifiedFile> baseVsCurrent = null;
+ Map<String, ModifiedFile> baseVsPrior = null;
+ Map<String, ModifiedFile> priorVsCurrent = null;
LabelTypes labelTypes = project.getLabelTypes();
- for (PatchSetApproval psa : priorApprovals) {
+ for (PatchSetApproval psa : priorApprovalsIncludingCopied) {
if (resultByUser.contains(psa.label(), psa.accountId())) {
continue;
}
@@ -419,10 +397,11 @@
if (baseVsCurrent == null
&& type.isPresent()
&& type.get().isCopyAllScoresIfListOfFilesDidNotChange()) {
- baseVsCurrent = listModifiedFiles(project, patchSet);
- baseVsPrior = listModifiedFiles(project, priorPatchSet.getValue());
+ baseVsCurrent = listModifiedFiles(project, patchSet, rw, repoConfig);
+ baseVsPrior = listModifiedFiles(project, priorPatchSet.getValue(), rw, repoConfig);
priorVsCurrent =
- listModifiedFiles(project, priorPatchSet.getValue().commitId(), patchSet.commitId());
+ listModifiedFiles(
+ project, priorPatchSet.getValue().commitId(), patchSet.commitId(), rw, repoConfig);
}
if (!type.isPresent()) {
logger.atFine().log(
@@ -445,7 +424,8 @@
baseVsCurrent,
baseVsPrior,
priorVsCurrent)
- && !canCopyBasedOnCopyCondition(notes, psa, patchSet, type.get(), changeKind)) {
+ && !canCopyBasedOnCopyCondition(
+ notes, psa, patchSet, type.get(), changeKind, rw, repoConfig)) {
continue;
}
resultByUser.put(psa.label(), psa.accountId(), psa.copyWithPatchSet(patchSet.id()));
@@ -457,14 +437,20 @@
* Gets the modified files between the two latest patch-sets. Can be used to compute difference in
* files between those two patch-sets .
*/
- private Map<String, FileDiffOutput> listModifiedFiles(ProjectState project, PatchSet ps) {
+ private Map<String, ModifiedFile> listModifiedFiles(
+ ProjectState project, PatchSet ps, RevWalk revWalk, Config repoConfig) {
try {
Integer parentNum =
listOfFilesUnchangedPredicate.isInitialCommit(project.getNameKey(), ps.commitId())
? 0
: 1;
- return diffOperations.listModifiedFilesAgainstParent(
- project.getNameKey(), ps.commitId(), parentNum, DiffOptions.DEFAULTS);
+ return diffOperations.loadModifiedFilesAgainstParent(
+ project.getNameKey(),
+ ps.commitId(),
+ parentNum,
+ DiffOptions.DEFAULTS,
+ revWalk,
+ repoConfig);
} catch (DiffNotAvailableException ex) {
throw new StorageException(
"failed to compute difference in files, so won't copy"
@@ -478,11 +464,20 @@
* Gets the modified files between two commits corresponding to different patchsets of the same
* change.
*/
- private Map<String, FileDiffOutput> listModifiedFiles(
- ProjectState project, ObjectId sourceCommit, ObjectId targetCommit) {
+ private Map<String, ModifiedFile> listModifiedFiles(
+ ProjectState project,
+ ObjectId sourceCommit,
+ ObjectId targetCommit,
+ RevWalk revWalk,
+ Config repoConfig) {
try {
- return diffOperations.listModifiedFiles(
- project.getNameKey(), sourceCommit, targetCommit, DiffOptions.DEFAULTS);
+ return diffOperations.loadModifiedFiles(
+ project.getNameKey(),
+ sourceCommit,
+ targetCommit,
+ DiffOptions.DEFAULTS,
+ revWalk,
+ repoConfig);
} catch (DiffNotAvailableException ex) {
throw new StorageException(
"failed to compute difference in files, so won't copy"
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index c2e35d2..c3921f8 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -26,7 +26,6 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
@@ -42,6 +41,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
+import com.google.gerrit.server.change.LabelNormalizer;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -98,7 +98,7 @@
private final ApprovalInference approvalInference;
private final PermissionBackend permissionBackend;
private final ProjectCache projectCache;
- private final ApprovalCache approvalCache;
+ private final LabelNormalizer labelNormalizer;
@VisibleForTesting
@Inject
@@ -106,11 +106,11 @@
ApprovalInference approvalInference,
PermissionBackend permissionBackend,
ProjectCache projectCache,
- ApprovalCache approvalCache) {
+ LabelNormalizer labelNormalizer) {
this.approvalInference = approvalInference;
this.permissionBackend = permissionBackend;
this.projectCache = projectCache;
- this.approvalCache = approvalCache;
+ this.labelNormalizer = labelNormalizer;
}
/**
@@ -339,30 +339,42 @@
}
}
- public ListMultimap<PatchSet.Id, PatchSetApproval> byChange(ChangeNotes notes) {
+ public ListMultimap<PatchSet.Id, PatchSetApproval> byChangeExcludingCopiedApprovals(
+ ChangeNotes notes) {
return notes.load().getApprovals();
}
- public Iterable<PatchSetApproval> byPatchSet(
- ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
- return approvalInference.forPatchSet(notes, psId, rw, repoConfig);
- }
-
- public Iterable<PatchSetApproval> byPatchSet(ChangeNotes notes, PatchSet patchSet) {
- return approvalInference.forPatchSet(notes, patchSet, /* rw= */ null, /* repoConfig= */ null);
- }
-
- public Iterable<PatchSetApproval> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
- return approvalCache.get(notes, psId);
- }
-
- public Iterable<PatchSetApproval> byPatchSetUser(
+ /**
+ * This method should only be used when we want to dynamically compute the approvals. Generally,
+ * the copied approvals are available in {@link ChangeNotes}. However, if the patch-set is just
+ * being created, we need to dynamically compute the approvals so that we can persist them in
+ * storage. The {@link RevWalk} and {@link Config} objects that are being used to create the new
+ * patch-set are required for this method. Here we also add those votes to the provided {@link
+ * ChangeUpdate} object.
+ */
+ public void persistCopiedApprovals(
ChangeNotes notes,
- PatchSet.Id psId,
- Account.Id accountId,
- @Nullable RevWalk rw,
- @Nullable Config repoConfig) {
- return filterApprovals(byPatchSet(notes, psId, rw, repoConfig), accountId);
+ PatchSet patchSet,
+ RevWalk revWalk,
+ Config repoConfig,
+ ChangeUpdate changeUpdate) {
+ approvalInference
+ .forPatchSet(notes, patchSet, revWalk, repoConfig)
+ .forEach(a -> changeUpdate.putCopiedApproval(a));
+ }
+
+ /**
+ * Gets {@link PatchSetApproval}s for a specified patch-set. The result includes copied votes but
+ * does not include deleted labels.
+ *
+ * @param notes changenotes of the change.
+ * @param psId patch-set id for the change and patch-set we want to get approvals.
+ * @return all approvals for the specified patch-set, including copied votes, not including
+ * deleted labels.
+ */
+ public Iterable<PatchSetApproval> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
+ List<PatchSetApproval> approvalsNotNormalized = notes.load().getApprovalsWithCopied().get(psId);
+ return labelNormalizer.normalize(notes, approvalsNotNormalized).getNormalized();
}
public Iterable<PatchSetApproval> byPatchSetUser(
@@ -375,8 +387,8 @@
return null;
}
try {
- // Submit approval is never copied, so bypass expensive byPatchSet call.
- return getSubmitter(c, byChange(notes).get(c));
+ // Submit approval is never copied.
+ return getSubmitter(c, byChangeExcludingCopiedApprovals(notes).get(c));
} catch (StorageException e) {
return null;
}
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 1e40429..415383a 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -223,7 +223,7 @@
private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId) {
Iterable<PatchSetApproval> approvals;
- approvals = approvalsUtil.byChange(ctx.getNotes()).values();
+ approvals = ctx.getNotes().getApprovalsWithCopied().values();
return Iterables.filter(approvals, psa -> accountId.equals(psa.accountId()));
}
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 209901d..cfdb576 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -104,7 +104,6 @@
private boolean allowClosed;
private boolean sendEmail = true;
private String topic;
- private boolean storeCopiedVotes = true;
// Fields set during some phase of BatchUpdate.Op.
private Change change;
@@ -204,17 +203,6 @@
return this;
}
- /**
- * We always want to store copied votes except when the change is getting submitted and a new
- * patch-set is created on submit (using submit strategies such as "REBASE_ALWAYS"). In such
- * cases, we already store the votes of the new patch-sets in SubmitStrategyOp#saveApprovals. We
- * should not also store the copied votes.
- */
- public PatchSetInserter setStoreCopiedVotes(boolean storeCopiedVotes) {
- this.storeCopiedVotes = storeCopiedVotes;
- return this;
- }
-
public Change getChange() {
checkState(change != null, "getChange() only valid after executing update");
return change;
@@ -298,11 +286,8 @@
}
}
- // Approvals that are being set in the new patch-set during this operation are not available yet
- // outside of the scope of this method. Only copied approvals are set here.
- if (storeCopiedVotes) {
- approvalsUtil.byPatchSet(ctx.getNotes(), patchSet).forEach(a -> update.putCopiedApproval(a));
- }
+ approvalsUtil.persistCopiedApprovals(
+ ctx.getNotes(), patchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig(), update);
return true;
}
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 3e67cca..4952464 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -92,7 +92,6 @@
private boolean detailedCommitMessage;
private boolean postMessage = true;
private boolean sendEmail = true;
- private boolean storeCopiedVotes = true;
private boolean matchAuthorToCommitterDate = false;
private CodeReviewCommit rebasedCommit;
@@ -170,17 +169,6 @@
return this;
}
- /**
- * We always want to store copied votes except when the change is getting submitted and a new
- * patch-set is created on submit (using submit strategies such as "REBASE_ALWAYS"). In such
- * cases, we already store the votes of the new patch-sets in SubmitStrategyOp#saveApprovals. We
- * should not also store the copied votes.
- */
- public RebaseChangeOp setStoreCopiedVotes(boolean storeCopiedVotes) {
- this.storeCopiedVotes = storeCopiedVotes;
- return this;
- }
-
public RebaseChangeOp setSendEmail(boolean sendEmail) {
this.sendEmail = sendEmail;
return this;
@@ -231,10 +219,7 @@
.setFireRevisionCreated(fireRevisionCreated)
.setCheckAddPatchSetPermission(checkAddPatchSetPermission)
.setValidate(validate)
- .setSendEmail(sendEmail)
- // The votes are automatically copied and they don't count as copied votes. See
- // method's javadoc.
- .setStoreCopiedVotes(storeCopiedVotes);
+ .setSendEmail(sendEmail);
if (!rebasedCommit.getFilesWithGitConflicts().isEmpty()
&& !notes.getChange().isWorkInProgress()) {
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 02435d9..7d3ff12 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -108,7 +108,6 @@
import com.google.gerrit.server.account.externalids.ExternalIdCacheModule;
import com.google.gerrit.server.account.externalids.ExternalIdModule;
import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
-import com.google.gerrit.server.approval.ApprovalCacheImpl;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.auth.AuthBackend;
import com.google.gerrit.server.auth.UniversalAuthBackend;
@@ -250,7 +249,6 @@
bind(RulesCache.class);
bind(BlameCache.class).to(BlameCacheImpl.class);
install(AccountCacheImpl.module());
- install(ApprovalCacheImpl.module());
install(BatchUpdate.module());
install(ChangeKindCacheImpl.module());
install(ChangeFinder.module());
diff --git a/java/com/google/gerrit/server/config/SshClientImplementation.java b/java/com/google/gerrit/server/config/SshClientImplementation.java
deleted file mode 100644
index 5811e4d..0000000
--- a/java/com/google/gerrit/server/config/SshClientImplementation.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// 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.server.config;
-
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Enums;
-import com.google.common.base.Strings;
-
-/* SSH implementation to use by JGit SSH client transport protocol. */
-public enum SshClientImplementation {
- /** JCraft JSch implementation. */
- JSCH,
-
- /** Apache MINA implementation. */
- APACHE;
-
- private static final String ENV_VAR = "SSH_CLIENT_IMPLEMENTATION";
- private static final String SYS_PROP = "gerrit.sshClientImplementation";
-
- @VisibleForTesting
- public static SshClientImplementation getFromEnvironment() {
- String value = System.getenv(ENV_VAR);
- if (Strings.isNullOrEmpty(value)) {
- value = System.getProperty(SYS_PROP);
- }
- if (Strings.isNullOrEmpty(value)) {
- return APACHE;
- }
- SshClientImplementation client =
- Enums.getIfPresent(SshClientImplementation.class, value).orNull();
- if (!Strings.isNullOrEmpty(System.getenv(ENV_VAR))) {
- checkArgument(
- client != null, "Invalid value for env variable %s: %s", ENV_VAR, System.getenv(ENV_VAR));
- } else {
- checkArgument(
- client != null,
- "Invalid value for system property %s: %s",
- SYS_PROP,
- System.getProperty(SYS_PROP));
- }
- return client;
- }
-
- public boolean isMina() {
- return this == APACHE;
- }
-}
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index a9ef70e..20ce991 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -345,9 +345,8 @@
update.putReviewer(ctx.getAccountId(), REVIEWER);
}
- // Approvals that are being set in the new patch-set during this operation are not available yet
- // outside of the scope of this method. Only copied approvals are set here.
- approvalsUtil.byPatchSet(ctx.getNotes(), newPatchSet).forEach(a -> update.putCopiedApproval(a));
+ approvalsUtil.persistCopiedApprovals(
+ ctx.getNotes(), newPatchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig(), update);
mailMessage = insertChangeMessage(update, ctx, reviewMessage);
if (mergedByPushOp == null) {
@@ -458,12 +457,7 @@
// We optimize here and only retrieve current when approvals provided
if (!approvals.isEmpty()) {
for (PatchSetApproval a :
- approvalsUtil.byPatchSetUser(
- ctx.getNotes(),
- priorPatchSetId,
- ctx.getAccountId(),
- ctx.getRevWalk(),
- ctx.getRepoView().getConfig())) {
+ approvalsUtil.byPatchSetUser(ctx.getNotes(), priorPatchSetId, ctx.getAccountId())) {
if (a.isLegacySubmit()) {
continue;
}
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 0710784..31e3948 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -378,8 +378,7 @@
// Get previous approvals from this user
Map<String, Short> approvals = new HashMap<>();
approvalsUtil
- .byPatchSetUser(
- notes, psId, ctx.getAccountId(), ctx.getRevWalk(), ctx.getRepoView().getConfig())
+ .byPatchSetUser(notes, psId, ctx.getAccountId())
.forEach(a -> approvals.put(a.label(), a.value()));
// Fire Gerrit event. Note that approvals can't be granted via email, so old and new approvals
// are always the same here.
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 2d9b014..bbfe8dd 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -391,13 +391,7 @@
return approvals;
}
- /**
- * This method is currently used only in tests. TODO(paiking): Use this method to fetch approvals
- * (including copied approvals) instead of computing copied approvals on demand. This will be used
- * by {@code ApprovalCache}.
- *
- * @return all approvals, including copied approvals.
- */
+ /** Gets all approvals, including copied approvals. */
public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovalsWithCopied() {
if (approvalsWithCopied == null) {
approvalsWithCopied = ImmutableListMultimap.copyOf(state.approvals());
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index c554ca5..b8b8a2c 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -61,7 +61,7 @@
.weigher(Weigher.class)
.maximumWeight(10 << 20)
.diskLimit(-1)
- .version(2)
+ .version(3)
.keySerializer(Key.Serializer.INSTANCE)
.valueSerializer(ChangeNotesState.Serializer.INSTANCE);
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 5cf3a64..a3a2b4b 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -40,11 +40,13 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_WORK_IN_PROGRESS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
import static java.util.Comparator.comparing;
+import static java.util.Comparator.comparingInt;
import static java.util.stream.Collectors.joining;
import com.google.common.base.Enums;
import com.google.common.base.Splitter;
import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.ListMultimap;
@@ -70,6 +72,7 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRecord.Label.Status;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.server.AssigneeStatusUpdate;
@@ -83,6 +86,7 @@
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -95,6 +99,7 @@
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
+import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.lib.ObjectId;
@@ -312,10 +317,81 @@
}
result.put(a.key().patchSetId(), a.build());
}
+ if (status != null && status.isClosed() && !isAnyApprovalCopied(result)) {
+ // If the change is closed, check if there are "submit records" with approvals that do not
+ // exist on the latest patch-set and copy them to the latest patch-set.
+ // We do not invoke this logic if any approval is copied. This is because prior to change
+ // https://gerrit-review.googlesource.com/c/gerrit/+/318135 we used to copy approvals
+ // dynamically (e.g. when requesting the change page). After that change, we started
+ // persisting copied votes in NoteDb, so we don't need to do this back-filling.
+ // Prior to that change (318135), we could've had changes with dynamically copied approvals
+ // that were merged in NoteDb but these approvals do not exist on the latest patch-set, so
+ // we need to back-fill these approvals.
+ PatchSet.Id latestPs = buildCurrentPatchSetId();
+ backFillMissingCopiedApprovalsFromSubmitRecords(result, latestPs).stream()
+ .forEach(a -> result.put(latestPs, a));
+ }
result.keySet().forEach(k -> result.get(k).sort(ChangeNotes.PSA_BY_TIME));
return result;
}
+ /**
+ * Returns patch-set approvals that do not exist on the latest patch-set but for which a submit
+ * record exists in NoteDb when the change was merged.
+ */
+ private List<PatchSetApproval> backFillMissingCopiedApprovalsFromSubmitRecords(
+ ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals, @Nullable PatchSet.Id latestPs) {
+ List<PatchSetApproval> copiedApprovals = new ArrayList<>();
+ if (latestPs == null) {
+ return copiedApprovals;
+ }
+ List<PatchSetApproval> approvalsOnLatestPs = allApprovals.get(latestPs);
+ ListMultimap<Account.Id, PatchSetApproval> approvalsByUser = getApprovalsByUser(allApprovals);
+ List<SubmitRecord.Label> submitRecordLabels =
+ submitRecords.stream()
+ .filter(r -> r.labels != null)
+ .flatMap(r -> r.labels.stream())
+ .filter(label -> Status.OK.equals(label.status) || Status.MAY.equals(label.status))
+ .collect(Collectors.toList());
+ for (SubmitRecord.Label recordLabel : submitRecordLabels) {
+ String labelName = recordLabel.label;
+ Account.Id appliedBy = recordLabel.appliedBy;
+ if (appliedBy == null || labelName == null) {
+ continue;
+ }
+ boolean existsAtLatestPs =
+ approvalsOnLatestPs.stream()
+ .anyMatch(a -> a.accountId().equals(appliedBy) && a.label().equals(labelName));
+ if (existsAtLatestPs) {
+ continue;
+ }
+ // Search for an approval for this label on the max previous patch-set and copy the approval.
+ Collection<PatchSetApproval> userApprovals =
+ approvalsByUser.get(appliedBy).stream()
+ .filter(approval -> approval.label().equals(labelName))
+ .collect(Collectors.toList());
+ if (userApprovals.isEmpty()) {
+ continue;
+ }
+ PatchSetApproval lastApproved =
+ Collections.max(userApprovals, comparingInt(a -> a.patchSetId().get()));
+ copiedApprovals.add(lastApproved.copyWithPatchSet(latestPs));
+ }
+ return copiedApprovals;
+ }
+
+ private boolean isAnyApprovalCopied(ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals) {
+ return allApprovals.values().stream().anyMatch(approval -> approval.copied());
+ }
+
+ private ListMultimap<Account.Id, PatchSetApproval> getApprovalsByUser(
+ ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals) {
+ return allApprovals.values().stream()
+ .collect(
+ ImmutableListMultimap.toImmutableListMultimap(
+ PatchSetApproval::accountId, Function.identity()));
+ }
+
private List<ReviewerStatusUpdate> buildReviewerUpdates() {
List<ReviewerStatusUpdate> result = new ArrayList<>();
HashMap<Account.Id, ReviewerStateInternal> lastState = new HashMap<>();
diff --git a/java/com/google/gerrit/server/patch/DiffOperations.java b/java/com/google/gerrit/server/patch/DiffOperations.java
index c84186d..a265337 100644
--- a/java/com/google/gerrit/server/patch/DiffOperations.java
+++ b/java/com/google/gerrit/server/patch/DiffOperations.java
@@ -18,10 +18,14 @@
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import java.util.Map;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevWalk;
/**
* An interface for all file diff related operations. Clients should use this interface to request:
@@ -60,6 +64,29 @@
throws DiffNotAvailableException;
/**
+ * This method is similar to {@link #listModifiedFilesAgainstParent(NameKey, ObjectId, int,
+ * DiffOptions)} but loads the modified files directly instead of retrieving them from the diff
+ * cache.
+ *
+ * <p>A RevWalk and repoConfig are also supplied and are used to look up the commit IDs. This is
+ * useful in case one the commits is currently being created, that's why the {@code revWalk}
+ * parameter is needed.
+ *
+ * <p>Note that rename detection is disabled for this method.
+ *
+ * @return a map of file paths to {@link ModifiedFile}. The {@link ModifiedFile} contains the
+ * old/new file paths and the change type (added, deleted, etc...).
+ */
+ Map<String, ModifiedFile> loadModifiedFilesAgainstParent(
+ Project.NameKey project,
+ ObjectId newCommit,
+ int parentNum,
+ DiffOptions diffOptions,
+ RevWalk revWalk,
+ Config repoConfig)
+ throws DiffNotAvailableException;
+
+ /**
* Returns the list of added, deleted or modified files between two commits (patchsets). The
* commit message and merge list (for merge commits) are also returned.
*
@@ -77,6 +104,29 @@
throws DiffNotAvailableException;
/**
+ * This method is similar to {@link #listModifiedFilesAgainstParent(NameKey, ObjectId, int,
+ * DiffOptions)} but loads the modified files directly instead of retrieving them from the diff
+ * cache.
+ *
+ * <p>A RevWalk and repoConfig are also supplied and are used to look up the commit IDs. This is
+ * useful in case one the commits is currently being created, that's why the {@code revWalk}
+ * parameter is needed.
+ *
+ * <p>Note that rename detection is disabled for this method.
+ *
+ * @return a map of file paths to {@link ModifiedFile}. The {@link ModifiedFile} contains the
+ * old/new file paths and the change type (added, deleted, etc...).
+ */
+ Map<String, ModifiedFile> loadModifiedFiles(
+ Project.NameKey project,
+ ObjectId oldCommit,
+ ObjectId newCommit,
+ DiffOptions diffOptions,
+ RevWalk revWalk,
+ Config repoConfig)
+ throws DiffNotAvailableException;
+
+ /**
* Returns the diff for a single file between a patchset commit against its parent or the
* auto-merge commit. For deleted files, the {@code fileName} parameter should contain the old
* name of the file. This method will return {@link FileDiffOutput#empty(String, ObjectId,
diff --git a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
index 3d230f8..012a91c 100644
--- a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
+++ b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
@@ -47,7 +47,15 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
/**
* Provides different file diff operations. Uses the underlying Git/Gerrit caches to speed up the
@@ -57,6 +65,19 @@
public class DiffOperationsImpl implements DiffOperations {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final ImmutableMap<DiffEntry.ChangeType, Patch.ChangeType> changeTypeMap =
+ ImmutableMap.of(
+ DiffEntry.ChangeType.ADD,
+ Patch.ChangeType.ADDED,
+ DiffEntry.ChangeType.MODIFY,
+ Patch.ChangeType.MODIFIED,
+ DiffEntry.ChangeType.DELETE,
+ Patch.ChangeType.DELETED,
+ DiffEntry.ChangeType.RENAME,
+ Patch.ChangeType.RENAMED,
+ DiffEntry.ChangeType.COPY,
+ Patch.ChangeType.COPIED);
+
private static final int RENAME_SCORE = 60;
private static final DiffAlgorithm DEFAULT_DIFF_ALGORITHM =
DiffAlgorithm.HISTOGRAM_WITH_FALLBACK_MYERS;
@@ -103,6 +124,27 @@
}
@Override
+ public Map<String, ModifiedFile> loadModifiedFilesAgainstParent(
+ Project.NameKey project,
+ ObjectId newCommit,
+ int parentNum,
+ DiffOptions diffOptions,
+ RevWalk revWalk,
+ Config repoConfig)
+ throws DiffNotAvailableException {
+ try {
+ DiffParameters diffParams = computeDiffParameters(project, newCommit, parentNum);
+ return loadModifiedFilesWithoutCache(project, diffParams, revWalk, repoConfig);
+ } catch (IOException e) {
+ throw new DiffNotAvailableException(
+ String.format(
+ "Failed to evaluate the parent/base commit for commit '%s' with parentNum=%d",
+ newCommit, parentNum),
+ e);
+ }
+ }
+
+ @Override
public Map<String, FileDiffOutput> listModifiedFiles(
Project.NameKey project, ObjectId oldCommit, ObjectId newCommit, DiffOptions diffOptions)
throws DiffNotAvailableException {
@@ -117,6 +159,25 @@
}
@Override
+ public Map<String, ModifiedFile> loadModifiedFiles(
+ Project.NameKey project,
+ ObjectId oldCommit,
+ ObjectId newCommit,
+ DiffOptions diffOptions,
+ RevWalk revWalk,
+ Config repoConfig)
+ throws DiffNotAvailableException {
+ DiffParameters params =
+ DiffParameters.builder()
+ .project(project)
+ .newCommit(newCommit)
+ .baseCommit(oldCommit)
+ .comparisonType(ComparisonType.againstOtherPatchSet())
+ .build();
+ return loadModifiedFilesWithoutCache(project, params, revWalk, repoConfig);
+ }
+
+ @Override
public FileDiffOutput getModifiedFileAgainstParent(
Project.NameKey project,
ObjectId newCommit,
@@ -329,6 +390,50 @@
.build();
}
+ /** Loads the modified file paths between two commits without inspecting the diff cache. */
+ private static Map<String, ModifiedFile> loadModifiedFilesWithoutCache(
+ Project.NameKey project, DiffParameters diffParams, RevWalk revWalk, Config repoConfig)
+ throws DiffNotAvailableException {
+ ObjectId newCommit = diffParams.newCommit();
+ ObjectId oldCommit = diffParams.baseCommit();
+ try {
+ ObjectReader reader = revWalk.getObjectReader();
+ List<DiffEntry> diffEntries;
+ try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
+ df.setReader(reader, repoConfig);
+ df.setDetectRenames(false);
+ diffEntries = df.scan(oldCommit.equals(ObjectId.zeroId()) ? null : oldCommit, newCommit);
+ }
+ return diffEntries.stream()
+ .map(
+ entry ->
+ ModifiedFile.builder()
+ .changeType(toChangeType(entry.getChangeType()))
+ .oldPath(getGitPath(entry.getOldPath()))
+ .newPath(getGitPath(entry.getNewPath()))
+ .build())
+ .collect(ImmutableMap.toImmutableMap(ModifiedFile::getDefaultPath, Function.identity()));
+ } catch (IOException e) {
+ throw new DiffNotAvailableException(
+ String.format(
+ "Failed to compute the modified files for project '%s',"
+ + " old commit '%s', new commit '%s'.",
+ project, oldCommit.name(), newCommit.name()),
+ e);
+ }
+ }
+
+ private static Optional<String> getGitPath(String path) {
+ return path.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(path);
+ }
+
+ private static Patch.ChangeType toChangeType(DiffEntry.ChangeType changeType) {
+ if (!changeTypeMap.containsKey(changeType)) {
+ throw new IllegalArgumentException("Unsupported type " + changeType);
+ }
+ return changeTypeMap.get(changeType);
+ }
+
@AutoValue
abstract static class DiffParameters {
abstract Project.NameKey project();
diff --git a/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java b/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
index f4e7ca3..3596a54 100644
--- a/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
+++ b/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
@@ -47,6 +47,10 @@
*/
public abstract Optional<String> newPath();
+ public String getDefaultPath() {
+ return newPath().isPresent() ? newPath().get() : oldPath().get();
+ }
+
public static Builder builder() {
return new AutoValue_ModifiedFile.Builder();
}
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalContext.java b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
index 4dedbb5..2839eb7 100644
--- a/java/com/google/gerrit/server/query/approval/ApprovalContext.java
+++ b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
@@ -21,6 +21,8 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.extensions.client.ChangeKind;
import com.google.gerrit.server.notedb.ChangeNotes;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevWalk;
/** Entity representing all required information to match predicates for copying approvals. */
@AutoValue
@@ -41,8 +43,19 @@
/** {@link ChangeKind} of the delta between the origin and target patch set. */
public abstract ChangeKind changeKind();
+ /** {@link RevWalk} of the repository for the current commit. */
+ public abstract RevWalk revWalk();
+
+ /** {@link RevWalk} of the repository for the current commit. */
+ public abstract Config repoConfig();
+
public static ApprovalContext create(
- ChangeNotes changeNotes, PatchSetApproval psa, PatchSet patchSet, ChangeKind changeKind) {
+ ChangeNotes changeNotes,
+ PatchSetApproval psa,
+ PatchSet patchSet,
+ ChangeKind changeKind,
+ RevWalk revWalk,
+ Config repoConfig) {
checkState(
psa.patchSetId().changeId().equals(patchSet.id().changeId()),
"approval and target must be the same change. got: %s, %s",
@@ -54,6 +67,7 @@
// As explained in the commit message of this change doing this check is only possible if there
// are no changes with gaps in patch set numbers. Since it's planned to fix-up old changes with
// gaps in patch set numbers, this todo is a reminder to add back the check once this is done.
- return new AutoValue_ApprovalContext(psa, patchSet, changeNotes, changeKind);
+ return new AutoValue_ApprovalContext(
+ psa, patchSet, changeNotes, changeKind, revWalk, repoConfig);
}
}
diff --git a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
index c35fa1c..2a72c49 100644
--- a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
+++ b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
@@ -24,7 +24,7 @@
import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.DiffOperations;
import com.google.gerrit.server.patch.DiffOptions;
-import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@@ -59,24 +59,30 @@
Integer parentNum =
isInitialCommit(ctx.changeNotes().getProjectName(), targetPatchSet.commitId()) ? 0 : 1;
try {
- Map<String, FileDiffOutput> baseVsCurrent =
- diffOperations.listModifiedFilesAgainstParent(
+ Map<String, ModifiedFile> baseVsCurrent =
+ diffOperations.loadModifiedFilesAgainstParent(
ctx.changeNotes().getProjectName(),
targetPatchSet.commitId(),
parentNum,
- DiffOptions.DEFAULTS);
- Map<String, FileDiffOutput> baseVsPrior =
- diffOperations.listModifiedFilesAgainstParent(
+ DiffOptions.DEFAULTS,
+ ctx.revWalk(),
+ ctx.repoConfig());
+ Map<String, ModifiedFile> baseVsPrior =
+ diffOperations.loadModifiedFilesAgainstParent(
ctx.changeNotes().getProjectName(),
sourcePatchSet.commitId(),
parentNum,
- DiffOptions.DEFAULTS);
- Map<String, FileDiffOutput> priorVsCurrent =
- diffOperations.listModifiedFiles(
+ DiffOptions.DEFAULTS,
+ ctx.revWalk(),
+ ctx.repoConfig());
+ Map<String, ModifiedFile> priorVsCurrent =
+ diffOperations.loadModifiedFiles(
ctx.changeNotes().getProjectName(),
sourcePatchSet.commitId(),
targetPatchSet.commitId(),
- DiffOptions.DEFAULTS);
+ DiffOptions.DEFAULTS,
+ ctx.revWalk(),
+ ctx.repoConfig());
return match(baseVsCurrent, baseVsPrior, priorVsCurrent);
} catch (DiffNotAvailableException ex) {
throw new StorageException(
@@ -92,9 +98,9 @@
* {@link ChangeType} matches for each modified file.
*/
public boolean match(
- Map<String, FileDiffOutput> baseVsCurrent,
- Map<String, FileDiffOutput> baseVsPrior,
- Map<String, FileDiffOutput> priorVsCurrent) {
+ Map<String, ModifiedFile> baseVsCurrent,
+ Map<String, ModifiedFile> baseVsPrior,
+ Map<String, ModifiedFile> priorVsCurrent) {
Set<String> allFiles = new HashSet<>();
allFiles.addAll(baseVsCurrent.keySet());
allFiles.addAll(baseVsPrior.keySet());
@@ -102,17 +108,17 @@
if (Patch.isMagic(file)) {
continue;
}
- FileDiffOutput fileDiffOutput1 = baseVsCurrent.get(file);
- FileDiffOutput fileDiffOutput2 = baseVsPrior.get(file);
+ ModifiedFile modifiedFile1 = baseVsCurrent.get(file);
+ ModifiedFile modifiedFile2 = baseVsPrior.get(file);
if (!priorVsCurrent.containsKey(file)) {
// If the file is not modified between prior and current patchsets, then scan safely skip
- // it. The file might has been modified due to rebase.
+ // it. The file might have been modified due to rebase.
continue;
}
- if (fileDiffOutput1 == null || fileDiffOutput2 == null) {
+ if (modifiedFile1 == null || modifiedFile2 == null) {
return false;
}
- if (!fileDiffOutput2.changeType().equals(fileDiffOutput1.changeType())) {
+ if (!modifiedFile2.changeType().equals(modifiedFile1.changeType())) {
return false;
}
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 397a8fa..f3a8a9d 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -774,7 +774,7 @@
if (!lazyload()) {
return ImmutableListMultimap.of();
}
- allApprovals = approvalsUtil.byChange(notes());
+ allApprovals = approvalsUtil.byChangeExcludingCopiedApprovals(notes());
}
return allApprovals;
}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 4387524..b266031 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -197,9 +197,7 @@
Account.Id accountId = accountState.account().id();
- for (PatchSetApproval a :
- approvalsUtil.byPatchSetUser(
- ctx.getNotes(), psId, accountId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
+ for (PatchSetApproval a : approvalsUtil.byPatchSetUser(ctx.getNotes(), psId, accountId)) {
if (!labelTypes.byLabel(a.labelId()).isPresent()) {
continue; // Ignore undefined labels.
} else if (!a.label().equals(label)) {
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index 8c21841..86ec6c8 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -260,11 +260,8 @@
* proposal: https://gerrit-review.googlesource.com/c/gerrit/+/129171
*/
private void updateApprovals(
- ChangeContext ctx, ChangeUpdate update, PatchSet.Id psId, Project.NameKey project)
- throws IOException {
- for (PatchSetApproval psa :
- approvalsUtil.byPatchSet(
- ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
+ ChangeContext ctx, ChangeUpdate update, PatchSet.Id psId, Project.NameKey project) {
+ for (PatchSetApproval psa : approvalsUtil.byPatchSet(ctx.getNotes(), psId)) {
ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
Optional<LabelType> type =
projectState.getLabelTypes(ctx.getNotes()).byLabel(psa.labelId());
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 5c252f4..24b0799 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -1579,12 +1579,7 @@
Map<String, PatchSetApproval> current = new HashMap<>();
for (PatchSetApproval a :
- approvalsUtil.byPatchSetUser(
- ctx.getNotes(),
- psId,
- user.getAccountId(),
- ctx.getRevWalk(),
- ctx.getRepoView().getConfig())) {
+ approvalsUtil.byPatchSetUser(ctx.getNotes(), psId, user.getAccountId())) {
if (a.isLegacySubmit()) {
continue;
}
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index 355d25f..cc3b75d 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -211,10 +211,7 @@
.setPostMessage(false)
.setSendEmail(false)
.setMatchAuthorToCommitterDate(
- args.project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE))
- // The votes are automatically copied and they don't count as copied votes. See
- // method's javadoc.
- .setStoreCopiedVotes(/* storeCopiedVotes = */ false);
+ args.project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE));
try {
rebaseOp.updateRepo(ctx);
} catch (MergeConflictException | NoSuchChangeException e) {
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index 7d428eb..cd8ea4c 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -348,13 +348,10 @@
}
}
- private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update)
- throws IOException {
+ private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update) {
PatchSet.Id psId = update.getPatchSetId();
Map<PatchSetApproval.Key, PatchSetApproval> byKey = new HashMap<>();
- for (PatchSetApproval psa :
- args.approvalsUtil.byPatchSet(
- ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
+ for (PatchSetApproval psa : args.approvalsUtil.byPatchSet(ctx.getNotes(), psId)) {
byKey.put(psa.key(), psa);
}
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index f3bd5e1..af7078d 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -4,7 +4,6 @@
name = "sshd",
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
- runtime_deps = ["//lib:jsch"],
deps = [
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/common:server",
diff --git a/java/com/google/gerrit/sshd/SshSessionFactoryInitializer.java b/java/com/google/gerrit/sshd/SshSessionFactoryInitializer.java
index 1cdf923..de91b68 100644
--- a/java/com/google/gerrit/sshd/SshSessionFactoryInitializer.java
+++ b/java/com/google/gerrit/sshd/SshSessionFactoryInitializer.java
@@ -14,9 +14,6 @@
package com.google.gerrit.sshd;
-import static com.google.gerrit.server.config.SshClientImplementation.APACHE;
-
-import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.sshd.DefaultProxyDataFactory;
import org.eclipse.jgit.transport.sshd.JGitKeyCache;
@@ -24,13 +21,11 @@
import org.eclipse.jgit.util.FS;
public class SshSessionFactoryInitializer {
- public static void init(Config config) {
- if (APACHE == config.getEnum("ssh", null, "clientImplementation", APACHE)) {
- SshdSessionFactory factory =
- new SshdSessionFactory(new JGitKeyCache(), new DefaultProxyDataFactory());
- factory.setHomeDirectory(FS.DETECTED.userHome());
- SshSessionFactory.setInstance(factory);
- }
+ public static void init() {
+ SshdSessionFactory factory =
+ new SshdSessionFactory(new JGitKeyCache(), new DefaultProxyDataFactory());
+ factory.setHomeDirectory(FS.DETECTED.userHome());
+ SshSessionFactory.setInstance(factory);
}
private SshSessionFactoryInitializer() {}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 3888679..ff8a2d0 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -34,7 +34,10 @@
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.collect.MoreCollectors;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
@@ -44,12 +47,15 @@
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.client.ChangeKind;
@@ -58,13 +64,18 @@
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.server.change.ChangeKindCacheImpl;
import com.google.gerrit.server.project.testing.TestLabels;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.SubmitRule;
import com.google.inject.Inject;
import com.google.inject.name.Named;
+import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Before;
import org.junit.Test;
@@ -75,6 +86,7 @@
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ChangeOperations changeOperations;
@Inject private ChangeKindCreator changeKindCreator;
+ @Inject private ExtensionRegistry extensionRegistry;
@Inject
@Named("change_kind")
@@ -260,6 +272,113 @@
}
@Test
+ public void sticky_copiedToLatestPatchSetFromSubmitRecords() throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().updateLabelType(LabelId.VERIFIED, b -> b.setFunction(LabelFunction.NO_BLOCK));
+ u.save();
+ }
+
+ // This test is covering the backfilling logic for changes which have been submitted, based on
+ // copied approvals, before Gerrit persisted copied votes as Copied-Label footers in NoteDb. It
+ // verifies that for such changes copied approvals are returned from the API even if the copied
+ // votes were not persisted as Copied-Label footers.
+ //
+ // In other words, this test verifies that given a change that was approved by a copied vote and
+ // then submitted and for which the copied approval is not persisted as a Copied-Label footer in
+ // NoteDb the copied approval is backfilled from the corresponding Submitted-With footer that
+ // got written to NoteDb on submit.
+ //
+ // Creating such a change would be possible by running the old Gerrit code from before Gerrit
+ // persisted copied labels as Copied-Label footers. However since this old Gerrit code is no
+ // longer available, the test needs to apply a trick to create a change in this state. It
+ // configures a fake submit rule, that pretends that an approval for a non-sticky label from an
+ // old patch set is still present on the current patch set and allows to submit the change.
+ // Since the label is non-sticky no Copied-Label footer is written for it. On submit the fake
+ // submit rule results in a Submitted-With footer that records the label as approved (although
+ // the label is actually not present on the current patch set). This is exactly the change state
+ // that we would have had by running the old code if submit was based on a copied label. As
+ // result of the backfilling logic we expect that this "copied" label (the label that is
+ // mentioned in the Submitted-With footer) is returned from the API.
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(new TestSubmitRule(user.id()))) {
+ // We want to add a vote on PS1, then not copy it to PS2, but include it in submit records
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ // Vote on patch-set 1
+ vote(admin, changeId, 2, 1);
+ vote(user, changeId, 1, -1);
+
+ // Upload patch-set 2. Change user's "Verified" vote on PS2.
+ changeOperations
+ .change(Change.id(r.getChange().getId().get()))
+ .newPatchset()
+ .file("new_file")
+ .content("content")
+ .commitMessage("Upload PS2")
+ .create();
+ vote(admin, changeId, 2, 1);
+ vote(user, changeId, 1, 1);
+
+ // Upload patch-set 3
+ changeOperations
+ .change(Change.id(r.getChange().getId().get()))
+ .newPatchset()
+ .file("another_file")
+ .content("content")
+ .commitMessage("Upload PS3")
+ .create();
+ vote(admin, changeId, 2, 1);
+
+ List<PatchSetApproval> patchSetApprovals =
+ notesFactory.create(project, r.getChange().getId()).getApprovalsWithCopied().values()
+ .stream()
+ .sorted(comparing(a -> a.patchSetId().get()))
+ .collect(toImmutableList());
+
+ // There's no verified approval on PS#3.
+ assertThat(
+ patchSetApprovals.stream()
+ .filter(
+ a ->
+ a.accountId().equals(user.id())
+ && a.label().equals(TestLabels.verified().getName())
+ && a.patchSetId().get() == 3)
+ .collect(Collectors.toList()))
+ .isEmpty();
+
+ // Submit the change. The TestSubmitRule will store a "submit record" containing a label
+ // voted by user, but the latest patch-set does not have an approval for this user, hence
+ // it will be copied if we request approvals after the change is merged.
+ requestScopeOperations.setApiUser(admin.id());
+ gApi.changes().id(changeId).current().submit();
+
+ patchSetApprovals =
+ notesFactory.create(project, r.getChange().getId()).getApprovalsWithCopied().values()
+ .stream()
+ .sorted(comparing(a -> a.patchSetId().get()))
+ .collect(toImmutableList());
+
+ // Get the copied approval for user on PS3 for the "Verified" label.
+ PatchSetApproval verifiedApproval =
+ patchSetApprovals.stream()
+ .filter(
+ a ->
+ a.accountId().equals(user.id())
+ && a.label().equals(TestLabels.verified().getName())
+ && a.patchSetId().get() == 3)
+ .collect(MoreCollectors.onlyElement());
+
+ assertCopied(
+ verifiedApproval,
+ /* psId= */ 3,
+ TestLabels.verified().getName(),
+ (short) 1,
+ /* copied= */ true);
+ }
+ }
+
+ @Test
public void stickyOnCopyValues() throws Exception {
TestAccount user2 = accountCreator.user2();
@@ -1265,6 +1384,35 @@
assertThat(nonCopiedSecondPatchsetRemovedVote.copied()).isFalse();
}
+ @Test
+ public void reviewerStickyVotingCanBeRemoved() throws Exception {
+ // Code-Review will be sticky.
+ String label = LabelId.CODE_REVIEW;
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().updateLabelType(label, b -> b.setCopyAnyScore(true));
+ u.save();
+ }
+
+ PushOneCommit.Result r = createChange();
+
+ // Add a new vote by user
+ requestScopeOperations.setApiUser(user.id());
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend());
+ requestScopeOperations.setApiUser(admin.id());
+
+ // Make a new patchset, keeping the Code-Review +1 vote.
+ amendChange(r.getChangeId());
+ assertVotes(detailedChange(r.getChangeId()), user, label, 1, null);
+
+ gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
+
+ assertThat(r.getChange().notes().getApprovalsWithCopied()).isEmpty();
+
+ // Changes message has info about vote removed.
+ assertThat(Iterables.getLast(gApi.changes().id(r.getChangeId()).messages()).message)
+ .contains("Code-Review+1 by User");
+ }
+
private void assertChangeKindCacheContains(ObjectId prior, ObjectId next) {
ChangeKind kind =
changeKindCache.getIfPresent(ChangeKindCacheImpl.Key.create(prior, next, "recursive"));
@@ -1352,4 +1500,29 @@
assertThat(approval.value()).isEqualTo(value);
assertThat(approval.copied()).isEqualTo(copied);
}
+
+ /**
+ * Test submit rule that always return a passing record with a "Verified" label applied by {@link
+ * TestSubmitRule#userAccountId}.
+ */
+ private static class TestSubmitRule implements SubmitRule {
+ Account.Id userAccountId;
+
+ TestSubmitRule(Account.Id userAccountId) {
+ this.userAccountId = userAccountId;
+ }
+
+ @Override
+ public Optional<SubmitRecord> evaluate(ChangeData changeData) {
+ SubmitRecord record = new SubmitRecord();
+ record.ruleName = "testSubmitRule";
+ record.status = SubmitRecord.Status.OK;
+ SubmitRecord.Label label = new SubmitRecord.Label();
+ label.label = "Verified";
+ label.status = SubmitRecord.Label.Status.OK;
+ label.appliedBy = userAccountId;
+ record.labels = Arrays.asList(label);
+ return Optional.of(record);
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
index 537c7d8..97f072e 100644
--- a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
@@ -36,6 +36,8 @@
import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.inject.Inject;
import java.util.Date;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Test;
public class ApprovalQueryIT extends AbstractDaemonTest {
@@ -250,7 +252,7 @@
}
private ApprovalContext contextForCodeReviewLabel(
- int value, PatchSet.Id psId, Account.Id approver) {
+ int value, PatchSet.Id psId, Account.Id approver) throws Exception {
ChangeNotes changeNotes = changeNotesFactory.create(project, psId.changeId());
PatchSet.Id newPsId = PatchSet.id(psId.changeId(), psId.get() + 1);
ChangeKind changeKind =
@@ -263,7 +265,15 @@
.key(PatchSetApproval.key(psId, approver, LabelId.create("Code-Review")))
.value(value)
.build();
- return ApprovalContext.create(
- changeNotes, approval, changeNotes.getPatchSets().get(newPsId), changeKind);
+ try (Repository repo = repoManager.openRepository(project);
+ RevWalk rw = new RevWalk(repo.newObjectReader())) {
+ return ApprovalContext.create(
+ changeNotes,
+ approval,
+ changeNotes.getPatchSets().get(newPsId),
+ changeKind,
+ rw,
+ repo.getConfig());
+ }
}
}
diff --git a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
index 9acb701..8b62628 100644
--- a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
+++ b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
@@ -19,10 +19,12 @@
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.InMemoryModule;
import com.google.inject.Guice;
@@ -30,6 +32,7 @@
import com.google.inject.Injector;
import java.io.IOException;
import java.util.Map;
+import java.util.Optional;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -37,6 +40,7 @@
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.TreeFormatter;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Before;
@@ -112,6 +116,66 @@
assertThat(repo.getRefDatabase().exactRef(autoMergeRef)).isNotNull();
}
+ @Test
+ public void loadModifiedFiles() throws Exception {
+ ImmutableMap<String, String> oldFiles =
+ ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2);
+ ObjectId oldCommitId = createCommit(repo, null, oldFiles);
+
+ ImmutableMap<String, String> newFiles =
+ ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2 + "\nnew line here");
+ ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
+
+ Repository repository = repoManager.openRepository(testProjectName);
+ ObjectReader objectReader = repository.newObjectReader();
+ RevWalk rw = new RevWalk(objectReader);
+ StoredConfig repoConfig = repository.getConfig();
+
+ // This call loads modified files directly without going through the diff cache.
+ Map<String, ModifiedFile> modifiedFiles =
+ diffOperations.loadModifiedFiles(
+ testProjectName, newCommitId, oldCommitId, DiffOptions.DEFAULTS, rw, repoConfig);
+
+ assertThat(modifiedFiles)
+ .containsExactly(
+ fileName2,
+ ModifiedFile.builder()
+ .changeType(ChangeType.MODIFIED)
+ .oldPath(Optional.of(fileName2))
+ .newPath(Optional.of(fileName2))
+ .build());
+ }
+
+ @Test
+ public void loadModifiedFilesAgainstParent() throws Exception {
+ ImmutableMap<String, String> oldFiles =
+ ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2);
+ ObjectId oldCommitId = createCommit(repo, null, oldFiles);
+
+ ImmutableMap<String, String> newFiles =
+ ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2 + "\nnew line here");
+ ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
+
+ Repository repository = repoManager.openRepository(testProjectName);
+ ObjectReader objectReader = repository.newObjectReader();
+ RevWalk rw = new RevWalk(objectReader);
+ StoredConfig repoConfig = repository.getConfig();
+
+ // This call loads modified files directly without going through the diff cache.
+ Map<String, ModifiedFile> modifiedFiles =
+ diffOperations.loadModifiedFilesAgainstParent(
+ testProjectName, newCommitId, /* parentNum=*/ 0, DiffOptions.DEFAULTS, rw, repoConfig);
+
+ assertThat(modifiedFiles)
+ .containsExactly(
+ fileName2,
+ ModifiedFile.builder()
+ .changeType(ChangeType.MODIFIED)
+ .oldPath(Optional.of(fileName2))
+ .newPath(Optional.of(fileName2))
+ .build());
+ }
+
private ObjectId createMergeCommit(
Repository repo,
ImmutableMap<String, String> fileNameToContent,
diff --git a/lib/BUILD b/lib/BUILD
index b2810cf..01e6a25 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -47,13 +47,6 @@
)
java_library(
- name = "jgit-ssh-jsch",
- data = ["//lib:LICENSE-jgit"],
- visibility = ["//visibility:public"],
- exports = ["@jgit//org.eclipse.jgit.ssh.jsch:ssh-jsch"],
-)
-
-java_library(
name = "jgit-ssh-apache",
data = ["//lib:LICENSE-jgit"],
visibility = ["//visibility:public"],
@@ -162,13 +155,6 @@
)
java_library(
- name = "jsch",
- data = ["//lib:LICENSE-jsch"],
- visibility = ["//visibility:public"],
- exports = ["@jsch//jar"],
-)
-
-java_library(
name = "juniversalchardet",
data = ["//lib:LICENSE-MPL1.1"],
visibility = ["//visibility:public"],
diff --git a/plugins/BUILD b/plugins/BUILD
index 0e5df2c..4b5343c 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -85,7 +85,6 @@
"//lib/jackson:jackson-core",
"//lib:jgit-servlet",
"//lib:jgit",
- "//lib:jgit-ssh-jsch",
"//lib:jsr305",
"//lib/log:api",
"//lib/log:log4j",
@@ -100,7 +99,6 @@
"//lib:guava-retrying",
"//lib:gson",
"//lib:icu4j",
- "//lib:jsch",
"//lib:mime-util",
"//lib:protobuf",
"//lib:servlet-api-without-neverlink",
diff --git a/plugins/replication b/plugins/replication
index b6f4011..d9c59a0 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit b6f4011070a8f0bf31fb1158ed12be71374eb47a
+Subproject commit d9c59a0be1c423706b2c4c74c955ebbeca5a894d
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index e397117..1d66aa5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -180,7 +180,7 @@
fireReload,
fireTitleChange,
} from '../../../utils/event-util';
-import {GerritView, routerView$} from '../../../services/router/router-model';
+import {GerritView} from '../../../services/router/router-model';
import {
debounce,
DelayedTask,
@@ -604,6 +604,8 @@
// Private but used in tests.
readonly changeModel = getAppContext().changeModel;
+ private readonly routerModel = getAppContext().routerModel;
+
private readonly commentsModel = getAppContext().commentsModel;
private readonly shortcuts = getAppContext().shortcutsService;
@@ -632,7 +634,7 @@
})
);
this.subscriptions.push(
- routerView$.subscribe(view => {
+ this.routerModel.routerView$.subscribe(view => {
this.isViewCurrent = view === GerritView.CHANGE;
})
);
@@ -880,6 +882,13 @@
this._tabState = e.detail.tabState;
}
+ /**
+ * Currently there is a bug in this code where this.unresolvedOnly is only
+ * assigned the correct value when _onPaperTabClick is triggered which is
+ * only triggered when user explicitly clicks on the tab however the comments
+ * tab can also be opened via the url in which case the correct value to
+ * unresolvedOnly is never assigned.
+ */
_onPaperTabClick(e: MouseEvent) {
let target = e.target as HTMLElement | null;
let tabName: string | undefined;
@@ -891,7 +900,8 @@
} while (target);
if (tabName === PrimaryTab.COMMENT_THREADS) {
- // Show unresolved threads by default only if they are present
+ // Show unresolved threads by default
+ // Show resolved threads only if no unresolved threads exist
const hasUnresolvedThreads =
(this._commentThreads ?? []).filter(thread => isUnresolved(thread))
.length > 0;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 0e74cc6..e8cce2d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -17,6 +17,7 @@
import '../../../test/common-test-setup-karma';
import '../../edit/gr-edit-constants';
+import '../gr-thread-list/gr-thread-list';
import './gr-change-view';
import {
ChangeStatus,
@@ -107,6 +108,7 @@
import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrChangeStar} from '../../shared/gr-change-star/gr-change-star';
+import {GrThreadList} from '../gr-thread-list/gr-thread-list';
const fixture = fixtureFromElement('gr-change-view');
@@ -862,7 +864,43 @@
});
});
- suite('Findings comment tab', () => {
+ suite('Comments tab', () => {
+ setup(async () => {
+ element._changeNum = TEST_NUMERIC_CHANGE_ID;
+ element._change = {
+ ...createChangeViewChange(),
+ revisions: {
+ rev2: createRevision(2),
+ rev1: createRevision(1),
+ rev13: createRevision(13),
+ rev3: createRevision(3),
+ rev4: createRevision(4),
+ },
+ current_revision: 'rev4' as CommitId,
+ };
+ element._commentThreads = THREADS;
+ await flush();
+ const paperTabs = element.shadowRoot!.querySelector('#primaryTabs')!;
+ tap(paperTabs.querySelectorAll('paper-tab')[1]);
+ await flush();
+ });
+
+ test('commentId overrides unresolveOnly default', async () => {
+ const threadList = queryAndAssert<GrThreadList>(
+ element,
+ 'gr-thread-list'
+ );
+ assert.isTrue(element.unresolvedOnly);
+ assert.isNotOk(element.scrollCommentId);
+ assert.isTrue(threadList.unresolvedOnly);
+
+ element.scrollCommentId = 'abcd' as UrlEncodedCommentId;
+ await flush();
+ assert.isFalse(threadList.unresolvedOnly);
+ });
+ });
+
+ suite('Findings robot-comment tab', () => {
setup(async () => {
element._changeNum = TEST_NUMERIC_CHANGE_ID;
element._change = {
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index 0349b3f..490162b 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -212,6 +212,7 @@
override willUpdate(changed: PropertyValues) {
if (changed.has('commentTabState')) this.onCommentTabStateUpdate();
+ if (changed.has('scrollCommentId')) this.onScrollCommentIdUpdate();
}
private onCommentTabStateUpdate() {
@@ -228,6 +229,14 @@
}
}
+ /**
+ * When user wants to scroll to a comment, render all comments so that the
+ * appropriate comment can be scrolled into view.
+ */
+ private onScrollCommentIdUpdate() {
+ if (this.scrollCommentId) this.handleAllComments();
+ }
+
static override get styles() {
return [
sharedStyles,
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 027c976..2a35494 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -66,7 +66,7 @@
AppElementParams,
} from '../../gr-app-types';
import {LocationChangeEventDetail} from '../../../types/events';
-import {GerritView, updateState} from '../../../services/router/router-model';
+import {GerritView} from '../../../services/router/router-model';
import {firePageError} from '../../../utils/event-util';
import {addQuotesWhen} from '../../../utils/string-util';
import {windowLocationReload} from '../../../utils/dom-util';
@@ -311,6 +311,8 @@
private readonly reporting = getAppContext().reportingService;
+ private readonly routerModel = getAppContext().routerModel;
+
private readonly restApiService = getAppContext().restApiService;
private readonly flagsService = getAppContext().flagsService;
@@ -323,11 +325,11 @@
}
_setParams(params: AppElementParams | GenerateUrlParameters) {
- updateState(
- params.view,
- 'changeNum' in params ? params.changeNum : undefined,
- 'patchNum' in params ? params.patchNum ?? undefined : undefined
- );
+ this.routerModel.updateState({
+ view: params.view,
+ changeNum: 'changeNum' in params ? params.changeNum : undefined,
+ patchNum: 'patchNum' in params ? params.patchNum ?? undefined : undefined,
+ });
this._appElement().params = params;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index e527134..ff02d61 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -108,7 +108,7 @@
import {AppElementParams, AppElementDiffViewParam} from '../../gr-app-types';
import {EventType, OpenFixPreviewEvent} from '../../../types/events';
import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
-import {GerritView, routerView$} from '../../../services/router/router-model';
+import {GerritView} from '../../../services/router/router-model';
import {assertIsDefined} from '../../../utils/common-util';
import {addGlobalShortcut, Key, toggleClass} from '../../../utils/dom-util';
import {CursorMoveResult} from '../../../api/core';
@@ -356,6 +356,9 @@
private readonly restApiService = getAppContext().restApiService;
// Private but used in tests.
+ readonly routerModel = getAppContext().routerModel;
+
+ // Private but used in tests.
readonly userModel = getAppContext().userModel;
// Private but used in tests.
@@ -420,7 +423,7 @@
this.subscriptions.push(
combineLatest([
this.changeModel.currentPatchNum$,
- routerView$,
+ this.routerModel.routerView$,
this.changeModel.diffPath$,
this.userModel.diffPreferences$,
])
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index 4c4361d..b2d5a27 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -21,7 +21,7 @@
import {ChangeStatus, DiffViewMode, createDefaultDiffPrefs} from '../../../constants/constants.js';
import {stubRestApi, stubUsers, waitUntil} from '../../../test/test-utils.js';
import {ChangeComments} from '../gr-comment-api/gr-comment-api.js';
-import {GerritView, _testOnly_setState as setRouterModelState} from '../../../services/router/router-model.js';
+import {GerritView} from '../../../services/router/router-model.js';
import {
createChange,
createRevisions,
@@ -1206,7 +1206,7 @@
change: createChange(),
diffPath: '/COMMIT_MSG'});
- setRouterModelState({
+ element.routerModel.setState({
changeNum: TEST_NUMERIC_CHANGE_ID, view: GerritView.DIFF, patchNum: 2}
);
element._patchRange = {
@@ -1245,7 +1245,7 @@
change: createChange(),
diffPath: '/COMMIT_MSG'});
- setRouterModelState({
+ element.routerModel.setState({
changeNum: TEST_NUMERIC_CHANGE_ID, view: GerritView.DIFF,
patchNum: 22}
);
@@ -1271,7 +1271,7 @@
change: createChange(),
diffPath: '/COMMIT_MSG'});
- setRouterModelState({
+ element.routerModel.setState({
changeNum: TEST_NUMERIC_CHANGE_ID, view: GerritView.DIFF, patchNum: 2}
);
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
index 9b3e75d..38ed276 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -96,6 +96,9 @@
userModel: (_ctx: Partial<AppContext>) => {
throw new Error('userModel is not implemented');
},
+ routerModel: (_ctx: Partial<AppContext>) => {
+ throw new Error('routerModel is not implemented');
+ },
shortcutsService: (_ctx: Partial<AppContext>) => {
throw new Error('shortcutsService is not implemented');
},
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 46d2178..fbf5b0f 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -27,6 +27,7 @@
import {GrStorageService} from './storage/gr-storage_impl';
import {UserModel} from './user/user-model';
import {CommentsModel} from './comments/comments-model';
+import {RouterModel} from './router/router-model';
import {ShortcutsService} from './shortcuts/shortcuts-service';
import {BrowserModel} from './browser/browser-model';
import {assertIsDefined} from '../utils/common-util';
@@ -37,6 +38,7 @@
*/
export function createAppContext(): AppContext & Finalizable {
const appRegistry: Registry<AppContext> = {
+ routerModel: (_ctx: Partial<AppContext>) => new RouterModel(),
flagsService: (_ctx: Partial<AppContext>) =>
new FlagsServiceImplementation(),
reportingService: (ctx: Partial<AppContext>) => {
@@ -54,28 +56,41 @@
return new GrRestApiServiceImpl(ctx.authService!, ctx.flagsService!);
},
changeModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.restApiService, 'restApiService');
- return new ChangeModel(ctx.restApiService!);
+ const routerModel = ctx.routerModel;
+ const restApiService = ctx.restApiService;
+ assertIsDefined(routerModel, 'routerModel');
+ assertIsDefined(restApiService, 'restApiService');
+ return new ChangeModel(routerModel, restApiService);
},
commentsModel: (ctx: Partial<AppContext>) => {
+ const routerModel = ctx.routerModel;
const changeModel = ctx.changeModel;
const restApiService = ctx.restApiService;
- const reporting = ctx.reportingService;
+ const reportingService = ctx.reportingService;
+ assertIsDefined(routerModel, 'routerModel');
assertIsDefined(changeModel, 'changeModel');
assertIsDefined(restApiService, 'restApiService');
- assertIsDefined(reporting, 'reportingService');
- return new CommentsModel(changeModel, restApiService, reporting);
+ assertIsDefined(reportingService, 'reportingService');
+ return new CommentsModel(
+ routerModel,
+ changeModel,
+ restApiService,
+ reportingService
+ );
},
checksModel: (ctx: Partial<AppContext>) => {
+ const routerModel = ctx.routerModel;
const changeModel = ctx.changeModel;
- const reporting = ctx.reportingService;
+ const reportingService = ctx.reportingService;
+ assertIsDefined(routerModel, 'routerModel');
assertIsDefined(changeModel, 'changeModel');
- assertIsDefined(reporting, 'reportingService');
- return new ChecksModel(changeModel, reporting);
+ assertIsDefined(reportingService, 'reportingService');
+ return new ChecksModel(routerModel, changeModel, reportingService);
},
jsApiService: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.reportingService, 'reportingService');
- return new GrJsApiInterface(ctx.reportingService!);
+ const reportingService = ctx.reportingService;
+ assertIsDefined(reportingService, 'reportingService');
+ return new GrJsApiInterface(reportingService!);
},
storageService: (_ctx: Partial<AppContext>) => new GrStorageService(),
configModel: (ctx: Partial<AppContext>) => {
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index bea371f..367fee7 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -26,11 +26,13 @@
import {StorageService} from './storage/gr-storage';
import {UserModel} from './user/user-model';
import {CommentsModel} from './comments/comments-model';
+import {RouterModel} from './router/router-model';
import {ShortcutsService} from './shortcuts/shortcuts-service';
import {BrowserModel} from './browser/browser-model';
import {ConfigModel} from './config/config-model';
export interface AppContext {
+ routerModel: RouterModel;
flagsService: FlagsService;
reportingService: ReportingService;
eventEmitter: EventEmitterService;
diff --git a/polygerrit-ui/app/services/change/change-model.ts b/polygerrit-ui/app/services/change/change-model.ts
index 6a819f8..7055447 100644
--- a/polygerrit-ui/app/services/change/change-model.ts
+++ b/polygerrit-ui/app/services/change/change-model.ts
@@ -32,14 +32,13 @@
startWith,
switchMap,
} from 'rxjs/operators';
-import {routerPatchNum$, routerState$} from '../router/router-model';
+import {RouterModel} from '../router/router-model';
import {
computeAllPatchSets,
computeLatestPatchNum,
} from '../../utils/patch-set-util';
import {ParsedChangeInfo} from '../../types/types';
-import {routerChangeNum$} from '../router/router-model';
import {ChangeInfo} from '../../types/common';
import {RestApiService} from '../gr-rest-api/gr-rest-api';
import {Finalizable} from '../registry';
@@ -118,7 +117,7 @@
* out inconsistent state, e.g. router changeNum already updated, change not
* yet reset to undefined.
*/
- combineLatest([routerState$, this.changeState$])
+ combineLatest([this.routerModel.routerState$, this.changeState$])
.pipe(
filter(([routerState, changeState]) => {
const changeNum = changeState.change?._number;
@@ -128,7 +127,7 @@
distinctUntilChanged()
)
.pipe(
- withLatestFrom(routerPatchNum$, this.latestPatchNum$),
+ withLatestFrom(this.routerModel.routerPatchNum$, this.latestPatchNum$),
map(([_, routerPatchN, latestPatchN]) => routerPatchN || latestPatchN),
distinctUntilChanged()
);
@@ -142,9 +141,12 @@
'reload'
).pipe(startWith(undefined));
- constructor(readonly restApiService: RestApiService) {
+ constructor(
+ readonly routerModel: RouterModel,
+ readonly restApiService: RestApiService
+ ) {
this.subscriptions = [
- combineLatest([routerChangeNum$, this.reload$])
+ combineLatest([this.routerModel.routerChangeNum$, this.reload$])
.pipe(
map(([changeNum, _]) => changeNum),
switchMap(changeNum => {
diff --git a/polygerrit-ui/app/services/change/change-model_test.ts b/polygerrit-ui/app/services/change/change-model_test.ts
index 099a11f..0fb9712 100644
--- a/polygerrit-ui/app/services/change/change-model_test.ts
+++ b/polygerrit-ui/app/services/change/change-model_test.ts
@@ -28,10 +28,7 @@
import {CommitId, NumericChangeId, PatchSetNum} from '../../types/common';
import {ParsedChangeInfo} from '../../types/types';
import {getAppContext} from '../app-context';
-import {
- GerritView,
- _testOnly_setState as setRouterState,
-} from '../router/router-model';
+import {GerritView} from '../router/router-model';
import {ChangeState, LoadingStatus} from './change-model';
import {ChangeModel} from './change-model';
@@ -40,7 +37,10 @@
let knownChange: ParsedChangeInfo;
const testCompleted = new Subject<void>();
setup(() => {
- changeModel = new ChangeModel(getAppContext().restApiService);
+ changeModel = new ChangeModel(
+ getAppContext().routerModel,
+ getAppContext().restApiService
+ );
knownChange = {
...createChange(),
revisions: {
@@ -80,7 +80,10 @@
assert.equal(stub.callCount, 0);
assert.isUndefined(state?.change);
- setRouterState({view: GerritView.CHANGE, changeNum: knownChange._number});
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: knownChange._number,
+ });
await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADING);
assert.equal(stub.callCount, 1);
assert.isUndefined(state?.change);
@@ -101,7 +104,10 @@
changeModel.changeState$
.pipe(takeUntil(testCompleted))
.subscribe(s => (state = s));
- setRouterState({view: GerritView.CHANGE, changeNum: knownChange._number});
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: knownChange._number,
+ });
promise.resolve(knownChange);
await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
@@ -127,7 +133,10 @@
changeModel.changeState$
.pipe(takeUntil(testCompleted))
.subscribe(s => (state = s));
- setRouterState({view: GerritView.CHANGE, changeNum: knownChange._number});
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: knownChange._number,
+ });
promise.resolve(knownChange);
await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
@@ -138,7 +147,10 @@
_number: 123 as NumericChangeId,
};
promise = mockPromise<ParsedChangeInfo | undefined>();
- setRouterState({view: GerritView.CHANGE, changeNum: otherChange._number});
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: otherChange._number,
+ });
await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADING);
assert.equal(stub.callCount, 2);
assert.isUndefined(state?.change);
@@ -159,7 +171,10 @@
changeModel.changeState$
.pipe(takeUntil(testCompleted))
.subscribe(s => (state = s));
- setRouterState({view: GerritView.CHANGE, changeNum: knownChange._number});
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: knownChange._number,
+ });
promise.resolve(knownChange);
await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
@@ -167,7 +182,10 @@
promise = mockPromise<ParsedChangeInfo | undefined>();
promise.resolve(undefined);
- setRouterState({view: GerritView.DASHBOARD, changeNum: undefined});
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: undefined,
+ });
await waitUntil(() => state?.loadingStatus === LoadingStatus.NOT_LOADED);
assert.equal(stub.callCount, 2);
assert.isUndefined(state?.change);
@@ -176,7 +194,10 @@
promise = mockPromise<ParsedChangeInfo | undefined>();
promise.resolve(knownChange);
- setRouterState({view: GerritView.CHANGE, changeNum: knownChange._number});
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: knownChange._number,
+ });
await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
assert.equal(stub.callCount, 3);
assert.equal(state?.change, knownChange);
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index 4024747..134afd8 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -53,9 +53,9 @@
import {getCurrentRevision} from '../../utils/change-util';
import {getShaByPatchNum} from '../../utils/patch-set-util';
import {ReportingService} from '../gr-reporting/gr-reporting';
-import {routerPatchNum$} from '../router/router-model';
import {Execution} from '../../constants/reporting';
import {fireAlert, fireEvent} from '../../utils/event-util';
+import {RouterModel} from '../router/router-model';
/**
* The checks model maintains the state of checks for two patchsets: the latest
@@ -322,6 +322,7 @@
);
constructor(
+ readonly routerModel: RouterModel,
readonly changeModel: ChangeModel,
readonly reporting: ReportingService
) {
@@ -331,14 +332,14 @@
this.checkToPluginMap = map;
}),
combineLatest([
- routerPatchNum$,
+ this.routerModel.routerPatchNum$,
this.changeModel.latestPatchNum$,
]).subscribe(([routerPs, latestPs]) => {
this.latestPatchNum = latestPs;
if (latestPs === undefined) {
this.setPatchset(undefined);
} else if (typeof routerPs === 'number') {
- this.setPatchset(routerPs);
+ this.setPatchset(routerPs as PatchSetNumber);
} else {
this.setPatchset(latestPs);
}
diff --git a/polygerrit-ui/app/services/checks/checks-model_test.ts b/polygerrit-ui/app/services/checks/checks-model_test.ts
index 0d46289..bb90fe2 100644
--- a/polygerrit-ui/app/services/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/services/checks/checks-model_test.ts
@@ -46,6 +46,7 @@
setup(() => {
model = new ChecksModel(
+ getAppContext().routerModel,
getAppContext().changeModel,
getAppContext().reportingService
);
diff --git a/polygerrit-ui/app/services/comments/comments-model.ts b/polygerrit-ui/app/services/comments/comments-model.ts
index d46e08a..95a1030 100644
--- a/polygerrit-ui/app/services/comments/comments-model.ts
+++ b/polygerrit-ui/app/services/comments/comments-model.ts
@@ -37,7 +37,7 @@
} from '../../utils/comment-util';
import {deepEqual} from '../../utils/deep-util';
import {select} from '../../utils/observable-util';
-import {routerChangeNum$} from '../router/router-model';
+import {RouterModel} from '../router/router-model';
import {Finalizable} from '../registry';
import {combineLatest, Subscription} from 'rxjs';
import {fire, fireAlert, fireEvent} from '../../utils/event-util';
@@ -291,6 +291,7 @@
private discardedDrafts: DraftInfo[] = [];
constructor(
+ readonly routerModel: RouterModel,
readonly changeModel: ChangeModel,
readonly restApiService: RestApiService,
readonly reporting: ReportingService
@@ -305,7 +306,7 @@
this.changeModel.currentPatchNum$.subscribe(x => (this.patchNum = x))
);
this.subscriptions.push(
- routerChangeNum$.subscribe(changeNum => {
+ this.routerModel.routerChangeNum$.subscribe(changeNum => {
this.changeNum = changeNum;
this.setState({...initialState});
this.reloadAllComments();
diff --git a/polygerrit-ui/app/services/comments/comments-model_test.ts b/polygerrit-ui/app/services/comments/comments-model_test.ts
index f39cad8..a8f2118 100644
--- a/polygerrit-ui/app/services/comments/comments-model_test.ts
+++ b/polygerrit-ui/app/services/comments/comments-model_test.ts
@@ -30,10 +30,7 @@
} from '../../test/test-data-generators';
import {stubRestApi, waitUntil, waitUntilCalled} from '../../test/test-utils';
import {getAppContext} from '../app-context';
-import {
- GerritView,
- updateState as updateRouterState,
-} from '../router/router-model';
+import {GerritView} from '../router/router-model';
import {PathToCommentsInfoMap} from '../../types/common';
suite('comments model tests', () => {
@@ -76,6 +73,7 @@
test('loads comments', async () => {
const model = new CommentsModel(
+ getAppContext().routerModel,
getAppContext().changeModel,
getAppContext().restApiService,
getAppContext().reportingService
@@ -102,7 +100,10 @@
model.portedComments$.subscribe(c => (portedComments = c ?? {}))
);
- updateRouterState(GerritView.CHANGE, TEST_NUMERIC_CHANGE_ID);
+ model.routerModel.updateState({
+ view: GerritView.CHANGE,
+ changeNum: TEST_NUMERIC_CHANGE_ID,
+ });
model.changeModel.updateStateChange(createParsedChange());
await waitUntilCalled(diffCommentsSpy, 'diffCommentsSpy');
diff --git a/polygerrit-ui/app/services/router/router-model.ts b/polygerrit-ui/app/services/router/router-model.ts
index f549e859..73dee78 100644
--- a/polygerrit-ui/app/services/router/router-model.ts
+++ b/polygerrit-ui/app/services/router/router-model.ts
@@ -15,9 +15,10 @@
* limitations under the License.
*/
-import {NumericChangeId, PatchSetNum} from '../../types/common';
import {BehaviorSubject, Observable} from 'rxjs';
import {distinctUntilChanged, map} from 'rxjs/operators';
+import {Finalizable} from '../registry';
+import {NumericChangeId, PatchSetNum} from '../../types/common';
export enum GerritView {
ADMIN = 'admin',
@@ -42,57 +43,44 @@
patchNum?: PatchSetNum;
}
-// TODO: Figure out how to best enforce immutability of all states. Use Immer?
-// Use DeepReadOnly?
-const initialState: RouterState = {};
+export class RouterModel implements Finalizable {
+ private readonly privateState$ = new BehaviorSubject<RouterState>({});
-const privateState$ = new BehaviorSubject<RouterState>(initialState);
+ readonly routerView$: Observable<GerritView | undefined>;
-export function _testOnly_resetState() {
- // We cannot assign a new subject to privateState$, because all the selectors
- // have already subscribed to the original subject. So we have to emit the
- // initial state on the existing subject.
- privateState$.next({...initialState});
+ readonly routerChangeNum$: Observable<NumericChangeId | undefined>;
+
+ readonly routerPatchNum$: Observable<PatchSetNum | undefined>;
+
+ constructor() {
+ this.routerView$ = this.privateState$.pipe(
+ map(state => state.view),
+ distinctUntilChanged()
+ );
+ this.routerChangeNum$ = this.privateState$.pipe(
+ map(state => state.changeNum),
+ distinctUntilChanged()
+ );
+ this.routerPatchNum$ = this.privateState$.pipe(
+ map(state => state.patchNum),
+ distinctUntilChanged()
+ );
+ }
+
+ finalize() {}
+
+ setState(state: RouterState) {
+ this.privateState$.next(state);
+ }
+
+ updateState(partial: Partial<RouterState>) {
+ this.privateState$.next({
+ ...this.privateState$.getValue(),
+ ...partial,
+ });
+ }
+
+ get routerState$(): Observable<RouterState> {
+ return this.privateState$;
+ }
}
-
-export function _testOnly_setState(state: RouterState) {
- privateState$.next(state);
-}
-
-export function _testOnly_getState() {
- return privateState$.getValue();
-}
-
-// Re-exporting as Observable so that you can only subscribe, but not emit.
-export const routerState$: Observable<RouterState> = privateState$;
-
-// Must only be used by the router service or whatever is in control of this
-// model.
-// TODO: Consider keeping params of type AppElementParams entirely in the state
-export function updateState(
- view?: GerritView,
- changeNum?: NumericChangeId,
- patchNum?: PatchSetNum
-) {
- privateState$.next({
- ...privateState$.getValue(),
- view,
- changeNum,
- patchNum,
- });
-}
-
-export const routerView$ = routerState$.pipe(
- map(state => state.view),
- distinctUntilChanged()
-);
-
-export const routerChangeNum$ = routerState$.pipe(
- map(state => state.changeNum),
- distinctUntilChanged()
-);
-
-export const routerPatchNum$ = routerState$.pipe(
- map(state => state.patchNum),
- distinctUntilChanged()
-);
diff --git a/polygerrit-ui/app/services/user/user-model.ts b/polygerrit-ui/app/services/user/user-model.ts
index 9107b31d..441c09d 100644
--- a/polygerrit-ui/app/services/user/user-model.ts
+++ b/polygerrit-ui/app/services/user/user-model.ts
@@ -85,7 +85,7 @@
preference => preference.diff_view ?? DiffViewMode.SIDE_BY_SIDE
);
- private readonly subscriptions: Subscription[] = [];
+ private subscriptions: Subscription[] = [];
get userState$(): Observable<UserState> {
return this.privateState$;
@@ -135,7 +135,7 @@
for (const s of this.subscriptions) {
s.unsubscribe();
}
- this.subscriptions.splice(0, this.subscriptions.length);
+ this.subscriptions = [];
}
updatePreferences(prefs: Partial<PreferencesInfo>) {
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index b8a5e49..94e3f69 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -45,8 +45,6 @@
import {_testOnly_allTasks} from '../utils/async-util';
import {cleanUpStorage} from '../services/storage/gr-storage_mock';
-import {_testOnly_resetState as resetRouterState} from '../services/router/router-model';
-
declare global {
interface Window {
assert: typeof chai.assert;
@@ -111,8 +109,6 @@
// tests.
initGlobalVariables(appContext);
- resetRouterState();
-
const shortcuts = appContext.shortcutsService;
assert.isTrue(shortcuts._testOnly_isEmpty());
const selection = document.getSelection();
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 01d776e..5f5507b 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -30,12 +30,14 @@
import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
import {UserModel} from '../services/user/user-model';
import {CommentsModel} from '../services/comments/comments-model';
+import {RouterModel} from '../services/router/router-model';
import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
import {BrowserModel} from '../services/browser/browser-model';
import {ConfigModel} from '../services/config/config-model';
export function createTestAppContext(): AppContext & Finalizable {
const appRegistry: Registry<AppContext> = {
+ routerModel: (_ctx: Partial<AppContext>) => new RouterModel(),
flagsService: (_ctx: Partial<AppContext>) =>
new FlagsServiceImplementation(),
reportingService: (_ctx: Partial<AppContext>) => grReportingMock,
@@ -46,23 +48,36 @@
},
restApiService: (_ctx: Partial<AppContext>) => grRestApiMock,
changeModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.restApiService, 'restApiService');
- return new ChangeModel(ctx.restApiService!);
+ const routerModel = ctx.routerModel;
+ const restApiService = ctx.restApiService;
+ assertIsDefined(routerModel, 'routerModel');
+ assertIsDefined(restApiService, 'restApiService');
+ return new ChangeModel(routerModel, restApiService);
},
commentsModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.changeModel, 'changeModel');
- assertIsDefined(ctx.restApiService, 'restApiService');
- assertIsDefined(ctx.reportingService, 'reportingService');
+ const routerModel = ctx.routerModel;
+ const changeModel = ctx.changeModel;
+ const restApiService = ctx.restApiService;
+ const reportingService = ctx.reportingService;
+ assertIsDefined(routerModel, 'routerModel');
+ assertIsDefined(changeModel, 'changeModel');
+ assertIsDefined(restApiService, 'restApiService');
+ assertIsDefined(reportingService, 'reportingService');
return new CommentsModel(
- ctx.changeModel!,
- ctx.restApiService!,
- ctx.reportingService!
+ routerModel,
+ changeModel,
+ restApiService,
+ reportingService
);
},
checksModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.changeModel, 'changeModel');
- assertIsDefined(ctx.reportingService, 'reportingService');
- return new ChecksModel(ctx.changeModel!, ctx.reportingService!);
+ const routerModel = ctx.routerModel;
+ const changeModel = ctx.changeModel;
+ const reportingService = ctx.reportingService;
+ assertIsDefined(routerModel, 'routerModel');
+ assertIsDefined(changeModel, 'changeModel');
+ assertIsDefined(reportingService, 'reportingService');
+ return new ChecksModel(routerModel, changeModel, reportingService);
},
jsApiService: (ctx: Partial<AppContext>) => {
assertIsDefined(ctx.reportingService, 'reportingService');
diff --git a/proto/cache.proto b/proto/cache.proto
index 16e5e95..750bea1 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -718,18 +718,3 @@
ComparisonType comparison_type = 11;
bool negative = 12;
}
-
-// Serialized form of com.google.gerrit.server.approval.ApprovalCacheImpl.Key.
-// Next ID: 5
-message PatchSetApprovalsKeyProto {
- string project = 1;
- int32 change_id = 2;
- int32 patch_set_id = 3;
- bytes id = 4;
-}
-
-// Repeated version of PatchSetApprovalProto
-// Next ID: 2
-message AllPatchSetApprovalsProto {
- repeated devtools.gerritcodereview.PatchSetApproval approval = 1;
-}
diff --git a/tools/bzl/license-map.py b/tools/bzl/license-map.py
index 43b172c..79285e6 100644
--- a/tools/bzl/license-map.py
+++ b/tools/bzl/license-map.py
@@ -170,7 +170,7 @@
Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
uploads of changes directly from `git push` command line clients.
-Gerrit includes an SSH client (JSch), to support authenticated
+Gerrit includes an SSH client (Apache SSHD), to support authenticated
replication of changes to remote systems, such as for automatic
updates of mirror servers, or realtime backups.
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index 2b473bc..c9ac0fe 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -23,7 +23,6 @@
"//lib/bouncycastle:bcprov",
"//lib/bouncycastle:bcpg",
"//lib/log:impl-log4j",
- "//lib:jgit-ssh-jsch",
"//prolog:gerrit-prolog-common",
"//resources:log4j-config",
]
diff --git a/tools/deps.bzl b/tools/deps.bzl
index 89c5584..94d0e98 100644
--- a/tools/deps.bzl
+++ b/tools/deps.bzl
@@ -75,12 +75,6 @@
)
maven_jar(
- name = "jzlib",
- artifact = "com.jcraft:jzlib:1.1.1",
- sha1 = "a1551373315ffc2f96130a0e5704f74e151777ba",
- )
-
- maven_jar(
name = "javaewah",
artifact = "com.googlecode.javaewah:JavaEWAH:1.1.6",
attach_source = False,
@@ -112,12 +106,6 @@
)
maven_jar(
- name = "jsch",
- artifact = "com.jcraft:jsch:0.1.54",
- sha1 = "da3584329a263616e277e15462b387addd1b208d",
- )
-
- maven_jar(
name = "juniversalchardet",
artifact = "com.github.albfernandez:juniversalchardet:2.0.0",
sha1 = "28c59f58f5adcc307604602e2aa89e2aca14c554",
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index ef20ace..ed5b464 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -186,8 +186,6 @@
classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/src')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/resources')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.junit/src')
- classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/src')
- classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/resources')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.apache/src')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.apache/resources')