Merge "Compute diff URLs in the view model"
diff --git a/java/com/google/gerrit/acceptance/TestConfigRule.java b/java/com/google/gerrit/acceptance/TestConfigRule.java
index a7f051a..e2ae416 100644
--- a/java/com/google/gerrit/acceptance/TestConfigRule.java
+++ b/java/com/google/gerrit/acceptance/TestConfigRule.java
@@ -47,8 +47,11 @@
@Override
public void evaluate() throws Throwable {
setTestConfigFromDescription(description);
- statement.evaluate();
- clear();
+ try {
+ statement.evaluate();
+ } finally {
+ clear();
+ }
}
};
}
diff --git a/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
index d9f1c09..5d15a56 100644
--- a/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
+++ b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
@@ -24,6 +24,8 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
import java.util.Optional;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@@ -58,11 +60,13 @@
int commentIdx = uriPath.indexOf("/comment");
String idString = commentIdx == -1 ? uriPath : uriPath.substring(0, commentIdx);
- if (idString.endsWith("/")) {
- idString = idString.substring(0, idString.length() - 1);
- }
+ List<String> uriSegments = Arrays.stream(idString.split("/")).toList();
+
+ idString = uriSegments.get(0);
+ String psString = (uriSegments.size() > 1) ? uriSegments.get(1) : null;
+
Optional<Change.Id> id = Change.Id.tryParse(idString);
- if (!id.isPresent()) {
+ if (id.isEmpty()) {
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
@@ -81,6 +85,8 @@
if (commentIdx > -1) {
// path already contain a trailing /, hence we start from "commentIdx + 1"
path = path + uriPath.substring(commentIdx + 1);
+ } else if (psString != null) {
+ path += psString;
}
UrlModule.toGerrit(path, req, rsp);
}
diff --git a/java/com/google/gerrit/httpd/UrlModule.java b/java/com/google/gerrit/httpd/UrlModule.java
index 7a100c7..1d62efe 100644
--- a/java/com/google/gerrit/httpd/UrlModule.java
+++ b/java/com/google/gerrit/httpd/UrlModule.java
@@ -73,6 +73,8 @@
serveRegex("^/register$").with(registerScreen(false));
serveRegex("^/register/(.+)$").with(registerScreen(true));
serveRegex("^(?:/c)?/([1-9][0-9]*)/?$").with(NumericChangeIdRedirectServlet.class);
+ serveRegex("^(?:/c)?/([1-9][0-9]*)/([1-9][0-9]*)/?$")
+ .with(NumericChangeIdRedirectServlet.class);
serveRegex("^(?:/c)?/([1-9][0-9]*)/comment/\\w+/?$").with(NumericChangeIdRedirectServlet.class);
serveRegex("^/p/(.*)$").with(queryProjectNew());
serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 68061bd..e515dcc 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -1592,6 +1592,15 @@
} else {
rejectProhibited(cmd, err.get());
}
+ if (ObjectId.zeroId().equals(cmd.getOldId())) {
+ // Git CLI sends DELETE 0..0 0...0 when the server doesn't send the deleted ref during
+ // negotiation. The server usually doesn't send it when ref doesn't exist or when it
+ // is not visible to a caller - so the message that the ref doesn't exist should be ok
+ // here.
+ // Without this check, such delete always fails with the "internal error" message, caused
+ // by the checkArgument in the ChainedReceiveCommands#add.
+ reject(cmd, String.format("The ref %s doesn't exist", cmd.getRefName()));
+ }
}
}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 5c7d524..2311240 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -356,6 +356,7 @@
throw new CommitValidationException(MISSING_CHANGE_ID_MSG, messages);
}
} else if (idList.size() > 1) {
+ messages.add(getMultipleChangeIdsErrorMsg(idList));
throw new CommitValidationException(MULTIPLE_CHANGE_ID_MSG, messages);
} else {
String v = idList.get(0).trim();
@@ -391,6 +392,24 @@
ValidationMessage.Type.ERROR);
}
+ private CommitValidationMessage getMultipleChangeIdsErrorMsg(List<String> idList) {
+ return new CommitValidationMessage(
+ MULTIPLE_CHANGE_ID_MSG
+ + "\n"
+ + "\nHint: the following Change-Ids were found:\n"
+ + idList.stream()
+ .map(
+ id ->
+ "* "
+ + id
+ + " ["
+ + (CHANGE_ID.matcher(id.trim()).matches() ? "VALID" : "INVALID")
+ + "]")
+ .collect(Collectors.joining("\n"))
+ + "\n",
+ ValidationMessage.Type.ERROR);
+ }
+
private String getCommitMessageHookInstallationHint() {
if (installCommitMsgHookCommand != null) {
return installCommitMsgHookCommand;
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index ac9ac98..dbdd26f 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -21,6 +21,8 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.LabelType;
@@ -261,6 +263,19 @@
}
return allowed;
}
+
+ /**
+ * Additional filter for changes query for reducing the cardinality of the results for current
+ * user.
+ *
+ * @return additional query filter to add to all user's change queries, null if no filters are
+ * required.
+ * @since 3.11
+ */
+ @UsedAt(UsedAt.Project.MODULE_VIRTUALHOST)
+ public @Nullable String filterQueryChanges() {
+ return null;
+ }
}
/** PermissionBackend scoped to a user and project. */
diff --git a/java/com/google/gerrit/server/plugins/Plugin.java b/java/com/google/gerrit/server/plugins/Plugin.java
index b5ff041..3de7e27 100644
--- a/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/java/com/google/gerrit/server/plugins/Plugin.java
@@ -78,7 +78,7 @@
protected LifecycleManager manager;
- private List<ReloadableRegistrationHandle<?>> reloadableHandles;
+ protected List<ReloadableRegistrationHandle<?>> reloadableHandles;
public Plugin(
String name, Path srcPath, PluginUser pluginUser, FileSnapshot snapshot, ApiType apiType) {
diff --git a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 0726913..8cfc6f3 100644
--- a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -355,6 +355,23 @@
reattachItem(old, sysItems, newPlugin.getSysInjector(), newPlugin);
reattachItem(old, sshItems, newPlugin.getSshInjector(), newPlugin);
reattachItem(old, httpItems, newPlugin.getHttpInjector(), newPlugin);
+
+ apiInjector = Optional.ofNullable(newPlugin.getApiInjector()).orElse(apiInjector);
+
+ if (apiInjector != null) {
+ apiItems.putAll(dynamicItemsOf(apiInjector));
+ apiSets.putAll(dynamicSetsOf(apiInjector));
+ apiMaps.putAll(dynamicMapsOf(apiInjector));
+
+ ImmutableList<Injector> allPluginInjectors =
+ listOfInjectors(
+ newPlugin.getSysInjector(),
+ newPlugin.getSshInjector(),
+ newPlugin.getHttpInjector());
+ allPluginInjectors.forEach(i -> reattachItem(old, apiItems, i, newPlugin));
+ allPluginInjectors.forEach(i -> reattachSet(old, apiSets, i, newPlugin));
+ allPluginInjectors.forEach(i -> reattachMap(old, apiMaps, i, newPlugin));
+ }
} finally {
exit(oldContext);
}
diff --git a/java/com/google/gerrit/server/plugins/ServerPlugin.java b/java/com/google/gerrit/server/plugins/ServerPlugin.java
index bd83b98..fb7cbe2 100644
--- a/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -58,7 +58,6 @@
private Injector sshInjector;
private Injector httpInjector;
private LifecycleManager serverManager;
- private List<ReloadableRegistrationHandle<?>> reloadableHandles;
private Optional<Module> apiModule = Optional.empty();
diff --git a/java/com/google/gerrit/server/restapi/change/QueryChanges.java b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
index 812711a..d05cbf6 100644
--- a/java/com/google/gerrit/server/restapi/change/QueryChanges.java
+++ b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.change;
+import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.client.ListChangesOption;
@@ -155,6 +156,7 @@
throws BadRequestException, AuthException, PermissionBackendException {
List<List<ChangeInfo>> out;
try {
+ applyPermissionBackendFilter();
out = query();
} catch (QueryRequiresAuthException e) {
throw new AuthException("Must be signed-in to use this operator", e);
@@ -165,6 +167,22 @@
return Response.ok(out.size() == 1 ? out.get(0) : out);
}
+ private void applyPermissionBackendFilter() {
+ String queryFilter = permissionBackend.currentUser().filterQueryChanges();
+ if (Strings.isNullOrEmpty(queryFilter)) {
+ return;
+ }
+
+ if (queries == null || queries.isEmpty()) {
+ addQuery(queryFilter);
+ return;
+ }
+
+ for (int i = 0; i < queries.size(); i++) {
+ queries.set(i, queries.get(i) + " " + queryFilter);
+ }
+ }
+
private List<List<ChangeInfo>> query()
throws BadRequestException, QueryParseException, PermissionBackendException {
ChangeQueryProcessor queryProcessor = queryProcessorProvider.get();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesFilterPermissionBackendIT.java b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesFilterPermissionBackendIT.java
new file mode 100644
index 0000000..07d32fe
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesFilterPermissionBackendIT.java
@@ -0,0 +1,147 @@
+// Copyright (C) 2024 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.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.conditions.BooleanCondition;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.DefaultPermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.google.inject.Singleton;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Test;
+
+public class QueryChangesFilterPermissionBackendIT extends AbstractDaemonTest {
+ @Inject private ProjectOperations projectOperations;
+
+ @Singleton
+ public static class TestPermissionBackend extends PermissionBackend {
+ private final DefaultPermissionBackend defaultPermissionBackend;
+ private final AtomicReference<String> extraQueryFilter;
+
+ public static class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(PermissionBackend.class).to(TestPermissionBackend.class).in(Scopes.SINGLETON);
+ }
+ }
+
+ @Inject
+ TestPermissionBackend(DefaultPermissionBackend defaultPermissionBackend) {
+ this.defaultPermissionBackend = defaultPermissionBackend;
+ this.extraQueryFilter = new AtomicReference<>();
+ }
+
+ @Override
+ public WithUser currentUser() {
+ return new TestPermissionWithUser(defaultPermissionBackend.currentUser());
+ }
+
+ @Override
+ public WithUser user(CurrentUser user) {
+ return new TestPermissionWithUser(defaultPermissionBackend.user(user));
+ }
+
+ @Override
+ public WithUser absentUser(Account.Id id) {
+ return new TestPermissionWithUser(defaultPermissionBackend.absentUser(id));
+ }
+
+ public String getExtraQueryFilter() {
+ return extraQueryFilter.get();
+ }
+
+ public void setExtraQueryFilter(String extraQueryFilter) {
+ this.extraQueryFilter.set(extraQueryFilter);
+ }
+
+ class TestPermissionWithUser extends WithUser {
+
+ private final WithUser defaultPermissioBackendWithUser;
+
+ TestPermissionWithUser(WithUser defaultPermissioBackendWithUser) {
+ this.defaultPermissioBackendWithUser = defaultPermissioBackendWithUser;
+ }
+
+ @Override
+ public ForProject project(Project.NameKey project) {
+ return defaultPermissioBackendWithUser.project(project);
+ }
+
+ @Override
+ public void check(GlobalOrPluginPermission perm)
+ throws AuthException, PermissionBackendException {
+ defaultPermissioBackendWithUser.check(perm);
+ }
+
+ @Override
+ public <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet)
+ throws PermissionBackendException {
+ return defaultPermissioBackendWithUser.test(permSet);
+ }
+
+ @Override
+ public BooleanCondition testCond(GlobalOrPluginPermission perm) {
+ return defaultPermissioBackendWithUser.testCond(perm);
+ }
+
+ @Override
+ public String filterQueryChanges() {
+ return extraQueryFilter.get();
+ }
+ }
+ }
+
+ @Override
+ public Module createModule() {
+ return new TestPermissionBackend.Module();
+ }
+
+ @Test
+ public void filterHidenProjectByAuthenticationBackend() throws Exception {
+ String projectChangeId = createChange().getChangeId();
+
+ Project.NameKey hiddenProject = projectOperations.newProject().create();
+ TestRepository<InMemoryRepository> hiddenRepo = cloneProject(hiddenProject, admin);
+ createChange(hiddenRepo);
+
+ assertThat(gApi.changes().query().get()).hasSize(2);
+
+ server
+ .getTestInjector()
+ .getInstance(TestPermissionBackend.class)
+ .setExtraQueryFilter("-project:" + hiddenProject);
+ List<ChangeInfo> projectChanges = gApi.changes().query().get();
+ assertThat(projectChanges.stream().map(c -> c.changeId)).containsExactly(projectChangeId);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
index 5c1f7c2..20554ac 100644
--- a/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
@@ -33,6 +33,7 @@
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.httpd.restapi.ParameterParser;
import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.regex.Pattern;
@@ -449,6 +450,30 @@
}
@Test
+ public void testNumericChangeIdWithPSRedirectWithPrefix() throws Exception {
+ ChangeData changeData = createChange().getChange();
+ int psNumber = changeData.currentPatchSet().id().get();
+ int changeNumber = changeData.getId().get();
+
+ String redirectUri = String.format("/c/%s/+/%d/%d", project.get(), changeNumber, psNumber);
+ anonymousRestSession
+ .get(String.format("/c/%d/%d", changeNumber, psNumber))
+ .assertTemporaryRedirect(redirectUri);
+ }
+
+ @Test
+ public void testNumericChangeIdWithPSAndSlashRedirectWithPrefix() throws Exception {
+ ChangeData changeData = createChange().getChange();
+ int psNumber = changeData.currentPatchSet().id().get();
+ int changeNumber = changeData.getId().get();
+
+ String redirectUri = String.format("/c/%s/+/%d/%d", project.get(), changeNumber, psNumber);
+ anonymousRestSession
+ .get(String.format("/c/%d/%d/", changeNumber, psNumber))
+ .assertTemporaryRedirect(redirectUri);
+ }
+
+ @Test
public void testCommentLinkWithoutPrefixRedirects() throws Exception {
int changeNumber = createChange().getChange().getId().get();
String commentId = "ff3303fd_8341647b";
diff --git a/lib/fonts/material-icons.woff2 b/lib/fonts/material-icons.woff2
index 11074da..4fd4a47 100644
--- a/lib/fonts/material-icons.woff2
+++ b/lib/fonts/material-icons.woff2
Binary files differ
diff --git a/plugins/replication b/plugins/replication
index 56b8ffb..aacb8b2 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 56b8ffbab5bf619c0b6b5d44f0255fd41b9e1c89
+Subproject commit aacb8b2a20267e88ccda811d27293bac66e2006b
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
index 1a0eeea..7edfe2b 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
@@ -208,7 +208,7 @@
return;
return html`
- <h3 class="heading-3">${this.repoConfig?.actions['gc']?.label}</h3>
+ <h2 class="heading-2">${this.repoConfig?.actions['gc']?.label}</h2>
<gr-button
title=${this.repoConfig?.actions['gc']?.title || ''}
?loading=${this.runningGC}
diff --git a/polygerrit-ui/app/styles/material-icons.css b/polygerrit-ui/app/styles/material-icons.css
index 4c0313c..0cce879 100644
--- a/polygerrit-ui/app/styles/material-icons.css
+++ b/polygerrit-ui/app/styles/material-icons.css
@@ -1,8 +1,8 @@
/**
- * This file has been produced by downloading this file on Sep 6, 2022:
+ * This file has been produced by downloading this file on June 11, 2024:
* https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0
- * The corresponding ttf file was downloaded on Sep 6, 2022 from:
- * https://fonts.gstatic.com/s/materialsymbolsoutlined/v51/kJF4BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzBwG-RpA6RzaxHMPdY40KH8nGzv3fzfVJU22ZZLsYEpzC_1ver5Y0J1Llf.woff2
+ * The corresponding ttf file was downloaded on June 11, 2024 from:
+ * https://fonts.gstatic.com/s/materialsymbolsoutlined/v192/kJF4BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzBwG-RpA6RzaxHMPdY40KH8nGzv3fzfVJU22ZZLsYEpzC_1ver5Y0.woff2
*/
@font-face {
font-family: 'Material Symbols Outlined';